#region Copyright & License Information /* * Copyright 2007-2015 The OpenRA Developers (see AUTHORS) * 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. For more information, * see COPYING. */ #endregion using System; using System.Collections.Generic; using System.Drawing; using System.Linq; namespace OpenRA.Traits { public enum SubCell { Invalid = int.MinValue, Any = int.MinValue / 2, FullCell = 0, First = 1 } public class ActorMapInfo : ITraitInfo { [Desc("Size of partition bins (cells)")] public readonly int BinSize = 10; public object Create(ActorInitializer init) { return new ActorMap(init.World, this); } } public class ActorMap : ITick { class InfluenceNode { public InfluenceNode Next; public SubCell SubCell; public Actor Actor; } class Bin { public readonly List Actors = new List(); public readonly List ProximityTriggers = new List(); } class CellTrigger { public readonly int Id; public readonly CPos[] Footprint; public bool Dirty; Action onActorEntered; Action onActorExited; IEnumerable currentActors = Enumerable.Empty(); public CellTrigger(int id, CPos[] footprint, Action onActorEntered, Action onActorExited) { Id = id; Footprint = footprint; this.onActorEntered = onActorEntered; this.onActorExited = onActorExited; // Notify any actors that are initially inside the trigger zone Dirty = true; } public void Tick(ActorMap am) { if (!Dirty) return; var oldActors = currentActors; currentActors = Footprint.SelectMany(c => am.GetUnitsAt(c)).ToList(); var entered = currentActors.Except(oldActors); var exited = oldActors.Except(currentActors); if (onActorEntered != null) foreach (var a in entered) onActorEntered(a); if (onActorExited != null) foreach (var a in exited) onActorExited(a); Dirty = false; } } class ProximityTrigger : IDisposable { public readonly int Id; public WPos Position { get; private set; } public WDist Range { get; private set; } public WPos TopLeft { get; private set; } public WPos BottomRight { get; private set; } public bool Dirty; Action onActorEntered; Action onActorExited; IEnumerable currentActors = Enumerable.Empty(); public ProximityTrigger(int id, WPos pos, WDist range, Action onActorEntered, Action onActorExited) { Id = id; this.onActorEntered = onActorEntered; this.onActorExited = onActorExited; Update(pos, range); } public void Update(WPos newPos, WDist newRange) { Position = newPos; Range = newRange; var offset = new WVec(newRange, newRange, WDist.Zero); TopLeft = newPos - offset; BottomRight = newPos + offset; Dirty = true; } public void Tick(ActorMap am) { if (!Dirty) return; var oldActors = currentActors; var delta = new WVec(Range, Range, WDist.Zero); currentActors = am.ActorsInBox(Position - delta, Position + delta) .Where(a => (a.CenterPosition - Position).HorizontalLengthSquared < Range.LengthSquared) .ToList(); var entered = currentActors.Except(oldActors); var exited = oldActors.Except(currentActors); if (onActorEntered != null) foreach (var a in entered) onActorEntered(a); if (onActorExited != null) foreach (var a in exited) onActorExited(a); Dirty = false; } public void Dispose() { if (onActorExited != null) foreach (var a in currentActors) onActorExited(a); } } readonly ActorMapInfo info; readonly Map map; readonly Dictionary cellTriggers = new Dictionary(); readonly Dictionary> cellTriggerInfluence = new Dictionary>(); readonly Dictionary proximityTriggers = new Dictionary(); int nextTriggerId; readonly CellLayer influence; readonly Bin[] bins; readonly int rows, cols; // Position updates are done in one pass // to ensure consistency during a tick readonly HashSet addActorPosition = new HashSet(); readonly HashSet removeActorPosition = new HashSet(); readonly Predicate actorShouldBeRemoved; public ActorMap(World world, ActorMapInfo info) { this.info = info; map = world.Map; influence = new CellLayer(world.Map); cols = CellCoordToBinIndex(world.Map.MapSize.X) + 1; rows = CellCoordToBinIndex(world.Map.MapSize.Y) + 1; bins = new Bin[rows * cols]; for (var row = 0; row < rows; row++) for (var col = 0; col < cols; col++) bins[row * cols + col] = new Bin(); // Cache this delegate so it does not have to be allocated repeatedly. actorShouldBeRemoved = removeActorPosition.Contains; } public IEnumerable GetUnitsAt(CPos a) { if (!map.Contains(a)) yield break; for (var i = influence[a]; i != null; i = i.Next) if (!i.Actor.Disposed) yield return i.Actor; } public IEnumerable GetUnitsAt(CPos a, SubCell sub) { if (!map.Contains(a)) yield break; for (var i = influence[a]; i != null; i = i.Next) if (!i.Actor.Disposed && (i.SubCell == sub || i.SubCell == SubCell.FullCell)) yield return i.Actor; } public bool HasFreeSubCell(CPos a, bool checkTransient = true) { return FreeSubCell(a, SubCell.Any, checkTransient) != SubCell.Invalid; } public SubCell FreeSubCell(CPos a, SubCell preferredSubCell = SubCell.Any, bool checkTransient = true) { if (preferredSubCell > SubCell.Any && !AnyUnitsAt(a, preferredSubCell, checkTransient)) return preferredSubCell; if (!AnyUnitsAt(a)) return map.DefaultSubCell; for (var i = (int)SubCell.First; i < map.SubCellOffsets.Length; i++) if (i != (int)preferredSubCell && !AnyUnitsAt(a, (SubCell)i, checkTransient)) return (SubCell)i; return SubCell.Invalid; } public SubCell FreeSubCell(CPos a, SubCell preferredSubCell, Func checkIfBlocker) { if (preferredSubCell > SubCell.Any && !AnyUnitsAt(a, preferredSubCell, checkIfBlocker)) return preferredSubCell; if (!AnyUnitsAt(a)) return map.DefaultSubCell; for (var i = (int)SubCell.First; i < map.SubCellOffsets.Length; i++) if (i != (int)preferredSubCell && !AnyUnitsAt(a, (SubCell)i, checkIfBlocker)) return (SubCell)i; return SubCell.Invalid; } // NOTE: always includes transients with influence public bool AnyUnitsAt(CPos a) { if (!map.Contains(a)) return false; return influence[a] != null; } // NOTE: can not check aircraft public bool AnyUnitsAt(CPos a, SubCell sub, bool checkTransient = true) { if (!map.Contains(a)) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; for (var i = influence[a]; i != null; i = i.Next) { if (always || i.SubCell == sub || i.SubCell == SubCell.FullCell) { if (checkTransient) return true; var pos = i.Actor.TraitOrDefault(); if (pos == null || !pos.IsLeavingCell(a, i.SubCell)) return true; } } return false; } // NOTE: can not check aircraft public bool AnyUnitsAt(CPos a, SubCell sub, Func withCondition) { if (!map.Contains(a)) return false; var always = sub == SubCell.FullCell || sub == SubCell.Any; for (var i = influence[a]; i != null; i = i.Next) if ((always || i.SubCell == sub || i.SubCell == SubCell.FullCell) && !i.Actor.Disposed && withCondition(i.Actor)) return true; return false; } public void AddInfluence(Actor self, IOccupySpace ios) { foreach (var c in ios.OccupiedCells()) { if (!map.Contains(c.First)) continue; influence[c.First] = new InfluenceNode { Next = influence[c.First], SubCell = c.Second, Actor = self }; List triggers; if (cellTriggerInfluence.TryGetValue(c.First, out triggers)) foreach (var t in triggers) t.Dirty = true; } } public void RemoveInfluence(Actor self, IOccupySpace ios) { foreach (var c in ios.OccupiedCells()) { if (!map.Contains(c.First)) continue; var temp = influence[c.First]; RemoveInfluenceInner(ref temp, self); influence[c.First] = temp; List triggers; if (cellTriggerInfluence.TryGetValue(c.First, out triggers)) foreach (var t in triggers) t.Dirty = true; } } void RemoveInfluenceInner(ref InfluenceNode influenceNode, Actor toRemove) { if (influenceNode == null) return; if (influenceNode.Actor == toRemove) influenceNode = influenceNode.Next; if (influenceNode != null) RemoveInfluenceInner(ref influenceNode.Next, toRemove); } public void Tick(Actor self) { // Position updates are done in one pass // to ensure consistency during a tick foreach (var bin in bins) { var removed = bin.Actors.RemoveAll(actorShouldBeRemoved); if (removed > 0) foreach (var t in bin.ProximityTriggers) t.Dirty = true; } removeActorPosition.Clear(); foreach (var a in addActorPosition) { var pos = a.CenterPosition; var col = WorldCoordToBinIndex(pos.X).Clamp(0, cols - 1); var row = WorldCoordToBinIndex(pos.Y).Clamp(0, rows - 1); var bin = BinAt(row, col); bin.Actors.Add(a); foreach (var t in bin.ProximityTriggers) t.Dirty = true; } addActorPosition.Clear(); foreach (var t in cellTriggers) t.Value.Tick(this); foreach (var t in proximityTriggers) t.Value.Tick(this); } public int AddCellTrigger(CPos[] cells, Action onEntry, Action onExit) { var id = nextTriggerId++; var t = new CellTrigger(id, cells, onEntry, onExit); cellTriggers.Add(id, t); foreach (var c in cells) { if (!map.Contains(c)) continue; if (!cellTriggerInfluence.ContainsKey(c)) cellTriggerInfluence.Add(c, new List()); cellTriggerInfluence[c].Add(t); } return id; } public void RemoveCellTrigger(int id) { CellTrigger trigger; if (!cellTriggers.TryGetValue(id, out trigger)) return; foreach (var c in trigger.Footprint) { if (!cellTriggerInfluence.ContainsKey(c)) continue; cellTriggerInfluence[c].RemoveAll(t => t == trigger); } } public int AddProximityTrigger(WPos pos, WDist range, Action onEntry, Action onExit) { var id = nextTriggerId++; var t = new ProximityTrigger(id, pos, range, onEntry, onExit); proximityTriggers.Add(id, t); foreach (var bin in BinsInBox(t.TopLeft, t.BottomRight)) bin.ProximityTriggers.Add(t); return id; } public void RemoveProximityTrigger(int id) { ProximityTrigger t; if (!proximityTriggers.TryGetValue(id, out t)) return; foreach (var bin in BinsInBox(t.TopLeft, t.BottomRight)) bin.ProximityTriggers.Remove(t); t.Dispose(); } public void UpdateProximityTrigger(int id, WPos newPos, WDist newRange) { ProximityTrigger t; if (!proximityTriggers.TryGetValue(id, out t)) return; foreach (var bin in BinsInBox(t.TopLeft, t.BottomRight)) bin.ProximityTriggers.Remove(t); t.Update(newPos, newRange); foreach (var bin in BinsInBox(t.TopLeft, t.BottomRight)) bin.ProximityTriggers.Add(t); } public void AddPosition(Actor a, IOccupySpace ios) { UpdatePosition(a, ios); } public void RemovePosition(Actor a, IOccupySpace ios) { removeActorPosition.Add(a); } public void UpdatePosition(Actor a, IOccupySpace ios) { RemovePosition(a, ios); addActorPosition.Add(a); } int CellCoordToBinIndex(int cell) { return cell / info.BinSize; } int WorldCoordToBinIndex(int world) { return CellCoordToBinIndex(world / 1024); } Rectangle BinRectangleCoveringWorldArea(int worldLeft, int worldTop, int worldRight, int worldBottom) { var minCol = WorldCoordToBinIndex(worldLeft).Clamp(0, cols - 1); var minRow = WorldCoordToBinIndex(worldTop).Clamp(0, rows - 1); var maxCol = WorldCoordToBinIndex(worldRight).Clamp(0, cols - 1); var maxRow = WorldCoordToBinIndex(worldBottom).Clamp(0, rows - 1); return Rectangle.FromLTRB(minCol, minRow, maxCol, maxRow); } Bin BinAt(int binRow, int binCol) { return bins[binRow * cols + binCol]; } IEnumerable BinsInBox(WPos a, WPos b) { var left = Math.Min(a.X, b.X); var top = Math.Min(a.Y, b.Y); var right = Math.Max(a.X, b.X); var bottom = Math.Max(a.Y, b.Y); var region = BinRectangleCoveringWorldArea(left, top, right, bottom); var minCol = region.Left; var minRow = region.Top; var maxCol = region.Right; var maxRow = region.Bottom; for (var row = minRow; row <= maxRow; row++) for (var col = minCol; col <= maxCol; col++) yield return BinAt(row, col); } public IEnumerable ActorsInBox(WPos a, WPos b) { var left = Math.Min(a.X, b.X); var top = Math.Min(a.Y, b.Y); var right = Math.Max(a.X, b.X); var bottom = Math.Max(a.Y, b.Y); var region = BinRectangleCoveringWorldArea(left, top, right, bottom); var minCol = region.Left; var minRow = region.Top; var maxCol = region.Right; var maxRow = region.Bottom; for (var row = minRow; row <= maxRow; row++) { for (var col = minCol; col <= maxCol; col++) { foreach (var actor in BinAt(row, col).Actors) { if (actor.IsInWorld) { var c = actor.CenterPosition; if (left <= c.X && c.X <= right && top <= c.Y && c.Y <= bottom) yield return actor; } } } } } public IEnumerable ActorsInWorld() { return bins.SelectMany(bin => bin.Actors.Where(actor => actor.IsInWorld)); } } }