When handling the Nodes collection in MiniYaml, individual nodes are located via one of two methods:
// Lookup a single key with linear search.
var node = yaml.Nodes.FirstOrDefault(n => n.Key == "SomeKey");
// Convert to dictionary, expecting many key lookups.
var dict = nodes.ToDictionary();
// Lookup a single key in the dictionary.
var node = dict["SomeKey"];
To simplify lookup of individual keys via linear search, provide helper methods NodeWithKeyOrDefault and NodeWithKey. These helpers do the equivalent of Single{OrDefault} searches. Whilst this requires checking the whole list, it provides a useful correctness check. Two duplicated keys in TS yaml are fixed as a result. We can also optimize the helpers to not use LINQ, avoiding allocation of the delegate to search for a key.
Adjust existing code to use either lnear searches or dictionary lookups based on whether it will be resolving many keys. Resolving few keys can be done with linear searches to avoid building a dictionary. Resolving many keys should be done with a dictionary to avoid quaradtic runtime from repeated linear searches.
358 lines
11 KiB
C#
358 lines
11 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Graphics;
|
|
using OpenRA.Traits;
|
|
|
|
namespace OpenRA.Mods.Common.Traits
|
|
{
|
|
public enum FootprintCellType
|
|
{
|
|
Empty = '_',
|
|
OccupiedPassable = '=',
|
|
Occupied = 'x',
|
|
OccupiedUntargetable = 'X',
|
|
OccupiedPassableTransitOnly = '+'
|
|
}
|
|
|
|
public class BuildingInfo : TraitInfo, IOccupySpaceInfo, IPlaceBuildingDecorationInfo
|
|
{
|
|
[Desc("Where you are allowed to place the building (Water, Clear, ...)")]
|
|
public readonly HashSet<string> TerrainTypes = new();
|
|
|
|
[Desc("x means cell is blocked, capital X means blocked but not counting as targetable, ",
|
|
"= means part of the footprint but passable, _ means completely empty.")]
|
|
[FieldLoader.LoadUsing(nameof(LoadFootprint))]
|
|
public readonly Dictionary<CVec, FootprintCellType> Footprint;
|
|
|
|
public readonly CVec Dimensions = new(1, 1);
|
|
|
|
[Desc("Shift center of the actor by this offset.")]
|
|
public readonly WVec LocalCenterOffset = WVec.Zero;
|
|
|
|
public readonly bool RequiresBaseProvider = false;
|
|
|
|
public readonly bool AllowInvalidPlacement = false;
|
|
|
|
public readonly bool AllowPlacementOnResources = false;
|
|
|
|
[Desc("Clear smudges from underneath the building footprint.")]
|
|
public readonly bool RemoveSmudgesOnBuild = true;
|
|
|
|
[Desc("Clear smudges from underneath the building footprint on sell.")]
|
|
public readonly bool RemoveSmudgesOnSell = true;
|
|
|
|
[Desc("Clear smudges from underneath the building footprint on transform.")]
|
|
public readonly bool RemoveSmudgesOnTransform = true;
|
|
|
|
public readonly string[] BuildSounds = Array.Empty<string>();
|
|
|
|
public readonly string[] UndeploySounds = Array.Empty<string>();
|
|
|
|
public override object Create(ActorInitializer init) { return new Building(init, this); }
|
|
|
|
protected static object LoadFootprint(MiniYaml yaml)
|
|
{
|
|
var footprintYaml = yaml.NodeWithKeyOrDefault("Footprint");
|
|
var footprintChars = footprintYaml?.Value.Value.Where(x => !char.IsWhiteSpace(x)).ToArray() ?? new[] { 'x' };
|
|
|
|
var dimensionsYaml = yaml.NodeWithKeyOrDefault("Dimensions");
|
|
var dim = dimensionsYaml != null ? FieldLoader.GetValue<CVec>("Dimensions", dimensionsYaml.Value.Value) : new CVec(1, 1);
|
|
|
|
if (footprintChars.Length != dim.X * dim.Y)
|
|
{
|
|
var fp = footprintYaml.Value.Value;
|
|
var dims = dim.X + "x" + dim.Y;
|
|
throw new YamlException($"Invalid footprint: {fp} does not match dimensions {dims}");
|
|
}
|
|
|
|
var index = 0;
|
|
var ret = new Dictionary<CVec, FootprintCellType>();
|
|
for (var y = 0; y < dim.Y; y++)
|
|
{
|
|
for (var x = 0; x < dim.X; x++)
|
|
{
|
|
var c = footprintChars[index++];
|
|
if (!Enum.IsDefined(typeof(FootprintCellType), (FootprintCellType)c))
|
|
throw new YamlException($"Invalid footprint cell type '{c}'");
|
|
|
|
ret[new CVec(x, y)] = (FootprintCellType)c;
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
public IEnumerable<CPos> FootprintTiles(CPos location, FootprintCellType type)
|
|
{
|
|
return Footprint.Where(kv => kv.Value == type).Select(kv => location + kv.Key);
|
|
}
|
|
|
|
public IEnumerable<CPos> Tiles(CPos location)
|
|
{
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedPassable))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.Occupied))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedUntargetable))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedPassableTransitOnly))
|
|
yield return t;
|
|
}
|
|
|
|
public IEnumerable<CPos> FrozenUnderFogTiles(CPos location)
|
|
{
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.Empty))
|
|
yield return t;
|
|
|
|
foreach (var t in Tiles(location))
|
|
yield return t;
|
|
}
|
|
|
|
public IEnumerable<CPos> OccupiedTiles(CPos location)
|
|
{
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.Occupied))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedUntargetable))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedPassableTransitOnly))
|
|
yield return t;
|
|
}
|
|
|
|
public IEnumerable<CPos> PathableTiles(CPos location)
|
|
{
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.Empty))
|
|
yield return t;
|
|
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedPassable))
|
|
yield return t;
|
|
}
|
|
|
|
public IEnumerable<CPos> TransitOnlyTiles(CPos location)
|
|
{
|
|
foreach (var t in FootprintTiles(location, FootprintCellType.OccupiedPassableTransitOnly))
|
|
yield return t;
|
|
}
|
|
|
|
public WVec CenterOffset(World w)
|
|
{
|
|
var off = (w.Map.CenterOfCell(new CPos(Dimensions.X, Dimensions.Y)) - w.Map.CenterOfCell(new CPos(1, 1))) / 2;
|
|
return off - new WVec(0, 0, off.Z) + LocalCenterOffset;
|
|
}
|
|
|
|
public BaseProvider FindBaseProvider(World world, Player p, CPos topLeft)
|
|
{
|
|
var center = world.Map.CenterOfCell(topLeft) + CenterOffset(world);
|
|
var mapBuildRadius = world.WorldActor.TraitOrDefault<MapBuildRadius>();
|
|
var allyBuildEnabled = mapBuildRadius != null && mapBuildRadius.AllyBuildRadiusEnabled;
|
|
|
|
if (mapBuildRadius == null || !mapBuildRadius.BuildRadiusEnabled)
|
|
return null;
|
|
|
|
foreach (var bp in world.ActorsWithTrait<BaseProvider>())
|
|
{
|
|
var validOwner = bp.Actor.Owner == p || (allyBuildEnabled && bp.Actor.Owner.RelationshipWith(p) == PlayerRelationship.Ally);
|
|
if (!validOwner || !bp.Trait.Ready())
|
|
continue;
|
|
|
|
// Range is counted from the center of the actor, not from each cell.
|
|
var target = Target.FromPos(bp.Actor.CenterPosition);
|
|
if (target.IsInRange(center, bp.Trait.Info.Range))
|
|
return bp.Trait;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static bool AnyGivesBuildableArea(IEnumerable<Actor> actors, Player p, bool allyBuildEnabled, RequiresBuildableAreaInfo rba)
|
|
{
|
|
foreach (var a in actors)
|
|
{
|
|
if (!a.IsInWorld)
|
|
continue;
|
|
|
|
if (a.Owner != p && (!allyBuildEnabled || a.Owner.RelationshipWith(p) != PlayerRelationship.Ally))
|
|
continue;
|
|
|
|
var overlaps = rba.AreaTypes.Overlaps(a.TraitsImplementing<GivesBuildableArea>()
|
|
.SelectMany(gba => gba.AreaTypes));
|
|
|
|
if (overlaps)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual bool IsCloseEnoughToBase(World world, Player p, ActorInfo ai, CPos topLeft)
|
|
{
|
|
var requiresBuildableArea = ai.TraitInfoOrDefault<RequiresBuildableAreaInfo>();
|
|
var mapBuildRadius = world.WorldActor.TraitOrDefault<MapBuildRadius>();
|
|
|
|
if (requiresBuildableArea == null || p.PlayerActor.Trait<DeveloperMode>().BuildAnywhere)
|
|
return true;
|
|
|
|
if (mapBuildRadius != null && mapBuildRadius.BuildRadiusEnabled && RequiresBaseProvider && FindBaseProvider(world, p, topLeft) == null)
|
|
return false;
|
|
|
|
var adjacent = requiresBuildableArea.Adjacent;
|
|
var buildingMaxBounds = Dimensions;
|
|
|
|
var scanStart = world.Map.Clamp(topLeft - new CVec(adjacent, adjacent));
|
|
var scanEnd = world.Map.Clamp(topLeft + buildingMaxBounds + new CVec(adjacent, adjacent));
|
|
|
|
var nearnessCandidates = new List<CPos>();
|
|
var bi = world.WorldActor.Trait<BuildingInfluence>();
|
|
var allyBuildEnabled = mapBuildRadius != null && mapBuildRadius.AllyBuildRadiusEnabled;
|
|
|
|
for (var y = scanStart.Y; y < scanEnd.Y; y++)
|
|
{
|
|
for (var x = scanStart.X; x < scanEnd.X; x++)
|
|
{
|
|
var c = new CPos(x, y);
|
|
if (AnyGivesBuildableArea(world.ActorMap.GetActorsAt(c), p, allyBuildEnabled, requiresBuildableArea))
|
|
{
|
|
nearnessCandidates.Add(c);
|
|
continue;
|
|
}
|
|
|
|
// Building bibs and pathable footprint cells are not included in the ActorMap
|
|
// TODO: Allow ActorMap to track these and finally remove the BuildingInfluence layer completely
|
|
if (AnyGivesBuildableArea(bi.GetBuildingsAt(c), p, allyBuildEnabled, requiresBuildableArea))
|
|
nearnessCandidates.Add(c);
|
|
}
|
|
}
|
|
|
|
var buildingTiles = Tiles(topLeft).ToList();
|
|
return nearnessCandidates
|
|
.Any(a => buildingTiles
|
|
.Any(b => Math.Abs(a.X - b.X) <= adjacent
|
|
&& Math.Abs(a.Y - b.Y) <= adjacent));
|
|
}
|
|
|
|
public IReadOnlyDictionary<CPos, SubCell> OccupiedCells(ActorInfo info, CPos topLeft, SubCell subCell = SubCell.Any)
|
|
{
|
|
return OccupiedTiles(topLeft)
|
|
.ToDictionary(c => c, c => SubCell.FullCell);
|
|
}
|
|
|
|
bool IOccupySpaceInfo.SharesCell => false;
|
|
|
|
public IEnumerable<IRenderable> RenderAnnotations(WorldRenderer wr, World w, ActorInfo ai, WPos centerPosition)
|
|
{
|
|
if (!RequiresBaseProvider)
|
|
return SpriteRenderable.None;
|
|
|
|
return w.ActorsWithTrait<BaseProvider>().SelectMany(a => a.Trait.RangeCircleRenderables());
|
|
}
|
|
}
|
|
|
|
public class Building : IOccupySpace, ITargetableCells, INotifySold, INotifyTransform, ISync,
|
|
INotifyAddedToWorld, INotifyRemovedFromWorld
|
|
{
|
|
public readonly BuildingInfo Info;
|
|
|
|
[Sync]
|
|
readonly CPos topLeft;
|
|
|
|
readonly Actor self;
|
|
readonly BuildingInfluence influence;
|
|
|
|
readonly (CPos, SubCell)[] occupiedCells;
|
|
readonly (CPos, SubCell)[] targetableCells;
|
|
readonly CPos[] transitOnlyCells;
|
|
|
|
public CPos TopLeft => topLeft;
|
|
public WPos CenterPosition { get; }
|
|
|
|
public Building(ActorInitializer init, BuildingInfo info)
|
|
{
|
|
self = init.Self;
|
|
topLeft = init.GetValue<LocationInit, CPos>();
|
|
Info = info;
|
|
influence = self.World.WorldActor.Trait<BuildingInfluence>();
|
|
|
|
occupiedCells = Info.OccupiedTiles(TopLeft)
|
|
.Select(c => (c, SubCell.FullCell)).ToArray();
|
|
|
|
targetableCells = Info.FootprintTiles(TopLeft, FootprintCellType.Occupied)
|
|
.Select(c => (c, SubCell.FullCell)).ToArray();
|
|
|
|
transitOnlyCells = Info.TransitOnlyTiles(TopLeft).ToArray();
|
|
|
|
CenterPosition = init.World.Map.CenterOfCell(topLeft) + Info.CenterOffset(init.World);
|
|
}
|
|
|
|
public (CPos, SubCell)[] OccupiedCells() { return occupiedCells; }
|
|
|
|
public CPos[] TransitOnlyCells() { return transitOnlyCells; }
|
|
|
|
(CPos, SubCell)[] ITargetableCells.TargetableCells() { return targetableCells; }
|
|
|
|
void INotifyAddedToWorld.AddedToWorld(Actor self)
|
|
{
|
|
AddedToWorld(self);
|
|
}
|
|
|
|
protected virtual void AddedToWorld(Actor self)
|
|
{
|
|
if (Info.RemoveSmudgesOnBuild)
|
|
RemoveSmudges();
|
|
|
|
self.World.AddToMaps(self, this);
|
|
influence.AddInfluence(self, Info.Tiles(self.Location));
|
|
}
|
|
|
|
void INotifyRemovedFromWorld.RemovedFromWorld(Actor self)
|
|
{
|
|
self.World.RemoveFromMaps(self, this);
|
|
influence.RemoveInfluence(self, Info.Tiles(self.Location));
|
|
}
|
|
|
|
void INotifySold.Selling(Actor self)
|
|
{
|
|
if (Info.RemoveSmudgesOnSell)
|
|
RemoveSmudges();
|
|
}
|
|
|
|
void INotifySold.Sold(Actor self) { }
|
|
|
|
void INotifyTransform.BeforeTransform(Actor self)
|
|
{
|
|
if (Info.RemoveSmudgesOnTransform)
|
|
RemoveSmudges();
|
|
|
|
foreach (var s in Info.UndeploySounds)
|
|
Game.Sound.PlayToPlayer(SoundType.World, self.Owner, s, self.CenterPosition);
|
|
}
|
|
|
|
void INotifyTransform.OnTransform(Actor self) { }
|
|
void INotifyTransform.AfterTransform(Actor self) { }
|
|
|
|
public void RemoveSmudges()
|
|
{
|
|
var smudgeLayers = self.World.WorldActor.TraitsImplementing<SmudgeLayer>();
|
|
|
|
foreach (var smudgeLayer in smudgeLayers)
|
|
foreach (var footprintTile in Info.Tiles(self.Location))
|
|
smudgeLayer.RemoveSmudge(footprintTile);
|
|
}
|
|
}
|
|
}
|