Files
OpenRA/OpenRA.Mods.Common/Traits/World/ResourceLayer.cs
RoosterDragon b7e0ed9b87 Improve lookups of nodes by key in MiniYaml.
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.
2023-09-23 14:31:04 +02:00

305 lines
9.0 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 readonly struct ResourceLayerContents
{
public static readonly ResourceLayerContents Empty = default;
public readonly string Type;
public readonly int Density;
public ResourceLayerContents(string type, int density)
{
Type = type;
Density = density;
}
}
[TraitLocation(SystemActors.World)]
[Desc("Attach this to the world actor.")]
public class ResourceLayerInfo : TraitInfo, IResourceLayerInfo, Requires<BuildingInfluenceInfo>
{
public class ResourceTypeInfo
{
[FieldLoader.Require]
[Desc("Resource index in the binary map data.")]
public readonly byte ResourceIndex = 0;
[FieldLoader.Require]
[Desc("Terrain type used to determine unit movement and minimap colors.")]
public readonly string TerrainType = null;
[FieldLoader.Require]
[Desc("Terrain types that this resource can spawn on.")]
public readonly HashSet<string> AllowedTerrainTypes = null;
[Desc("Maximum number of resource units allowed in a single cell.")]
public readonly int MaxDensity = 10;
public ResourceTypeInfo(MiniYaml yaml)
{
FieldLoader.Load(this, yaml);
}
}
[FieldLoader.LoadUsing(nameof(LoadResourceTypes))]
public readonly Dictionary<string, ResourceTypeInfo> ResourceTypes = null;
[Desc("Override the density saved in maps with values calculated based on the number of neighbouring resource cells.")]
public readonly bool RecalculateResourceDensity = false;
// Copied to EditorResourceLayerInfo, ResourceRendererInfo
protected static object LoadResourceTypes(MiniYaml yaml)
{
var ret = new Dictionary<string, ResourceTypeInfo>();
var resources = yaml.NodeWithKeyOrDefault("ResourceTypes");
if (resources != null)
foreach (var r in resources.Value.Nodes)
ret[r.Key] = new ResourceTypeInfo(r.Value);
return ret;
}
bool IResourceLayerInfo.TryGetTerrainType(string resourceType, out string terrainType)
{
if (resourceType == null || !ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
{
terrainType = null;
return false;
}
terrainType = resourceInfo.TerrainType;
return true;
}
bool IResourceLayerInfo.TryGetResourceIndex(string resourceType, out byte index)
{
if (resourceType == null || !ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
{
index = 0;
return false;
}
index = resourceInfo.ResourceIndex;
return true;
}
public override object Create(ActorInitializer init) { return new ResourceLayer(init.Self, this); }
}
public class ResourceLayer : IResourceLayer, IWorldLoaded
{
readonly ResourceLayerInfo info;
readonly World world;
protected readonly Map Map;
protected readonly BuildingInfluence BuildingInfluence;
protected readonly CellLayer<ResourceLayerContents> Content;
protected readonly Dictionary<byte, string> ResourceTypesByIndex;
int resCells;
public event Action<CPos, string> CellChanged;
public ResourceLayer(Actor self, ResourceLayerInfo info)
{
this.info = info;
world = self.World;
Map = world.Map;
BuildingInfluence = self.Trait<BuildingInfluence>();
Content = new CellLayer<ResourceLayerContents>(Map);
ResourceTypesByIndex = info.ResourceTypes.ToDictionary(
kv => kv.Value.ResourceIndex,
kv => kv.Key);
}
protected virtual void WorldLoaded(World w, WorldRenderer wr)
{
foreach (var cell in w.Map.AllCells)
{
var resource = world.Map.Resources[cell];
if (!ResourceTypesByIndex.TryGetValue(resource.Type, out var resourceType))
continue;
if (!AllowResourceAt(resourceType, cell))
continue;
Content[cell] = CreateResourceCell(resourceType, cell, resource.Index);
}
if (!info.RecalculateResourceDensity)
return;
// Set initial density based on the number of neighboring resources
foreach (var cell in w.Map.AllCells)
{
var resource = Content[cell];
if (resource.Type == null || !info.ResourceTypes.TryGetValue(resource.Type, out var resourceInfo))
continue;
var adjacent = 0;
var directions = CVec.Directions;
for (var i = 0; i < directions.Length; i++)
{
var c = cell + directions[i];
if (Content.Contains(c) && Content[c].Type == resource.Type)
++adjacent;
}
// Adjacent includes the current cell, so is always >= 1
var density = Math.Max(int2.Lerp(0, resourceInfo.MaxDensity, adjacent, 9), 1);
Content[cell] = new ResourceLayerContents(resource.Type, density);
}
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) { WorldLoaded(w, wr); }
protected virtual bool AllowResourceAt(string resourceType, CPos cell)
{
if (!Map.Contains(cell) || Map.Ramp[cell] != 0)
return false;
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
if (!resourceInfo.AllowedTerrainTypes.Contains(Map.GetTerrainInfo(cell).Type))
return false;
return !BuildingInfluence.AnyBuildingAt(cell);
}
ResourceLayerContents CreateResourceCell(string resourceType, CPos cell, int density)
{
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
{
world.Map.CustomTerrain[cell] = byte.MaxValue;
return ResourceLayerContents.Empty;
}
world.Map.CustomTerrain[cell] = world.Map.Rules.TerrainInfo.GetTerrainIndex(resourceInfo.TerrainType);
++resCells;
return new ResourceLayerContents(resourceType, density.Clamp(1, resourceInfo.MaxDensity));
}
bool CanAddResource(string resourceType, CPos cell, int amount = 1)
{
if (!world.Map.Contains(cell))
return false;
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
var content = Content[cell];
if (content.Type == null)
return amount <= resourceInfo.MaxDensity && AllowResourceAt(resourceType, cell);
if (content.Type != resourceType)
return false;
return content.Density + amount <= resourceInfo.MaxDensity;
}
int AddResource(string resourceType, CPos cell, int amount = 1)
{
if (!Content.Contains(cell))
return 0;
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
var content = Content[cell];
if (content.Type == null)
content = CreateResourceCell(resourceType, cell, 0);
if (content.Type != resourceType)
return 0;
var oldDensity = content.Density;
var density = Math.Min(resourceInfo.MaxDensity, oldDensity + amount);
Content[cell] = new ResourceLayerContents(content.Type, density);
CellChanged?.Invoke(cell, content.Type);
return density - oldDensity;
}
int RemoveResource(string resourceType, CPos cell, int amount = 1)
{
if (!Content.Contains(cell))
return 0;
var content = Content[cell];
if (content.Type == null || content.Type != resourceType)
return 0;
var oldDensity = content.Density;
var density = Math.Max(0, oldDensity - amount);
if (density == 0)
{
Content[cell] = ResourceLayerContents.Empty;
Map.CustomTerrain[cell] = byte.MaxValue;
--resCells;
CellChanged?.Invoke(cell, null);
}
else
{
Content[cell] = new ResourceLayerContents(content.Type, density);
CellChanged?.Invoke(cell, content.Type);
}
return oldDensity - density;
}
void ClearResources(CPos cell)
{
if (!Content.Contains(cell))
return;
// Don't break other users of CustomTerrain if there are no resources
var content = Content[cell];
if (content.Type == null)
return;
Content[cell] = ResourceLayerContents.Empty;
Map.CustomTerrain[cell] = byte.MaxValue;
--resCells;
CellChanged?.Invoke(cell, null);
}
ResourceLayerContents IResourceLayer.GetResource(CPos cell) { return Content.Contains(cell) ? Content[cell] : default; }
int IResourceLayer.GetMaxDensity(string resourceType)
{
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
return resourceInfo.MaxDensity;
}
bool IResourceLayer.CanAddResource(string resourceType, CPos cell, int amount) { return CanAddResource(resourceType, cell, amount); }
int IResourceLayer.AddResource(string resourceType, CPos cell, int amount) { return AddResource(resourceType, cell, amount); }
int IResourceLayer.RemoveResource(string resourceType, CPos cell, int amount) { return RemoveResource(resourceType, cell, amount); }
void IResourceLayer.ClearResources(CPos cell) { ClearResources(cell); }
bool IResourceLayer.IsVisible(CPos cell) { return !world.FogObscures(cell); }
bool IResourceLayer.IsEmpty => resCells < 1;
IResourceLayerInfo IResourceLayer.Info => info;
}
}