diff --git a/OpenRA.Game/Traits/World/ResourceLayer.cs b/OpenRA.Game/Traits/World/ResourceLayer.cs index 1fb5edf034..047bec6d1b 100644 --- a/OpenRA.Game/Traits/World/ResourceLayer.cs +++ b/OpenRA.Game/Traits/World/ResourceLayer.cs @@ -12,12 +12,13 @@ using System; using System.Drawing; using System.Linq; using OpenRA.Graphics; +using System.Collections.Generic; namespace OpenRA.Traits { public class ResourceLayerInfo : TraitInfo { } - public class ResourceLayer: IRenderOverlay, IWorldLoaded + public class ResourceLayer : IRenderOverlay, IWorldLoaded { World world; @@ -26,12 +27,12 @@ namespace OpenRA.Traits bool hasSetupPalettes; - public void Render( WorldRenderer wr ) + public void Render(WorldRenderer wr) { if (!hasSetupPalettes) { hasSetupPalettes = true; - foreach( var rt in world.WorldActor.TraitsImplementing() ) + foreach (var rt in world.WorldActor.TraitsImplementing()) rt.info.PaletteIndex = wr.GetPaletteIndex(rt.info.Palette); } @@ -104,7 +105,7 @@ namespace OpenRA.Traits int sum = 0; for (var u = -1; u < 2; u++) for (var v = -1; v < 2; v++) - if (content[i+u, j+v].type == t) + if (content[i + u, j + v].type == t) ++sum; return sum; } @@ -131,14 +132,14 @@ namespace OpenRA.Traits content[i, j].image.Length - 1, content[i, j].density + n); - world.Map.CustomTerrain[i,j] = t.info.TerrainType; + world.Map.CustomTerrain[i, j] = t.info.TerrainType; } public bool IsFull(int i, int j) { return content[i, j].density == content[i, j].image.Length - 1; } public ResourceType Harvest(CPos p) { - var type = content[p.X,p.Y].type; + var type = content[p.X, p.Y].type; if (type == null) return null; if (--content[p.X, p.Y].density < 0) @@ -163,6 +164,12 @@ namespace OpenRA.Traits } public ResourceType GetResource(CPos p) { return content[p.X, p.Y].type; } + public int GetResourceDensity(CPos p) { return content[p.X, p.Y].density; } + public int GetMaxResourceDensity(CPos p) + { + if (content[p.X, p.Y].image == null) return 0; + return content[p.X, p.Y].image.Length - 1; + } public struct CellContents { diff --git a/OpenRA.Mods.RA/Activities/DeliverResources.cs b/OpenRA.Mods.RA/Activities/DeliverResources.cs index 6a83cf53df..c1835a90b6 100755 --- a/OpenRA.Mods.RA/Activities/DeliverResources.cs +++ b/OpenRA.Mods.RA/Activities/DeliverResources.cs @@ -18,29 +18,48 @@ namespace OpenRA.Mods.RA.Activities public class DeliverResources : Activity { bool isDocking; + int chosenTicks; + + const int NextChooseTime = 100; public DeliverResources() { } - public override Activity Tick( Actor self ) + public override Activity Tick(Actor self) { - if( NextActivity != null ) + if (NextActivity != null) return NextActivity; var mobile = self.Trait(); var harv = self.Trait(); + // Find the nearest best refinery if not explicitly ordered to a specific refinery: + if (harv.OwnerLinkedProc == null || !harv.OwnerLinkedProc.IsInWorld) + { + // Maybe we lost the owner-linked refinery: + harv.OwnerLinkedProc = null; + if (self.World.FrameNumber - chosenTicks > NextChooseTime) + { + harv.ChooseNewProc(self, null); + chosenTicks = self.World.FrameNumber; + } + } + else + { + harv.LinkedProc = harv.OwnerLinkedProc; + } + if (harv.LinkedProc == null || !harv.LinkedProc.IsInWorld) harv.ChooseNewProc(self, null); if (harv.LinkedProc == null) // no procs exist; check again in 1s. - return Util.SequenceActivities( new Wait(25), this ); + return Util.SequenceActivities(new Wait(25), this); var proc = harv.LinkedProc; var iao = proc.Trait(); self.SetTargetLine(Target.FromActor(proc), Color.Green, false); - if( self.Location != proc.Location + iao.DeliverOffset ) - return Util.SequenceActivities( mobile.MoveTo(proc.Location + iao.DeliverOffset, 0), this ); + if (self.Location != proc.Location + iao.DeliverOffset) + return Util.SequenceActivities(mobile.MoveTo(proc.Location + iao.DeliverOffset, 0), this); if (!isDocking) { @@ -48,7 +67,7 @@ namespace OpenRA.Mods.RA.Activities iao.OnDock(self, this); } - return Util.SequenceActivities( new Wait(10), this ); + return Util.SequenceActivities(new Wait(10), this); } // Cannot be cancelled diff --git a/OpenRA.Mods.RA/Activities/FindResources.cs b/OpenRA.Mods.RA/Activities/FindResources.cs index e2108ccb01..f85e7b1479 100755 --- a/OpenRA.Mods.RA/Activities/FindResources.cs +++ b/OpenRA.Mods.RA/Activities/FindResources.cs @@ -14,6 +14,7 @@ using System.Linq; using OpenRA.Mods.RA.Move; using OpenRA.Mods.RA.Render; using OpenRA.Traits; +using System; namespace OpenRA.Mods.RA.Activities { @@ -24,19 +25,85 @@ namespace OpenRA.Mods.RA.Activities if (IsCanceled || NextActivity != null) return NextActivity; var harv = self.Trait(); + if (harv.IsFull) return Util.SequenceActivities(new DeliverResources(), NextActivity); var harvInfo = self.Info.Traits.Get(); var mobile = self.Trait(); var mobileInfo = self.Info.Traits.Get(); - var res = self.World.WorldActor.Trait(); - var path = self.World.WorldActor.Trait().FindPath(PathSearch.Search(self.World, mobileInfo, self.Owner, true) - .WithHeuristic(loc => (res.GetResource(loc) != null && harvInfo.Resources.Contains(res.GetResource(loc).info.Name)) ? 0 : 1) - .FromPoint(self.Location)); + var resLayer = self.World.WorldActor.Trait(); + var territory = self.World.WorldActor.Trait(); + + // Find harvestable resources nearby: + var path = self.World.WorldActor.Trait().FindPath( + PathSearch.Search(self.World, mobileInfo, self.Owner, true) + .WithCustomCost(loc => + { + // Avoid enemy territory: + int safetycost = ( + // TODO: calculate weapons ranges of units and factor those in instead of hard-coding 8. + from u in self.World.FindUnitsInCircle(loc.ToPPos(), Game.CellSize * 8) + where !u.Destroyed + where self.Owner.Stances[u.Owner] == Stance.Enemy + select Math.Max(0, 64 - (loc - u.Location).LengthSquared) + ).Sum(); + + return safetycost; + }) + .WithHeuristic(loc => + { + // Don't harvest out of range: + int distSquared = (loc - (harv.LastOrderLocation ?? harv.LinkedProc.Location)).LengthSquared; + if (distSquared > (12 * 12)) + return int.MaxValue; + + // Get the resource at this location: + var resType = resLayer.GetResource(loc); + + if (resType == null) return 1; + // Can the harvester collect this kind of resource? + if (!harvInfo.Resources.Contains(resType.info.Name)) return 1; + + // Another harvester has claimed this resource: + ResourceClaim claim; + if (territory.IsClaimedByAnyoneElse(self, loc, out claim)) return 1; + + // Is anyone covering the location already? + // NOTE(jsd): This is required to prevent harvester deadlocking. + var unitsAtLoc = + from u in self.World.FindUnits(loc.ToPPos(), loc.ToPPos() + PVecInt.OneCell) + where u != self + select u; + if (unitsAtLoc.Any()) return 1; + + return 0; + }) + .FromPoint(self.Location) + ); if (path.Count == 0) - return NextActivity; + { + if (!harv.IsEmpty) + return new DeliverResources(); + else + { + // Get out of the way if we are: + harv.UnblockRefinery(self); + if (NextActivity != null) + return Util.SequenceActivities(NextActivity, new Wait(90), this); + else + return Util.SequenceActivities(new Wait(90), this); + } + } + + // Attempt to claim a resource as ours: + if (!territory.ClaimResource(self, path[0])) + return Util.SequenceActivities(new Wait(25), this); + + // If not given a direct order, assume ordered to the first resource location we find: + if (harv.LastOrderLocation == null) + harv.LastOrderLocation = path[0]; self.SetTargetLine(Target.FromCell(path[0]), Color.Red, false); return Util.SequenceActivities(mobile.MoveTo(path[0], 1), new HarvestResource(), this); @@ -55,23 +122,38 @@ namespace OpenRA.Mods.RA.Activities public override Activity Tick(Actor self) { if (isHarvesting) return this; - if (IsCanceled) return NextActivity; + + var territory = self.World.WorldActor.Trait(); + if (IsCanceled) + { + territory.UnclaimByActor(self); + return NextActivity; + } + var harv = self.Trait(); harv.LastHarvestedCell = self.Location; if (harv.IsFull) + { + territory.UnclaimByActor(self); return NextActivity; + } + + var resLayer = self.World.WorldActor.Trait(); + var resource = resLayer.Harvest(self.Location); + if (resource == null) + { + territory.UnclaimByActor(self); + return NextActivity; + } var renderUnit = self.Trait(); /* better have one of these! */ - var resource = self.World.WorldActor.Trait().Harvest(self.Location); - if (resource == null) - return NextActivity; - if (renderUnit.anim.CurrentSequence.Name != "harvest") { isHarvesting = true; renderUnit.PlayCustomAnimation(self, "harvest", () => isHarvesting = false); } + harv.AcceptResource(resource); return this; } diff --git a/OpenRA.Mods.RA/Activities/RAHarvesterDockSequence.cs b/OpenRA.Mods.RA/Activities/RAHarvesterDockSequence.cs index d64de01fcd..a94a6693a9 100644 --- a/OpenRA.Mods.RA/Activities/RAHarvesterDockSequence.cs +++ b/OpenRA.Mods.RA/Activities/RAHarvesterDockSequence.cs @@ -55,6 +55,8 @@ namespace OpenRA.Mods.RA state = State.Wait; return this; case State.Complete: + harv.LastLinkedProc = harv.LinkedProc; + harv.LinkedProc = null; return NextActivity; } throw new InvalidOperationException("Invalid harvester dock state"); diff --git a/OpenRA.Mods.RA/Harvester.cs b/OpenRA.Mods.RA/Harvester.cs index 42a0dafc03..d44a9cc6e6 100644 --- a/OpenRA.Mods.RA/Harvester.cs +++ b/OpenRA.Mods.RA/Harvester.cs @@ -30,53 +30,72 @@ namespace OpenRA.Mods.RA } public class Harvester : IIssueOrder, IResolveOrder, IPips, - IExplodeModifier, IOrderVoice, ISpeedModifier, ISync + IExplodeModifier, IOrderVoice, ISpeedModifier, ISync, INotifyResourceClaimLost, INotifyIdle { Dictionary contents = new Dictionary(); + [Sync] public Actor OwnerLinkedProc = null; + [Sync] public Actor LastLinkedProc = null; [Sync] public Actor LinkedProc = null; [Sync] int currentUnloadTicks; public CPos? LastHarvestedCell = null; - [Sync] public int ContentValue { get { return contents.Sum(c => c.Key.ValuePerUnit*c.Value); } } + public CPos? LastOrderLocation = null; + [Sync] public int ContentValue { get { return contents.Sum(c => c.Key.ValuePerUnit * c.Value); } } readonly HarvesterInfo Info; public Harvester(Actor self, HarvesterInfo info) { Info = info; - self.QueueActivity( new CallFunc( () => ChooseNewProc(self, null))); + self.QueueActivity(new CallFunc(() => ChooseNewProc(self, null))); } - public void ChooseNewProc(Actor self, Actor ignore) { LinkedProc = ClosestProc(self, ignore); } + public void ChooseNewProc(Actor self, Actor ignore) + { + LinkedProc = ClosestProc(self, ignore); + LastLinkedProc = null; + } public void ContinueHarvesting(Actor self) { - if (LastHarvestedCell.HasValue) - { - var mobile = self.Trait(); - self.QueueActivity( mobile.MoveTo(LastHarvestedCell.Value, 5) ); - self.SetTargetLine(Target.FromCell(LastHarvestedCell.Value), Color.Red, false); - } - self.QueueActivity( new FindResources() ); + // Move out of the refinery dock and continue harvesting: + UnblockRefinery(self); + self.QueueActivity(new FindResources()); } Actor ClosestProc(Actor self, Actor ignore) { - var refs = self.World.ActorsWithTrait() - .Where(x => x.Actor != ignore && x.Actor.Owner == self.Owner) - .ToList(); + // Find all refineries and their occupancy count: + var refs = ( + from r in self.World.ActorsWithTrait() + where r.Actor != ignore && r.Actor.Owner == self.Owner + let linkedHarvs = self.World.ActorsWithTrait().Where(a => a.Trait.LinkedProc == r.Actor).Count() + select new { Location = r.Actor.Location + r.Trait.DeliverOffset, Actor = r.Actor, Occupancy = linkedHarvs } + ).ToDictionary(r => r.Location); + + // Start a search from each refinery's delivery location: var mi = self.Info.Traits.Get(); var path = self.World.WorldActor.Trait().FindPath( - PathSearch.FromPoints(self.World, mi, self.Owner, - refs.Select(r => r.Actor.Location + r.Trait.DeliverOffset), - self.Location, false)); + PathSearch.FromPoints(self.World, mi, self.Owner, refs.Values.Select(r => r.Location), self.Location, false) + .WithCustomCost((loc) => + { + if (!refs.ContainsKey(loc)) return 0; + int occupancy = refs[loc].Occupancy; + // 4 harvesters clogs up the refinery's delivery location: + if (occupancy >= 3) return int.MaxValue; + + // Prefer refineries with less occupancy (multiplier is to offset distance cost): + return occupancy * 12; + }) + ); + + // Reverse the found-path to find the refinery location instead of our location: path.Reverse(); if (path.Count != 0) - return refs.Where(x => x.Actor.Location + x.Trait.DeliverOffset == path[0]) - .Select(a => a.Actor).FirstOrDefault(); - else - return null; + return refs[path[0]].Actor; + + return null; } public bool IsFull { get { return contents.Values.Sum() == Info.Capacity; } } @@ -89,6 +108,43 @@ namespace OpenRA.Mods.RA else contents[type.info]++; } + public void UnblockRefinery(Actor self) + { + // Check that we're not in a critical location and being useless (refinery drop-off): + var lastproc = LastLinkedProc ?? LinkedProc; + if (lastproc != null) + { + var deliveryLoc = lastproc.Location + lastproc.Trait().DeliverOffset; + if (self.Location == deliveryLoc) + { + // Get out of the way: + var mobile = self.Trait(); + var harv = self.Trait(); + // TODO: would like an interruptible move here + var moveTo = harv.LastHarvestedCell ?? (deliveryLoc + new CVec(0, 4)); + self.QueueActivity(mobile.MoveTo(moveTo, 1)); + self.SetTargetLine(Target.FromCell(moveTo), Color.Red, false); + self.World.WorldActor.Trait().ClaimResource(self, moveTo); + return; + } + } + } + + public void TickIdle(Actor self) + { + // Are we not empty? Deliver resources: + if (!IsEmpty) + { + self.QueueActivity(new DeliverResources()); + return; + } + + UnblockRefinery(self); + + // Wait for a bit before becoming idle again: + self.QueueActivity(new Wait(10)); + } + // Returns true when unloading is complete public bool TickUnload(Actor self, Actor proc) { @@ -125,12 +181,12 @@ namespace OpenRA.Mods.RA } } - public Order IssueOrder( Actor self, IOrderTargeter order, Target target, bool queued ) + public Order IssueOrder(Actor self, IOrderTargeter order, Target target, bool queued) { - if( order.OrderID == "Deliver" ) + if (order.OrderID == "Deliver") return new Order(order.OrderID, self, queued) { TargetActor = target.Actor }; - if( order.OrderID == "Harvest" ) + if (order.OrderID == "Harvest") return new Order(order.OrderID, self, queued) { TargetLocation = target.CenterLocation.ToCPos() }; return null; @@ -145,23 +201,35 @@ namespace OpenRA.Mods.RA { if (order.OrderString == "Harvest") { + // NOTE: An explicit harvest order allows the harvester to decide which refinery to deliver to. + OwnerLinkedProc = null; + var mobile = self.Trait(); self.CancelActivity(); if (order.TargetLocation != CPos.Zero) { - self.QueueActivity(mobile.MoveTo(order.TargetLocation, 0)); - self.SetTargetLine(Target.FromOrder(order), Color.Red); + var loc = order.TargetLocation; + var territory = self.World.WorldActor.Trait(); + + // Find the nearest claimable cell to the order location (useful for group-select harvest): + loc = mobile.NearestCell(loc, p => mobile.CanEnterCell(p) && territory.ClaimResource(self, p), 1, 6); + + self.QueueActivity(mobile.MoveTo(loc, 0)); + self.SetTargetLine(Target.FromCell(loc), Color.Red); + + LastOrderLocation = loc; } self.QueueActivity(new FindResources()); } else if (order.OrderString == "Deliver") { + // NOTE: An explicit deliver order forces the harvester to always deliver to this refinery. var iao = order.TargetActor.TraitOrDefault(); if (iao == null || !iao.AllowDocking) return; - if (order.TargetActor != LinkedProc) - LinkedProc = order.TargetActor; + if (order.TargetActor != OwnerLinkedProc) + LinkedProc = OwnerLinkedProc = order.TargetActor; if (IsEmpty) return; @@ -179,6 +247,15 @@ namespace OpenRA.Mods.RA ChooseNewProc(self, proc); } + public void OnNotifyResourceClaimLost(Actor self, ResourceClaim claim, Actor claimer) + { + if (self == claimer) return; + + // Our claim on a resource was stolen, find more unclaimed resources: + self.CancelActivity(); + self.QueueActivity(new FindResources()); + } + PipType GetPipAt(int i) { var n = i * Info.Capacity / Info.PipCount; @@ -204,7 +281,7 @@ namespace OpenRA.Mods.RA public decimal GetSpeedModifier() { - return 1m - ( 1m - Info.FullyLoadedSpeed ) * contents.Values.Sum() / Info.Capacity; + return 1m - (1m - Info.FullyLoadedSpeed) * contents.Values.Sum() / Info.Capacity; } class HarvestOrderTargeter : IOrderTargeter @@ -223,11 +300,11 @@ namespace OpenRA.Mods.RA // Don't leak info about resources under the shroud if (!self.World.LocalShroud.IsExplored(location)) return false; - var res = self.World.WorldActor.Trait().GetResource( location ); + var res = self.World.WorldActor.Trait().GetResource(location); var info = self.Info.Traits.Get(); - if( res == null ) return false; - if( !info.Resources.Contains( res.info.Name ) ) return false; + if (res == null) return false; + if (!info.Resources.Contains(res.info.Name)) return false; cursor = "harvest"; IsQueued = forceQueued; diff --git a/OpenRA.Mods.RA/Move/Mobile.cs b/OpenRA.Mods.RA/Move/Mobile.cs index 4e8713174c..6344d604f1 100755 --- a/OpenRA.Mods.RA/Move/Mobile.cs +++ b/OpenRA.Mods.RA/Move/Mobile.cs @@ -200,13 +200,18 @@ namespace OpenRA.Mods.RA.Move } public CPos NearestMoveableCell(CPos target) + { + return NearestMoveableCell(target, 1, 10); + } + + public CPos NearestMoveableCell(CPos target, int minRange, int maxRange) { if (CanEnterCell(target)) return target; var searched = new List(); // Limit search to a radius of 10 tiles - for (int r = 1; r < 10; r++) + for (int r = minRange; r < maxRange; r++) foreach (var tile in self.World.FindTilesInCircle(target, r).Except(searched)) { if (CanEnterCell(tile)) @@ -219,6 +224,25 @@ namespace OpenRA.Mods.RA.Move return target; } + public CPos NearestCell(CPos target, Func check, int minRange, int maxRange) + { + if (check(target)) + return target; + + var searched = new List(); + for (int r = minRange; r < maxRange; r++) + foreach (var tile in self.World.FindTilesInCircle(target, r).Except(searched)) + { + if (check(tile)) + return tile; + + searched.Add(tile); + } + + // Couldn't find a cell + return target; + } + void PerformMoveInner(Actor self, CPos targetLocation, bool queued) { var currentLocation = NearestMoveableCell(targetLocation); diff --git a/OpenRA.Mods.RA/Move/Move.cs b/OpenRA.Mods.RA/Move/Move.cs index 1d95370079..7fbf9fef59 100755 --- a/OpenRA.Mods.RA/Move/Move.cs +++ b/OpenRA.Mods.RA/Move/Move.cs @@ -233,7 +233,7 @@ namespace OpenRA.Mods.RA.Move public override void Cancel( Actor self ) { - path = new List(); + path = new List(0); base.Cancel(self); } diff --git a/OpenRA.Mods.RA/Move/PathFinder.cs b/OpenRA.Mods.RA/Move/PathFinder.cs index b1d0033985..7322c08094 100755 --- a/OpenRA.Mods.RA/Move/PathFinder.cs +++ b/OpenRA.Mods.RA/Move/PathFinder.cs @@ -19,13 +19,13 @@ namespace OpenRA.Mods.RA.Move { public class PathFinderInfo : ITraitInfo { - public object Create( ActorInitializer init ) { return new PathFinder( init.world ); } + public object Create(ActorInitializer init) { return new PathFinder(init.world); } } public class PathFinder { readonly World world; - public PathFinder( World world ) { this.world = world; } + public PathFinder(World world) { this.world = world; } class CachedPath { @@ -47,7 +47,8 @@ namespace OpenRA.Mods.RA.Move if (cached != null) { Log.Write("debug", "Actor {0} asked for a path from {1} tick(s) ago", self.ActorID, world.FrameNumber - cached.tick); - cached.tick = world.FrameNumber; + if (world.FrameNumber - cached.tick > MaxPathAge) + CachedPaths.Remove(cached); return new List(cached.result); } @@ -68,11 +69,11 @@ namespace OpenRA.Mods.RA.Move public List FindUnitPathToRange(CPos src, CPos target, int range, Actor self) { - using( new PerfSample( "Pathfinder" ) ) + using (new PerfSample("Pathfinder")) { var mi = self.Info.Traits.Get(); var tilesInRange = world.FindTilesInCircle(target, range) - .Where( t => mi.CanEnterCell(self.World, self.Owner, t, null, true)); + .Where(t => mi.CanEnterCell(self.World, self.Owner, t, null, true)); var path = FindBidiPath( PathSearch.FromPoints(world, mi, self.Owner, tilesInRange, src, true), @@ -105,10 +106,10 @@ namespace OpenRA.Mods.RA.Move var ret = new List(); CPos pathNode = destination; - while( cellInfo[ pathNode.X, pathNode.Y ].Path != pathNode ) + while (cellInfo[pathNode.X, pathNode.Y].Path != pathNode) { - ret.Add( pathNode ); - pathNode = cellInfo[ pathNode.X, pathNode.Y ].Path; + ret.Add(pathNode); + pathNode = cellInfo[pathNode.X, pathNode.Y].Path; } ret.Add(pathNode); @@ -153,8 +154,8 @@ namespace OpenRA.Mods.RA.Move var q = p; while (ca[q.X, q.Y].Path != q) { - ret.Add( q ); - q = ca[ q.X, q.Y ].Path; + ret.Add(q); + q = ca[q.X, q.Y].Path; } ret.Add(q); @@ -167,22 +168,22 @@ namespace OpenRA.Mods.RA.Move ret.Add(q); } - CheckSanePath( ret ); + CheckSanePath(ret); return ret; } - [Conditional( "SANITY_CHECKS" )] + [Conditional("SANITY_CHECKS")] static void CheckSanePath(List path) { - if( path.Count == 0 ) + if (path.Count == 0) return; - var prev = path[ 0 ]; - for( int i = 0 ; i < path.Count ; i++ ) + var prev = path[0]; + for (int i = 0; i < path.Count; i++) { - var d = path[ i ] - prev; - if( Math.Abs( d.X ) > 1 || Math.Abs( d.Y ) > 1 ) - throw new InvalidOperationException( "(PathFinder) path sanity check failed" ); - prev = path[ i ]; + var d = path[i] - prev; + if (Math.Abs(d.X) > 1 || Math.Abs(d.Y) > 1) + throw new InvalidOperationException("(PathFinder) path sanity check failed"); + prev = path[i]; } } diff --git a/OpenRA.Mods.RA/Move/PathSearch.cs b/OpenRA.Mods.RA/Move/PathSearch.cs index 04e0348e9d..89d0ebc941 100755 --- a/OpenRA.Mods.RA/Move/PathSearch.cs +++ b/OpenRA.Mods.RA/Move/PathSearch.cs @@ -18,9 +18,10 @@ namespace OpenRA.Mods.RA.Move public class PathSearch : IDisposable { World world; - public CellInfo[ , ] cellInfo; + public CellInfo[,] cellInfo; public PriorityQueue queue; public Func heuristic; + Func customCost; Func customBlock; public bool checkForBlocked; public Actor ignoreBuilding; @@ -35,6 +36,7 @@ namespace OpenRA.Mods.RA.Move cellInfo = InitCellInfo(); this.mobileInfo = mobileInfo; this.owner = owner; + customCost = null; queue = new PriorityQueue(); } @@ -62,6 +64,12 @@ namespace OpenRA.Mods.RA.Move return this; } + public PathSearch WithCustomCost(Func w) + { + customCost = w; + return this; + } + public PathSearch WithoutLaneBias() { LaneBias = 0; @@ -70,7 +78,7 @@ namespace OpenRA.Mods.RA.Move public PathSearch FromPoint(CPos from) { - AddInitialCell( from ); + AddInitialCell(from); return this; } @@ -92,12 +100,19 @@ namespace OpenRA.Mods.RA.Move if (thisCost == int.MaxValue) return p.Location; + if (customCost != null) + { + int c = customCost(p.Location); + if (c == int.MaxValue) + return p.Location; + } + foreach( CVec d in directions ) { CPos newHere = p.Location + d; if (!world.Map.IsInMap(newHere.X, newHere.Y)) continue; - if( cellInfo[ newHere.X, newHere.Y ].Seen ) + if (cellInfo[newHere.X, newHere.Y].Seen) continue; var costHere = mobileInfo.MovementCostForCell(world, newHere); @@ -111,32 +126,37 @@ namespace OpenRA.Mods.RA.Move if (customBlock != null && customBlock(newHere)) continue; - var est = heuristic( newHere ); - if( est == int.MaxValue ) + var est = heuristic(newHere); + if (est == int.MaxValue) continue; int cellCost = costHere; - if( d.X * d.Y != 0 ) cellCost = ( cellCost * 34 ) / 24; + if (d.X * d.Y != 0) cellCost = (cellCost * 34) / 24; + + if (customCost != null) + cellCost += customCost(newHere); // directional bonuses for smoother flow! - var ux = (newHere.X + (inReverse ? 1 : 0) & 1); - var uy = (newHere.Y + (inReverse ? 1 : 0) & 1); + if (LaneBias != 0) + { + var ux = (newHere.X + (inReverse ? 1 : 0) & 1); + var uy = (newHere.Y + (inReverse ? 1 : 0) & 1); - if (ux == 0 && d.Y < 0) cellCost += LaneBias; - else if (ux == 1 && d.Y > 0) cellCost += LaneBias; - if (uy == 0 && d.X < 0) cellCost += LaneBias; - else if (uy == 1 && d.X > 0) cellCost += LaneBias; + if (ux == 0 && d.Y < 0) cellCost += LaneBias; + else if (ux == 1 && d.Y > 0) cellCost += LaneBias; + if (uy == 0 && d.X < 0) cellCost += LaneBias; + else if (uy == 1 && d.X > 0) cellCost += LaneBias; + } - int newCost = cellInfo[ p.Location.X, p.Location.Y ].MinCost + cellCost; + int newCost = cellInfo[p.Location.X, p.Location.Y].MinCost + cellCost; - if( newCost >= cellInfo[ newHere.X, newHere.Y ].MinCost ) + if (newCost >= cellInfo[newHere.X, newHere.Y].MinCost) continue; - cellInfo[ newHere.X, newHere.Y ].Path = p.Location; - cellInfo[ newHere.X, newHere.Y ].MinCost = newCost; - - queue.Add( new PathDistance( newCost + est, newHere ) ); + cellInfo[newHere.X, newHere.Y].Path = p.Location; + cellInfo[newHere.X, newHere.Y].MinCost = newCost; + queue.Add(new PathDistance(newCost + est, newHere)); } return p.Location; } @@ -158,24 +178,28 @@ namespace OpenRA.Mods.RA.Move if (!world.Map.IsInMap(location.X, location.Y)) return; - cellInfo[ location.X, location.Y ] = new CellInfo( 0, location, false ); - queue.Add( new PathDistance( heuristic( location ), location ) ); + cellInfo[location.X, location.Y] = new CellInfo(0, location, false); + queue.Add(new PathDistance(heuristic(location), location)); } - public static PathSearch Search( World world, MobileInfo mi, Player owner, bool checkForBlocked ) + public static PathSearch Search(World world, MobileInfo mi, Player owner, bool checkForBlocked) { - var search = new PathSearch(world, mi, owner) { - checkForBlocked = checkForBlocked }; + var search = new PathSearch(world, mi, owner) + { + checkForBlocked = checkForBlocked + }; return search; } public static PathSearch FromPoint(World world, MobileInfo mi, Player owner, CPos from, CPos target, bool checkForBlocked) { - var search = new PathSearch(world, mi, owner) { - heuristic = DefaultEstimator( target ), - checkForBlocked = checkForBlocked }; + var search = new PathSearch(world, mi, owner) + { + heuristic = DefaultEstimator(target), + checkForBlocked = checkForBlocked + }; - search.AddInitialCell( from ); + search.AddInitialCell(from); return search; } @@ -187,8 +211,8 @@ namespace OpenRA.Mods.RA.Move checkForBlocked = checkForBlocked }; - foreach( var sl in froms ) - search.AddInitialCell( sl ); + foreach (var sl in froms) + search.AddInitialCell(sl); return search; } @@ -207,7 +231,7 @@ namespace OpenRA.Mods.RA.Move cellInfoPool.Enqueue(ci); } - CellInfo[ , ] InitCellInfo() + CellInfo[,] InitCellInfo() { CellInfo[,] result = null; while (cellInfoPool.Count > 0) @@ -225,10 +249,10 @@ namespace OpenRA.Mods.RA.Move } if (result == null) - result = new CellInfo[ world.Map.MapSize.X, world.Map.MapSize.Y ]; + result = new CellInfo[world.Map.MapSize.X, world.Map.MapSize.Y]; - for( int x = 0 ; x < world.Map.MapSize.X ; x++ ) - for( int y = 0 ; y < world.Map.MapSize.Y ; y++ ) + for (int x = 0; x < world.Map.MapSize.X; x++) + for (int y = 0; y < world.Map.MapSize.Y; y++) result[ x, y ] = new CellInfo( int.MaxValue, new CPos( x, y ), false ); return result; @@ -239,8 +263,8 @@ namespace OpenRA.Mods.RA.Move return here => { CVec d = (here - destination).Abs(); - int diag = Math.Min( d.X, d.Y ); - int straight = Math.Abs( d.X - d.Y ); + int diag = Math.Min(d.X, d.Y); + int straight = Math.Abs(d.X - d.Y); return (3400 * diag / 24) + (100 * straight); }; } diff --git a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj index d78e59a294..f1b484702f 100644 --- a/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj +++ b/OpenRA.Mods.RA/OpenRA.Mods.RA.csproj @@ -356,6 +356,8 @@ + + diff --git a/OpenRA.Mods.RA/OreRefinery.cs b/OpenRA.Mods.RA/OreRefinery.cs index 19e94a50dc..5b5d41db7e 100644 --- a/OpenRA.Mods.RA/OreRefinery.cs +++ b/OpenRA.Mods.RA/OreRefinery.cs @@ -116,7 +116,6 @@ namespace OpenRA.Mods.RA harv.QueueActivity( new CallFunc( () => harv.Trait().ContinueHarvesting(harv) ) ); } - public void OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner) { // Steal any docked harv too diff --git a/OpenRA.Mods.RA/TraitsInterfaces.cs b/OpenRA.Mods.RA/TraitsInterfaces.cs index d02d252259..e4f4f2d32d 100755 --- a/OpenRA.Mods.RA/TraitsInterfaces.cs +++ b/OpenRA.Mods.RA/TraitsInterfaces.cs @@ -37,4 +37,9 @@ namespace OpenRA.Mods.RA { IEnumerable ProvidesPrerequisites {get;} } + + public interface INotifyResourceClaimLost + { + void OnNotifyResourceClaimLost(Actor self, ResourceClaim claim, Actor claimer); + } } diff --git a/OpenRA.Mods.RA/World/ResourceClaim.cs b/OpenRA.Mods.RA/World/ResourceClaim.cs new file mode 100644 index 0000000000..42a8cccf81 --- /dev/null +++ b/OpenRA.Mods.RA/World/ResourceClaim.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace OpenRA.Mods.RA +{ + public sealed class ResourceClaim + { + public readonly Actor Claimer; + public CPos Cell; + + public ResourceClaim(Actor claimer, CPos cell) { Claimer = claimer; Cell = cell; } + } +} diff --git a/OpenRA.Mods.RA/World/ResourceClaimLayer.cs b/OpenRA.Mods.RA/World/ResourceClaimLayer.cs new file mode 100644 index 0000000000..1228f7b67d --- /dev/null +++ b/OpenRA.Mods.RA/World/ResourceClaimLayer.cs @@ -0,0 +1,121 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using OpenRA.Traits; + +namespace OpenRA.Mods.RA +{ + public sealed class ResourceClaimLayerInfo : TraitInfo + { + } + + public sealed class ResourceClaimLayer : IWorldLoaded + { + Dictionary claimByCell; + Dictionary claimByActor; + + private void MakeClaim(Actor claimer, CPos cell) + { + UnclaimByActor(claimer); + UnclaimByCell(cell, claimer); + claimByActor[claimer] = claimByCell[cell] = new ResourceClaim(claimer, cell); + } + + private void Unclaim(ResourceClaim claim, Actor claimer) + { + if (claimByActor.Remove(claim.Claimer) & claimByCell.Remove(claim.Cell)) + { + if (claim.Claimer.Destroyed) return; + if (!claim.Claimer.IsInWorld) return; + if (claim.Claimer.IsDead()) return; + + claim.Claimer.Trait().OnNotifyResourceClaimLost(claim.Claimer, claim, claimer); + } + } + + public void WorldLoaded(OpenRA.World w) + { + // NOTE(jsd): 32 seems a sane default initial capacity for the total # of harvesters in a game. Purely a guesstimate. + claimByCell = new Dictionary(32); + claimByActor = new Dictionary(32); + } + + /// + /// Attempt to claim the resource at the cell for the given actor. + /// + /// + /// + /// + public bool ClaimResource(Actor claimer, CPos cell) + { + // Has anyone else claimed this point? + ResourceClaim claim; + if (claimByCell.TryGetValue(cell, out claim)) + { + // Same claimer: + if (claim.Claimer == claimer) return true; + + // This is to prevent in-fighting amongst friendly harvesters: + if (claimer.Owner == claim.Claimer.Owner) return false; + if (claimer.Owner.Stances[claim.Claimer.Owner] == Stance.Ally) return false; + + // If an enemy/neutral claimed this, don't respect that claim: + } + + // Either nobody else claims this point or an enemy/neutral claims it: + MakeClaim(claimer, cell); + return true; + } + + /// + /// Release the last resource claim made on this cell. + /// + /// + public void UnclaimByCell(CPos cell, Actor claimer) + { + ResourceClaim claim; + if (claimByCell.TryGetValue(cell, out claim)) + Unclaim(claim, claimer); + } + + /// + /// Release the last resource claim made by this actor. + /// + /// + public void UnclaimByActor(Actor claimer) + { + ResourceClaim claim; + if (claimByActor.TryGetValue(claimer, out claim)) + Unclaim(claim, claimer); + } + + /// + /// Is the cell location claimed for harvesting by any other actor? + /// + /// + /// + /// true if already claimed by an ally that isn't ; false otherwise. + public bool IsClaimedByAnyoneElse(Actor self, CPos cell, out ResourceClaim claim) + { + if (claimByCell.TryGetValue(cell, out claim)) + { + // Same claimer: + if (claim.Claimer == self) return false; + + // This is to prevent in-fighting amongst friendly harvesters: + if (self.Owner == claim.Claimer.Owner) return true; + if (self.Owner.Stances[claim.Claimer.Owner] == Stance.Ally) return true; + + // If an enemy/neutral claimed this, don't respect that claim and fall through: + } + else + { + // No claim. + claim = null; + } + + return false; + } + } +} diff --git a/mods/ra/rules/system.yaml b/mods/ra/rules/system.yaml index 42c77b2350..54ea9beac1 100644 --- a/mods/ra/rules/system.yaml +++ b/mods/ra/rules/system.yaml @@ -261,6 +261,7 @@ World: Race: soviet BibLayer: ResourceLayer: + ResourceClaimLayer: ResourceType@ore: ResourceType: 1 Palette: terrain