Many harvester behavior improvements; summary below.
Implemented Harvester territory marking with a simple resource claim system in ResourceClaimLayer trait added to World. Added customCost for PathSearch to support new Harvester search preferences. Explicit delivery order forces harvester to always deliver to that refinery. Explicit harvest order frees harvester from forced delivery refinery and allows for auto-balancing. Harvesters auto-balance refinery choice such that no more than 3 harvesters are linked to any one refinery at a time. Harvesters try very hard to not block the refinery dock location. Harvesters try to avoid enemy territory when searching for resources. Group-select harvest order intelligently disperses harvesters around the order location. Fixed PathFinder caching to not be a sliding window. This is a correctness issue. Sliding window causes no-route paths to be cached permanently in tight move loops and doesn't allow eventual progress to be made. This may have negative performance implications.
This commit is contained in:
@@ -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<ResourceLayer> { }
|
||||
|
||||
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<ResourceType>() )
|
||||
foreach (var rt in world.WorldActor.TraitsImplementing<ResourceType>())
|
||||
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
|
||||
{
|
||||
|
||||
@@ -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<Mobile>();
|
||||
var harv = self.Trait<Harvester>();
|
||||
|
||||
// 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<IAcceptOre>();
|
||||
|
||||
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
|
||||
|
||||
@@ -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<Harvester>();
|
||||
|
||||
if (harv.IsFull)
|
||||
return Util.SequenceActivities(new DeliverResources(), NextActivity);
|
||||
|
||||
var harvInfo = self.Info.Traits.Get<HarvesterInfo>();
|
||||
var mobile = self.Trait<Mobile>();
|
||||
var mobileInfo = self.Info.Traits.Get<MobileInfo>();
|
||||
var res = self.World.WorldActor.Trait<ResourceLayer>();
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().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<ResourceLayer>();
|
||||
var territory = self.World.WorldActor.Trait<ResourceClaimLayer>();
|
||||
|
||||
// Find harvestable resources nearby:
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().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<ResourceClaimLayer>();
|
||||
if (IsCanceled)
|
||||
{
|
||||
territory.UnclaimByActor(self);
|
||||
return NextActivity;
|
||||
}
|
||||
|
||||
var harv = self.Trait<Harvester>();
|
||||
harv.LastHarvestedCell = self.Location;
|
||||
|
||||
if (harv.IsFull)
|
||||
{
|
||||
territory.UnclaimByActor(self);
|
||||
return NextActivity;
|
||||
}
|
||||
|
||||
var resLayer = self.World.WorldActor.Trait<ResourceLayer>();
|
||||
var resource = resLayer.Harvest(self.Location);
|
||||
if (resource == null)
|
||||
{
|
||||
territory.UnclaimByActor(self);
|
||||
return NextActivity;
|
||||
}
|
||||
|
||||
var renderUnit = self.Trait<RenderUnit>(); /* better have one of these! */
|
||||
var resource = self.World.WorldActor.Trait<ResourceLayer>().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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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<ResourceTypeInfo, int> contents = new Dictionary<ResourceTypeInfo, int>();
|
||||
|
||||
[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<Mobile>();
|
||||
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<IAcceptOre>()
|
||||
.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<IAcceptOre>()
|
||||
where r.Actor != ignore && r.Actor.Owner == self.Owner
|
||||
let linkedHarvs = self.World.ActorsWithTrait<Harvester>().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<MobileInfo>();
|
||||
var path = self.World.WorldActor.Trait<PathFinder>().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<IAcceptOre>().DeliverOffset;
|
||||
if (self.Location == deliveryLoc)
|
||||
{
|
||||
// Get out of the way:
|
||||
var mobile = self.Trait<Mobile>();
|
||||
var harv = self.Trait<Harvester>();
|
||||
// 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<ResourceClaimLayer>().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<Mobile>();
|
||||
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<ResourceClaimLayer>();
|
||||
|
||||
// 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<IAcceptOre>();
|
||||
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<ResourceLayer>().GetResource( location );
|
||||
var res = self.World.WorldActor.Trait<ResourceLayer>().GetResource(location);
|
||||
var info = self.Info.Traits.Get<HarvesterInfo>();
|
||||
|
||||
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;
|
||||
|
||||
|
||||
@@ -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<CPos>();
|
||||
// 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<CPos, bool> check, int minRange, int maxRange)
|
||||
{
|
||||
if (check(target))
|
||||
return target;
|
||||
|
||||
var searched = new List<CPos>();
|
||||
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);
|
||||
|
||||
@@ -233,7 +233,7 @@ namespace OpenRA.Mods.RA.Move
|
||||
|
||||
public override void Cancel( Actor self )
|
||||
{
|
||||
path = new List<CPos>();
|
||||
path = new List<CPos>(0);
|
||||
base.Cancel(self);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CPos>(cached.result);
|
||||
}
|
||||
|
||||
@@ -68,11 +69,11 @@ namespace OpenRA.Mods.RA.Move
|
||||
|
||||
public List<CPos> FindUnitPathToRange(CPos src, CPos target, int range, Actor self)
|
||||
{
|
||||
using( new PerfSample( "Pathfinder" ) )
|
||||
using (new PerfSample("Pathfinder"))
|
||||
{
|
||||
var mi = self.Info.Traits.Get<MobileInfo>();
|
||||
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>();
|
||||
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<CPos> 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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,9 +18,10 @@ namespace OpenRA.Mods.RA.Move
|
||||
public class PathSearch : IDisposable
|
||||
{
|
||||
World world;
|
||||
public CellInfo[ , ] cellInfo;
|
||||
public CellInfo[,] cellInfo;
|
||||
public PriorityQueue<PathDistance> queue;
|
||||
public Func<CPos, int> heuristic;
|
||||
Func<CPos, int> customCost;
|
||||
Func<CPos, bool> 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<PathDistance>();
|
||||
}
|
||||
|
||||
@@ -62,6 +64,12 @@ namespace OpenRA.Mods.RA.Move
|
||||
return this;
|
||||
}
|
||||
|
||||
public PathSearch WithCustomCost(Func<CPos, int> 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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -356,6 +356,8 @@
|
||||
<Compile Include="Widgets\WorldCommandWidget.cs" />
|
||||
<Compile Include="Widgets\WorldTooltipWidget.cs" />
|
||||
<Compile Include="World\ChooseBuildTabOnSelect.cs" />
|
||||
<Compile Include="World\ResourceClaim.cs" />
|
||||
<Compile Include="World\ResourceClaimLayer.cs" />
|
||||
<Compile Include="World\PlayMusicOnMapLoad.cs" />
|
||||
<Compile Include="World\SmudgeLayer.cs" />
|
||||
<Compile Include="Player\BaseAttackNotifier.cs" />
|
||||
|
||||
@@ -116,7 +116,6 @@ namespace OpenRA.Mods.RA
|
||||
harv.QueueActivity( new CallFunc( () => harv.Trait<Harvester>().ContinueHarvesting(harv) ) );
|
||||
}
|
||||
|
||||
|
||||
public void OnCapture(Actor self, Actor captor, Player oldOwner, Player newOwner)
|
||||
{
|
||||
// Steal any docked harv too
|
||||
|
||||
@@ -37,4 +37,9 @@ namespace OpenRA.Mods.RA
|
||||
{
|
||||
IEnumerable<string> ProvidesPrerequisites {get;}
|
||||
}
|
||||
|
||||
public interface INotifyResourceClaimLost
|
||||
{
|
||||
void OnNotifyResourceClaimLost(Actor self, ResourceClaim claim, Actor claimer);
|
||||
}
|
||||
}
|
||||
|
||||
15
OpenRA.Mods.RA/World/ResourceClaim.cs
Normal file
15
OpenRA.Mods.RA/World/ResourceClaim.cs
Normal file
@@ -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; }
|
||||
}
|
||||
}
|
||||
121
OpenRA.Mods.RA/World/ResourceClaimLayer.cs
Normal file
121
OpenRA.Mods.RA/World/ResourceClaimLayer.cs
Normal file
@@ -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<ResourceClaimLayer>
|
||||
{
|
||||
}
|
||||
|
||||
public sealed class ResourceClaimLayer : IWorldLoaded
|
||||
{
|
||||
Dictionary<CPos, ResourceClaim> claimByCell;
|
||||
Dictionary<Actor, ResourceClaim> 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<INotifyResourceClaimLost>().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<CPos, ResourceClaim>(32);
|
||||
claimByActor = new Dictionary<Actor, ResourceClaim>(32);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to claim the resource at the cell for the given actor.
|
||||
/// </summary>
|
||||
/// <param name="claimer"></param>
|
||||
/// <param name="cell"></param>
|
||||
/// <returns></returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release the last resource claim made on this cell.
|
||||
/// </summary>
|
||||
/// <param name="cell"></param>
|
||||
public void UnclaimByCell(CPos cell, Actor claimer)
|
||||
{
|
||||
ResourceClaim claim;
|
||||
if (claimByCell.TryGetValue(cell, out claim))
|
||||
Unclaim(claim, claimer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Release the last resource claim made by this actor.
|
||||
/// </summary>
|
||||
/// <param name="claimer"></param>
|
||||
public void UnclaimByActor(Actor claimer)
|
||||
{
|
||||
ResourceClaim claim;
|
||||
if (claimByActor.TryGetValue(claimer, out claim))
|
||||
Unclaim(claim, claimer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is the cell location <paramref name="cell"/> claimed for harvesting by any other actor?
|
||||
/// </summary>
|
||||
/// <param name="self"></param>
|
||||
/// <param name="cell"></param>
|
||||
/// <returns>true if already claimed by an ally that isn't <paramref name="self"/>; false otherwise.</returns>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -261,6 +261,7 @@ World:
|
||||
Race: soviet
|
||||
BibLayer:
|
||||
ResourceLayer:
|
||||
ResourceClaimLayer:
|
||||
ResourceType@ore:
|
||||
ResourceType: 1
|
||||
Palette: terrain
|
||||
|
||||
Reference in New Issue
Block a user