Overhaul resource layer logic:

* ResourceType trait has been removed.
* Simulation-related data is now defined on the
  ResourceLayer (which mods can subclass/replace).
* Support non-money resources by moving the resource
  values to the PlayerResources trait.
* Allow mods to disable the neighbour density override
  and instead always use the map-defined densities.
* Allow mods to define their own resource placement
  logic (e.g. allow resources on slopes) by subclassing
  (Editor)ResourceLayer.
* Improve ability to subclass/override ResourceRenderer
  by exposing more virtual methods.
This commit is contained in:
Paul Chote
2021-03-07 17:48:52 +00:00
committed by reaperrr
parent c35e9fb016
commit 0bdd46451e
20 changed files with 625 additions and 416 deletions

View File

@@ -57,7 +57,6 @@ namespace OpenRA.Mods.Common.Traits
{
readonly Actor self;
readonly RefineryInfo info;
readonly Dictionary<string, int> resourceValues;
PlayerResources playerResources;
IEnumerable<int> resourceValueModifiers;
@@ -83,8 +82,6 @@ namespace OpenRA.Mods.Common.Traits
this.info = info;
playerResources = self.Owner.PlayerActor.Trait<PlayerResources>();
currentDisplayTick = info.TickRate;
resourceValues = self.World.WorldActor.TraitsImplementing<ResourceType>()
.ToDictionary(r => r.Info.Type, r => r.Info.ValuePerUnit);
}
void INotifyCreated.Created(Actor self)
@@ -105,7 +102,7 @@ namespace OpenRA.Mods.Common.Traits
int IAcceptResources.AcceptResources(string resourceType, int count)
{
if (!resourceValues.TryGetValue(resourceType, out var resourceValue))
if (!playerResources.Info.ResourceValues.TryGetValue(resourceType, out var resourceValue))
return 0;
var value = Util.ApplyPercentageModifiers(count * resourceValue, resourceValueModifiers);

View File

@@ -52,6 +52,9 @@ namespace OpenRA.Mods.Common.Traits
[NotificationReference("Sounds")]
public readonly string CashTickDownNotification = null;
[Desc("Monetery value of each resource type.", "Dictionary of [resource type]: [value per unit].")]
public readonly Dictionary<string, int> ResourceValues = new Dictionary<string, int>();
IEnumerable<LobbyOption> ILobbyOptions.LobbyOptions(Ruleset rules)
{
var startingCash = SelectableCash.ToDictionary(c => c.ToString(), c => "$" + c.ToString());

View File

@@ -13,22 +13,47 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
[Desc("Required for the map editor to work. Attach this to the world actor.")]
public class EditorResourceLayerInfo : TraitInfo, IResourceLayerInfo, Requires<ResourceTypeInfo>
public class EditorResourceLayerInfo : TraitInfo, IResourceLayerInfo, IMapPreviewSignatureInfo
{
public override object Create(ActorInitializer init) { return new EditorResourceLayer(init.Self); }
[FieldLoader.LoadUsing(nameof(LoadResourceTypes))]
public readonly Dictionary<string, ResourceLayerInfo.ResourceTypeInfo> ResourceTypes = null;
// Copied from ResourceLayerInfo
protected static object LoadResourceTypes(MiniYaml yaml)
{
var ret = new Dictionary<string, ResourceLayerInfo.ResourceTypeInfo>();
var resources = yaml.Nodes.FirstOrDefault(n => n.Key == "ResourceTypes");
if (resources != null)
foreach (var r in resources.Value.Nodes)
ret[r.Key] = new ResourceLayerInfo.ResourceTypeInfo(r.Value);
return ret;
}
[Desc("Override the density saved in maps with values calculated based on the number of neighbouring resource cells.")]
public readonly bool RecalculateResourceDensity = false;
void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<(MPos, Color)> destinationBuffer)
{
ResourceLayerInfo.PopulateMapPreviewSignatureCells(map, ResourceTypes, destinationBuffer);
}
public override object Create(ActorInitializer init) { return new EditorResourceLayer(init.Self, this); }
}
public class EditorResourceLayer : IResourceLayer, IWorldLoaded, INotifyActorDisposing
{
readonly EditorResourceLayerInfo info;
protected readonly Map Map;
protected readonly Dictionary<string, ResourceTypeInfo> ResourceInfo;
protected readonly Dictionary<int, string> Resources;
protected readonly Dictionary<byte, string> ResourceTypesByIndex;
protected readonly CellLayer<ResourceLayerContents> Tiles;
protected Dictionary<string, int> resourceValues;
public int NetWorth { get; protected set; }
@@ -39,10 +64,7 @@ namespace OpenRA.Mods.Common.Traits
ResourceLayerContents IResourceLayer.GetResource(CPos cell) { return Tiles.Contains(cell) ? Tiles[cell] : default; }
int IResourceLayer.GetMaxDensity(string resourceType)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
return 0;
return resourceInfo.MaxDensity;
return info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo) ? resourceInfo.MaxDensity : 0;
}
bool IResourceLayer.CanAddResource(string resourceType, CPos cell, int amount) { return CanAddResource(resourceType, cell, amount); }
@@ -52,17 +74,17 @@ namespace OpenRA.Mods.Common.Traits
bool IResourceLayer.IsVisible(CPos cell) { return Map.Contains(cell); }
bool IResourceLayer.IsEmpty => false;
public EditorResourceLayer(Actor self)
public EditorResourceLayer(Actor self, EditorResourceLayerInfo info)
{
if (self.World.Type != WorldType.Editor)
return;
this.info = info;
Map = self.World.Map;
Tiles = new CellLayer<ResourceLayerContents>(Map);
ResourceInfo = self.TraitsImplementing<ResourceType>()
.ToDictionary(r => r.Info.Type, r => r.Info);
Resources = ResourceInfo.Values
.ToDictionary(r => r.ResourceType, r => r.Type);
ResourceTypesByIndex = info.ResourceTypes.ToDictionary(
kv => kv.Value.ResourceIndex,
kv => kv.Key);
Map.Resources.CellEntryChanged += UpdateCell;
}
@@ -72,6 +94,9 @@ namespace OpenRA.Mods.Common.Traits
if (w.Type != WorldType.Editor)
return;
var playerResourcesInfo = w.Map.Rules.Actors["player"].TraitInfoOrDefault<PlayerResourcesInfo>();
resourceValues = playerResourcesInfo?.ResourceValues ?? new Dictionary<string, int>();
foreach (var cell in Map.AllCells)
UpdateCell(cell);
}
@@ -87,9 +112,9 @@ namespace OpenRA.Mods.Common.Traits
var newTile = ResourceLayerContents.Empty;
var newTerrain = byte.MaxValue;
if (Resources.TryGetValue(tile.Type, out var resourceType) && ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (ResourceTypesByIndex.TryGetValue(tile.Type, out var resourceType) && info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
{
newTile = new ResourceLayerContents(resourceType, CalculateCellDensity(resourceType, cell));
newTile = new ResourceLayerContents(resourceType, CalculateCellDensity(new ResourceLayerContents(resourceType, tile.Index), cell));
newTerrain = Map.Rules.TerrainInfo.GetTerrainIndex(resourceInfo.TerrainType);
}
@@ -102,7 +127,10 @@ namespace OpenRA.Mods.Common.Traits
Map.CustomTerrain[uv] = newTerrain;
CellChanged?.Invoke(cell, newTile.Type);
// Neighbouring cell density depends on this cell
if (!info.RecalculateResourceDensity)
return;
// Update neighbour density to account for this cell
foreach (var d in CVec.Directions)
{
var neighbouringCell = cell + d;
@@ -110,7 +138,10 @@ namespace OpenRA.Mods.Common.Traits
continue;
var neighbouringTile = Tiles[neighbouringCell];
var density = CalculateCellDensity(neighbouringTile.Type, neighbouringCell);
if (neighbouringTile.Type == null)
continue;
var density = CalculateCellDensity(neighbouringTile, neighbouringCell);
if (neighbouringTile.Density == density)
continue;
@@ -124,19 +155,22 @@ namespace OpenRA.Mods.Common.Traits
void UpdateNetWorth(string oldResourceType, int oldDensity, string newResourceType, int newDensity)
{
// Density + 1 as workaround for fixing ResourceLayer.Harvest as it would be very disruptive to balancing
if (oldResourceType != null && oldDensity > 0 && ResourceInfo.TryGetValue(oldResourceType, out var oldResourceInfo))
NetWorth -= (oldDensity + 1) * oldResourceInfo.ValuePerUnit;
if (oldResourceType != null && oldDensity > 0 && resourceValues.TryGetValue(oldResourceType, out var oldResourceValue))
NetWorth -= (oldDensity + 1) * oldResourceValue;
if (newResourceType != null && newDensity > 0 && ResourceInfo.TryGetValue(newResourceType, out var newResourceInfo))
NetWorth += (newDensity + 1) * newResourceInfo.ValuePerUnit;
if (newResourceType != null && newDensity > 0 && resourceValues.TryGetValue(newResourceType, out var newResourceValue))
NetWorth += (newDensity + 1) * newResourceValue;
}
public int CalculateCellDensity(string resourceType, CPos c)
protected virtual int CalculateCellDensity(ResourceLayerContents contents, CPos c)
{
var resources = Map.Resources;
if (resourceType == null || !ResourceInfo.TryGetValue(resourceType, out var resourceInfo) || resources[c].Type != resourceInfo.ResourceType)
if (contents.Type == null || !info.ResourceTypes.TryGetValue(contents.Type, out var resourceInfo) || resources[c].Type != resourceInfo.ResourceIndex)
return 0;
if (!info.RecalculateResourceDensity)
return contents.Density.Clamp(1, resourceInfo.MaxDensity);
// Set density based on the number of neighboring resources
var adjacent = 0;
for (var u = -1; u < 2; u++)
@@ -144,7 +178,7 @@ namespace OpenRA.Mods.Common.Traits
for (var v = -1; v < 2; v++)
{
var cell = c + new CVec(u, v);
if (resources.Contains(cell) && resources[cell].Type == resourceInfo.ResourceType)
if (resources.Contains(cell) && resources[cell].Type == resourceInfo.ResourceIndex)
adjacent++;
}
}
@@ -152,23 +186,20 @@ namespace OpenRA.Mods.Common.Traits
return Math.Max(int2.Lerp(0, resourceInfo.MaxDensity, adjacent, 9), 1);
}
bool AllowResourceAt(string resourceType, CPos cell)
protected virtual bool AllowResourceAt(string resourceType, CPos cell)
{
var mapResources = Map.Resources;
if (!mapResources.Contains(cell))
if (!Map.Ramp.Contains(cell) || Map.Ramp[cell] != 0)
return false;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
// Ignore custom terrain types when spawning resources in the editor
var terrainInfo = Map.Rules.TerrainInfo;
var terrainType = terrainInfo.TerrainTypes[terrainInfo.GetTerrainInfo(Map.Tiles[cell]).TerrainType].Type;
if (!resourceInfo.AllowedTerrainTypes.Contains(terrainType))
return false;
// TODO: Check against actors in the EditorActorLayer
return resourceInfo.AllowOnRamps || Map.Ramp[cell] == 0;
return resourceInfo.AllowedTerrainTypes.Contains(terrainType);
}
bool CanAddResource(string resourceType, CPos cell, int amount = 1)
@@ -177,57 +208,57 @@ namespace OpenRA.Mods.Common.Traits
if (!resources.Contains(cell))
return false;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
// The editor allows the user to replace one resource type with another, so treat mismatching resource type as an empty cell
var content = resources[cell];
if (content.Type != resourceInfo.ResourceType)
if (content.Type != resourceInfo.ResourceIndex)
return amount <= resourceInfo.MaxDensity && AllowResourceAt(resourceType, cell);
var oldDensity = content.Type == resourceInfo.ResourceType ? content.Index : 0;
var oldDensity = content.Type == resourceInfo.ResourceIndex ? content.Index : 0;
return oldDensity + amount <= resourceInfo.MaxDensity;
}
int AddResource(string resourceType, CPos cell, int amount = 1)
protected virtual int AddResource(string resourceType, CPos cell, int amount = 1)
{
var resources = Map.Resources;
if (!resources.Contains(cell))
return 0;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
// The editor allows the user to replace one resource type with another, so treat mismatching resource type as an empty cell
var content = resources[cell];
var oldDensity = content.Type == resourceInfo.ResourceType ? content.Index : 0;
var oldDensity = content.Type == resourceInfo.ResourceIndex ? content.Index : 0;
var density = (byte)Math.Min(resourceInfo.MaxDensity, oldDensity + amount);
Map.Resources[cell] = new ResourceTile((byte)resourceInfo.ResourceType, density);
Map.Resources[cell] = new ResourceTile((byte)resourceInfo.ResourceIndex, density);
return density - oldDensity;
}
int RemoveResource(string resourceType, CPos cell, int amount = 1)
protected virtual int RemoveResource(string resourceType, CPos cell, int amount = 1)
{
var resources = Map.Resources;
if (!resources.Contains(cell))
return 0;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
var content = resources[cell];
if (content.Type == 0 || content.Type != resourceInfo.ResourceType)
if (content.Type == 0 || content.Type != resourceInfo.ResourceIndex)
return 0;
var oldDensity = content.Index;
var density = (byte)Math.Max(0, oldDensity - amount);
resources[cell] = density > 0 ? new ResourceTile((byte)resourceInfo.ResourceType, density) : default;
resources[cell] = density > 0 ? new ResourceTile(resourceInfo.ResourceIndex, density) : default;
return oldDensity - density;
}
void ClearResources(CPos cell)
protected virtual void ClearResources(CPos cell)
{
Map.Resources[cell] = default;
}

View File

@@ -13,6 +13,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
@@ -31,99 +32,158 @@ namespace OpenRA.Mods.Common.Traits
}
[Desc("Attach this to the world actor.")]
public class ResourceLayerInfo : TraitInfo, IResourceLayerInfo, Requires<ResourceTypeInfo>, Requires<BuildingInfluenceInfo>
public class ResourceLayerInfo : TraitInfo, IResourceLayerInfo, Requires<BuildingInfluenceInfo>, IMapPreviewSignatureInfo
{
public override object Create(ActorInitializer init) { return new ResourceLayer(init.Self); }
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.Nodes.FirstOrDefault(n => n.Key == "ResourceTypes");
if (resources != null)
foreach (var r in resources.Value.Nodes)
ret[r.Key] = new ResourceTypeInfo(r.Value);
return ret;
}
public static void PopulateMapPreviewSignatureCells(Map map, Dictionary<string, ResourceTypeInfo> resources, List<(MPos, Color)> destinationBuffer)
{
var terrainInfo = map.Rules.TerrainInfo;
var colors = resources.Values.ToDictionary(
r => r.ResourceIndex,
r => terrainInfo.TerrainTypes[terrainInfo.GetTerrainIndex(r.TerrainType)].Color);
for (var i = 0; i < map.MapSize.X; i++)
{
for (var j = 0; j < map.MapSize.Y; j++)
{
var cell = new MPos(i, j);
if (colors.TryGetValue(map.Resources[cell].Type, out var color))
destinationBuffer.Add((cell, color));
}
}
}
void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<(MPos, Color)> destinationBuffer)
{
PopulateMapPreviewSignatureCells(map, ResourceTypes, destinationBuffer);
}
public override object Create(ActorInitializer init) { return new ResourceLayer(init.Self, this); }
}
public class ResourceLayer : IResourceLayer, IWorldLoaded
{
readonly ResourceLayerInfo info;
readonly World world;
readonly BuildingInfluence buildingInfluence;
protected readonly Dictionary<string, ResourceTypeInfo> ResourceInfo;
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)
public ResourceLayer(Actor self, ResourceLayerInfo info)
{
this.info = info;
world = self.World;
buildingInfluence = self.Trait<BuildingInfluence>();
ResourceInfo = self.TraitsImplementing<ResourceType>()
.ToDictionary(r => r.Info.Type, r => r.Info);
Content = new CellLayer<ResourceLayerContents>(world.Map);
Map = world.Map;
BuildingInfluence = self.Trait<BuildingInfluence>();
Content = new CellLayer<ResourceLayerContents>(Map);
ResourceTypesByIndex = info.ResourceTypes.ToDictionary(
kv => kv.Value.ResourceIndex,
kv => kv.Key);
}
int GetAdjacentCellsWith(string resourceType, CPos cell)
protected virtual void WorldLoaded(World w, WorldRenderer wr)
{
var sum = 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 == resourceType)
++sum;
}
return sum;
}
public void WorldLoaded(World w, WorldRenderer wr)
{
var resources = w.WorldActor.TraitsImplementing<ResourceType>()
.ToDictionary(r => r.Info.ResourceType, r => r.Info.Type);
foreach (var cell in w.Map.AllCells)
{
if (!resources.TryGetValue(w.Map.Resources[cell].Type, out var resourceType))
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);
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 resourceType = Content[cell].Type;
if (resourceType != null && ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
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++)
{
// Set initial density based on the number of neighboring resources
// Adjacent includes the current cell, so is always >= 1
var adjacent = GetAdjacentCellsWith(resourceType, cell);
var density = int2.Lerp(0, resourceInfo.MaxDensity, adjacent, 9);
Content[cell] = new ResourceLayerContents(Content[cell].Type, Math.Max(density, 1));
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);
}
}
public bool AllowResourceAt(string resourceType, CPos cell)
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) { WorldLoaded(w, wr); }
protected virtual bool AllowResourceAt(string resourceType, CPos cell)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!Map.Contains(cell) || Map.Ramp[cell] != 0)
return false;
if (!world.Map.Contains(cell))
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
if (!resourceInfo.AllowedTerrainTypes.Contains(world.Map.GetTerrainInfo(cell).Type))
if (!resourceInfo.AllowedTerrainTypes.Contains(Map.GetTerrainInfo(cell).Type))
return false;
if (!resourceInfo.AllowUnderActors && world.ActorMap.AnyActorsAt(cell))
return false;
if (!resourceInfo.AllowUnderBuildings && buildingInfluence.GetBuildingAt(cell) != null)
return false;
return resourceInfo.AllowOnRamps || world.Map.Ramp[cell] == 0;
return BuildingInfluence.GetBuildingAt(cell) == null;
}
ResourceLayerContents CreateResourceCell(string resourceType, CPos cell)
ResourceLayerContents CreateResourceCell(string resourceType, CPos cell, int density)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
{
world.Map.CustomTerrain[cell] = byte.MaxValue;
return ResourceLayerContents.Empty;
@@ -132,7 +192,7 @@ namespace OpenRA.Mods.Common.Traits
world.Map.CustomTerrain[cell] = world.Map.Rules.TerrainInfo.GetTerrainIndex(resourceInfo.TerrainType);
++resCells;
return new ResourceLayerContents(resourceType, 0);
return new ResourceLayerContents(resourceType, density.Clamp(1, resourceInfo.MaxDensity));
}
bool CanAddResource(string resourceType, CPos cell, int amount = 1)
@@ -140,7 +200,7 @@ namespace OpenRA.Mods.Common.Traits
if (!world.Map.Contains(cell))
return false;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return false;
var content = Content[cell];
@@ -158,12 +218,12 @@ namespace OpenRA.Mods.Common.Traits
if (!Content.Contains(cell))
return 0;
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (resourceType == null || !info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
var content = Content[cell];
if (content.Type == null)
content = CreateResourceCell(resourceType, cell);
content = CreateResourceCell(resourceType, cell, 0);
if (content.Type != resourceType)
return 0;
@@ -192,7 +252,7 @@ namespace OpenRA.Mods.Common.Traits
if (density == 0)
{
Content[cell] = ResourceLayerContents.Empty;
world.Map.CustomTerrain[cell] = byte.MaxValue;
Map.CustomTerrain[cell] = byte.MaxValue;
--resCells;
CellChanged?.Invoke(cell, null);
@@ -217,7 +277,7 @@ namespace OpenRA.Mods.Common.Traits
return;
Content[cell] = ResourceLayerContents.Empty;
world.Map.CustomTerrain[cell] = byte.MaxValue;
Map.CustomTerrain[cell] = byte.MaxValue;
--resCells;
CellChanged?.Invoke(cell, null);
@@ -226,7 +286,7 @@ namespace OpenRA.Mods.Common.Traits
ResourceLayerContents IResourceLayer.GetResource(CPos cell) { return Content.Contains(cell) ? Content[cell] : default; }
int IResourceLayer.GetMaxDensity(string resourceType)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
return 0;
return resourceInfo.MaxDensity;

View File

@@ -21,56 +21,97 @@ namespace OpenRA.Mods.Common.Traits
[Desc("Visualizes the state of the `ResourceLayer`.", " Attach this to the world actor.")]
public class ResourceRendererInfo : TraitInfo, Requires<IResourceLayerInfo>
{
[FieldLoader.Require]
[Desc("Only render these ResourceType names.")]
public readonly string[] RenderTypes = null;
public class ResourceTypeInfo
{
[Desc("Sequence image that holds the different variants.")]
public readonly string Image = "resources";
[FieldLoader.Require]
[SequenceReference(nameof(Image))]
[Desc("Randomly chosen image sequences.")]
public readonly string[] Sequences = { };
[PaletteReference]
[Desc("Palette used for rendering the resource sprites.")]
public readonly string Palette = TileSet.TerrainPaletteInternalName;
[FieldLoader.Require]
[Desc("Resource name used by tooltips.")]
public readonly string Name = null;
public ResourceTypeInfo(MiniYaml yaml)
{
FieldLoader.Load(this, yaml);
}
}
[FieldLoader.LoadUsing(nameof(LoadResourceTypes))]
public readonly Dictionary<string, ResourceTypeInfo> ResourceTypes = null;
// Copied from ResourceLayerInfo
protected static object LoadResourceTypes(MiniYaml yaml)
{
var ret = new Dictionary<string, ResourceTypeInfo>();
var resources = yaml.Nodes.FirstOrDefault(n => n.Key == "ResourceTypes");
if (resources != null)
foreach (var r in resources.Value.Nodes)
ret[r.Key] = new ResourceTypeInfo(r.Value);
return ret;
}
public override object Create(ActorInitializer init) { return new ResourceRenderer(init.Self, this); }
}
public class ResourceRenderer : IResourceRenderer, IWorldLoaded, IRenderOverlay, ITickRender, INotifyActorDisposing
{
protected readonly IResourceLayer ResourceLayer;
protected readonly CellLayer<RendererCellContents> RenderContent;
protected readonly ResourceRendererInfo Info;
protected readonly Dictionary<string, ResourceType> ResourceInfo;
protected readonly IResourceLayer ResourceLayer;
protected readonly CellLayer<RendererCellContents> RenderContents;
protected readonly Dictionary<string, Dictionary<string, ISpriteSequence>> Variants = new Dictionary<string, Dictionary<string, ISpriteSequence>>();
protected readonly World World;
readonly HashSet<CPos> dirty = new HashSet<CPos>();
readonly Queue<CPos> cleanDirty = new Queue<CPos>();
TerrainSpriteLayer shadowLayer;
TerrainSpriteLayer spriteLayer;
bool disposed;
public ResourceRenderer(Actor self, ResourceRendererInfo info)
{
Info = info;
World = self.World;
ResourceLayer = self.Trait<IResourceLayer>();
ResourceLayer.CellChanged += AddDirtyCell;
ResourceInfo = self.TraitsImplementing<ResourceType>()
.ToDictionary(r => r.Info.Type, r => r);
RenderContent = new CellLayer<RendererCellContents>(self.World.Map);
RenderContents = new CellLayer<RendererCellContents>(self.World.Map);
}
void AddDirtyCell(CPos cell, string resourceType)
{
if (resourceType == null || Info.RenderTypes.Contains(resourceType))
if (resourceType == null || Info.ResourceTypes.ContainsKey(resourceType))
dirty.Add(cell);
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr)
protected virtual void WorldLoaded(World w, WorldRenderer wr)
{
foreach (var resourceType in ResourceInfo.Values)
var sequences = w.Map.Rules.Sequences;
foreach (var kv in Info.ResourceTypes)
{
var resourceInfo = kv.Value;
var resourceVariants = resourceInfo.Sequences
.ToDictionary(v => v, v => sequences.GetSequence(resourceInfo.Image, v));
Variants.Add(kv.Key, resourceVariants);
if (spriteLayer == null)
{
var first = resourceType.Variants.First().Value.GetSprite(0);
var first = resourceVariants.First().Value.GetSprite(0);
var emptySprite = new Sprite(first.Sheet, Rectangle.Empty, TextureChannel.Alpha);
spriteLayer = new TerrainSpriteLayer(w, wr, emptySprite, first.BlendMode, wr.World.Type != WorldType.Editor);
}
if (shadowLayer == null)
{
var firstWithShadow = resourceType.Variants.Values.FirstOrDefault(v => v.ShadowStart > 0);
var firstWithShadow = resourceVariants.Values.FirstOrDefault(v => v.ShadowStart > 0);
if (firstWithShadow != null)
{
var first = firstWithShadow.GetShadow(0, WAngle.Zero);
@@ -80,30 +121,36 @@ namespace OpenRA.Mods.Common.Traits
}
// All resources must share a blend mode
var sprites = resourceType.Variants.Values.SelectMany(v => Exts.MakeArray(v.Length, x => v.GetSprite(x)));
var sprites = resourceVariants.Values.SelectMany(v => Exts.MakeArray(v.Length, x => v.GetSprite(x)));
if (sprites.Any(s => s.BlendMode != spriteLayer.BlendMode))
throw new InvalidDataException("Resource sprites specify different blend modes. "
+ "Try using different ResourceRenderer traits for resource types that use different blend modes.");
}
// Initialize the RenderContent with the initial map state
// because the shroud may not be enabled.
// Initialize the RenderContent with the initial map state so it is visible
// through the fog with the Explored Map option enabled
foreach (var cell in w.Map.AllCells)
{
var type = ResourceLayer.GetResource(cell).Type;
if (type != null && Info.RenderTypes.Contains(type))
var resource = ResourceLayer.GetResource(cell);
var rendererCellContents = CreateRenderCellContents(wr, resource, cell);
if (rendererCellContents.Type != null)
{
var resourceContent = ResourceLayer.GetResource(cell);
if (!ResourceInfo.TryGetValue(resourceContent.Type, out var resourceType))
continue;
var rendererCellContents = new RendererCellContents(ChooseRandomVariant(resourceType), resourceType, resourceContent.Density);
RenderContent[cell] = rendererCellContents;
RenderContents[cell] = rendererCellContents;
UpdateRenderedSprite(cell, rendererCellContents);
}
}
}
void IWorldLoaded.WorldLoaded(World w, WorldRenderer wr) { WorldLoaded(w, wr); }
protected RendererCellContents CreateRenderCellContents(WorldRenderer wr, ResourceLayerContents contents, CPos cell)
{
if (contents.Type != null && contents.Density > 0 && Info.ResourceTypes.TryGetValue(contents.Type, out var resourceInfo))
return new RendererCellContents(contents.Type, contents.Density, resourceInfo, ChooseVariant(contents.Type, cell), wr.Palette(resourceInfo.Palette));
return RendererCellContents.Empty;
}
protected void UpdateSpriteLayers(CPos cell, ISpriteSequence sequence, int frame, PaletteReference palette)
{
// resource.Type is meaningless (and may be null) if resource.Sequence is null
@@ -132,27 +179,21 @@ namespace OpenRA.Mods.Common.Traits
if (!ResourceLayer.IsVisible(cell))
continue;
var resourceContent = ResourceLayer.GetResource(cell);
if (resourceContent.Density > 0)
var rendererCellContents = RendererCellContents.Empty;
var contents = ResourceLayer.GetResource(cell);
if (contents.Density > 0)
{
var cellContents = RenderContent[cell];
var resourceData = ResourceInfo[resourceContent.Type];
var variant = cellContents.Variant;
if (cellContents.Variant == null || cellContents.Type.Info.Type != resourceContent.Type)
variant = ChooseRandomVariant(resourceData);
rendererCellContents = RenderContents[cell];
var rendererCellContents = new RendererCellContents(variant, resourceData, resourceContent.Density);
RenderContent[cell] = rendererCellContents;
UpdateRenderedSprite(cell, rendererCellContents);
}
else
{
var rendererCellContents = RendererCellContents.Empty;
RenderContent[cell] = rendererCellContents;
UpdateRenderedSprite(cell, rendererCellContents);
// Contents are the same, so just update the density
if (rendererCellContents.Type == contents.Type)
rendererCellContents = new RendererCellContents(rendererCellContents, contents.Density);
else
rendererCellContents = CreateRenderCellContents(wr, contents, cell);
}
RenderContents[cell] = rendererCellContents;
UpdateRenderedSprite(cell, rendererCellContents);
cleanDirty.Enqueue(cell);
}
@@ -162,28 +203,17 @@ namespace OpenRA.Mods.Common.Traits
protected virtual void UpdateRenderedSprite(CPos cell, RendererCellContents content)
{
var density = content.Density;
var type = content.Type;
if (content.Density > 0)
{
// The call chain for this method (that starts with AddDirtyCell()) guarantees
// that the new content type would still be suitable for this renderer,
// but that is a bit too fragile to rely on in case the code starts changing.
if (!Info.RenderTypes.Contains(type.Info.Type))
return;
var sprites = type.Variants[content.Variant];
var maxDensity = type.Info.MaxDensity;
var frame = int2.Lerp(0, sprites.Length - 1, density, maxDensity);
UpdateSpriteLayers(cell, sprites, frame, type.Palette);
var maxDensity = ResourceLayer.GetMaxDensity(content.Type);
var frame = int2.Lerp(0, content.Sequence.Length - 1, content.Density, maxDensity);
UpdateSpriteLayers(cell, content.Sequence, frame, content.Palette);
}
else
UpdateSpriteLayers(cell, null, 0, null);
}
bool disposed;
void INotifyActorDisposing.Disposing(Actor self)
protected virtual void Disposing(Actor self)
{
if (disposed)
return;
@@ -196,16 +226,18 @@ namespace OpenRA.Mods.Common.Traits
disposed = true;
}
protected virtual string ChooseRandomVariant(ResourceType t)
void INotifyActorDisposing.Disposing(Actor self) { Disposing(self); }
protected virtual ISpriteSequence ChooseVariant(string resourceType, CPos cell)
{
return t.Variants.Keys.Random(Game.CosmeticRandom);
return Variants[resourceType].Values.Random(World.LocalRandom);
}
protected virtual string GetRenderedResourceType(CPos cell) { return RenderContent[cell].Type.Info.Type; }
protected virtual string GetRenderedResourceType(CPos cell) { return RenderContents[cell].Type; }
protected virtual string GetRenderedResourceTooltip(CPos cell) { return RenderContent[cell].Type?.Info.Name; }
protected virtual string GetRenderedResourceTooltip(CPos cell) { return RenderContents[cell].Info?.Name; }
IEnumerable<string> IResourceRenderer.ResourceTypes => ResourceInfo.Keys;
IEnumerable<string> IResourceRenderer.ResourceTypes => Info.ResourceTypes.Keys;
string IResourceRenderer.GetRenderedResourceType(CPos cell) { return GetRenderedResourceType(cell); }
@@ -213,13 +245,16 @@ namespace OpenRA.Mods.Common.Traits
IEnumerable<IRenderable> IResourceRenderer.RenderUIPreview(WorldRenderer wr, string resourceType, int2 origin, float scale)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!Variants.TryGetValue(resourceType, out var variant))
yield break;
var sequence = resourceInfo.Variants.First().Value;
if (!Info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
yield break;
var sequence = variant.First().Value;
var sprite = sequence.GetSprite(sequence.Length - 1);
var shadow = sequence.GetShadow(sequence.Length - 1, WAngle.Zero);
var palette = resourceInfo.Palette;
var palette = wr.Palette(resourceInfo.Palette);
if (shadow != null)
yield return new UISpriteRenderable(shadow, WPos.Zero, origin, 0, palette, scale);
@@ -229,14 +264,17 @@ namespace OpenRA.Mods.Common.Traits
IEnumerable<IRenderable> IResourceRenderer.RenderPreview(WorldRenderer wr, string resourceType, WPos origin)
{
if (!ResourceInfo.TryGetValue(resourceType, out var resourceInfo))
if (!Variants.TryGetValue(resourceType, out var variant))
yield break;
var sequence = resourceInfo.Variants.First().Value;
if (!Info.ResourceTypes.TryGetValue(resourceType, out var resourceInfo))
yield break;
var sequence = variant.First().Value;
var sprite = sequence.GetSprite(sequence.Length - 1);
var shadow = sequence.GetShadow(sequence.Length - 1, WAngle.Zero);
var alpha = sequence.GetAlpha(sequence.Length - 1);
var palette = resourceInfo.Palette;
var palette = wr.Palette(resourceInfo.Palette);
var tintModifiers = sequence.IgnoreWorldTint ? TintModifiers.IgnoreWorldTint : TintModifiers.None;
if (shadow != null)
@@ -247,17 +285,30 @@ namespace OpenRA.Mods.Common.Traits
public readonly struct RendererCellContents
{
public readonly string Variant;
public readonly ResourceType Type;
public readonly string Type;
public readonly ResourceRendererInfo.ResourceTypeInfo Info;
public readonly ISpriteSequence Sequence;
public readonly PaletteReference Palette;
public readonly int Density;
public static readonly RendererCellContents Empty = default;
public RendererCellContents(string variant, ResourceType type, int density)
public RendererCellContents(string resourceType, int density, ResourceRendererInfo.ResourceTypeInfo info, ISpriteSequence sequence, PaletteReference palette)
{
Variant = variant;
Type = type;
Type = resourceType;
Density = density;
Info = info;
Sequence = sequence;
Palette = palette;
}
public RendererCellContents(RendererCellContents contents, int density)
{
Type = contents.Type;
Density = density;
Info = contents.Info;
Sequence = contents.Sequence;
Palette = contents.Palette;
}
}
}

View File

@@ -1,107 +0,0 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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, either version 3 of
* the License, or (at your option) any later version. For more
* information, see COPYING.
*/
#endregion
using System.Collections.Generic;
using OpenRA.Graphics;
using OpenRA.Primitives;
using OpenRA.Traits;
namespace OpenRA.Mods.Common.Traits
{
public class ResourceTypeInfo : TraitInfo, IMapPreviewSignatureInfo
{
[Desc("Sequence image that holds the different variants.")]
public readonly string Image = "resources";
[FieldLoader.Require]
[SequenceReference(nameof(Image))]
[Desc("Randomly chosen image sequences.")]
public readonly string[] Sequences = { };
[PaletteReference]
[Desc("Palette used for rendering the resource sprites.")]
public readonly string Palette = TileSet.TerrainPaletteInternalName;
[Desc("Resource index used in the binary map data.")]
public readonly int ResourceType = 1;
[Desc("Credit value of a single resource unit.")]
public readonly int ValuePerUnit = 0;
[Desc("Maximum number of resource units allowed in a single cell.")]
public readonly int MaxDensity = 10;
[FieldLoader.Require]
[Desc("Resource identifier used by other traits.")]
public readonly string Type = null;
[FieldLoader.Require]
[Desc("Resource name used by tooltips.")]
public readonly string Name = null;
[FieldLoader.Require]
[Desc("Terrain type used to determine unit movement and minimap colors.")]
public readonly string TerrainType = null;
[Desc("Terrain types that this resource can spawn on.")]
public readonly HashSet<string> AllowedTerrainTypes = new HashSet<string>();
[Desc("Allow resource to spawn under Mobile actors.")]
public readonly bool AllowUnderActors = false;
[Desc("Allow resource to spawn under Buildings.")]
public readonly bool AllowUnderBuildings = false;
[Desc("Allow resource to spawn on ramp tiles.")]
public readonly bool AllowOnRamps = false;
void IMapPreviewSignatureInfo.PopulateMapPreviewSignatureCells(Map map, ActorInfo ai, ActorReference s, List<(MPos, Color)> destinationBuffer)
{
var terrainInfo = map.Rules.TerrainInfo;
var color = terrainInfo.TerrainTypes[terrainInfo.GetTerrainIndex(TerrainType)].Color;
for (var i = 0; i < map.MapSize.X; i++)
{
for (var j = 0; j < map.MapSize.Y; j++)
{
var cell = new MPos(i, j);
if (map.Resources[cell].Type == ResourceType)
destinationBuffer.Add((cell, color));
}
}
}
public override object Create(ActorInitializer init) { return new ResourceType(this, init.World); }
}
public class ResourceType : IWorldLoaded
{
public readonly ResourceTypeInfo Info;
public PaletteReference Palette { get; private set; }
public readonly Dictionary<string, ISpriteSequence> Variants;
public ResourceType(ResourceTypeInfo info, World world)
{
Info = info;
Variants = new Dictionary<string, ISpriteSequence>();
foreach (var v in info.Sequences)
{
var seq = world.Map.Rules.Sequences.GetSequence(Info.Image, v);
Variants.Add(v, seq);
}
}
public void WorldLoaded(World w, WorldRenderer wr)
{
Palette = wr.Palette(Info.Palette);
}
}
}

View File

@@ -0,0 +1,126 @@
#region Copyright & License Information
/*
* Copyright 2007-2020 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, 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;
namespace OpenRA.Mods.Common.UpdateRules.Rules
{
public class RemoveResourceType : UpdateRule
{
public override string Name => "Remove ResourceType definitions.";
public override string Description =>
"The ResourceType trait has been removed, and resource definitions moved to the\n" +
"ResourceLayer, EditorResourceLayer, ResourceRenderer, and PlayerResources traits.";
MiniYaml resourceLayer;
MiniYaml resourceRenderer;
MiniYaml values;
public override IEnumerable<string> BeforeUpdate(ModData modData)
{
resourceLayer = new MiniYaml("");
resourceRenderer = new MiniYaml("");
values = new MiniYaml("");
yield break;
}
public override IEnumerable<string> AfterUpdate(ModData modData)
{
if (resourceLayer.Nodes.Any())
yield return "Add the following definitions to your ResourceLayer and EditorResourceLayer definitions:\n\t" +
"RecalculateResourceDensity: true\n\t" +
resourceLayer.ToLines("ResourceTypes").JoinWith("\n\t");
if (resourceLayer.Nodes.Any())
yield return "Add the following definitions to your ResourceRenderer definition:\n\t" +
resourceRenderer.ToLines("ResourceTypes").JoinWith("\n\t");
if (values.Nodes.Any())
yield return "Add the following definition to your ^BasePlayer definition:\n\t" +
"PlayerResources:\n\t\t" +
values.ToLines("ResourceValues").JoinWith("\n\t\t");
if (resourceLayer.Nodes.Any())
yield return "Support for AllowUnderActors, AllowUnderBuildings, and AllowOnRamps have been removed.\n" +
"You must define a custom ResourceLayer subclass if you want to customize the default behaviour.";
}
public override IEnumerable<string> UpdateActorNode(ModData modData, MiniYamlNode actorNode)
{
foreach (var resourceNode in actorNode.ChildrenMatching("ResourceType"))
{
var typeNode = resourceNode.LastChildMatching("Type");
if (typeNode != null)
{
var resourceLayerNode = new MiniYamlNode(typeNode.Value.Value, "");
resourceLayer.Nodes.Add(resourceLayerNode);
var resourceRendererNode = new MiniYamlNode(typeNode.Value.Value, "");
resourceRenderer.Nodes.Add(resourceRendererNode);
var indexNode = resourceNode.LastChildMatching("ResourceType");
if (indexNode != null)
{
indexNode.RenameKey("ResourceIndex");
resourceLayerNode.AddNode(indexNode);
}
var terrainTypeNode = resourceNode.LastChildMatching("TerrainType");
if (terrainTypeNode != null)
resourceLayerNode.AddNode(terrainTypeNode);
var allowedTerrainNode = resourceNode.LastChildMatching("AllowedTerrainTypes");
if (allowedTerrainNode != null)
resourceLayerNode.AddNode(allowedTerrainNode);
var maxDensityNode = resourceNode.LastChildMatching("MaxDensity");
if (maxDensityNode != null)
resourceLayerNode.AddNode(maxDensityNode);
var valueNode = resourceNode.LastChildMatching("ValuePerUnit");
if (valueNode != null)
values.Nodes.Add(new MiniYamlNode(typeNode.Value.Value, valueNode.Value.Value));
var imageNode = resourceNode.LastChildMatching("Image");
if (imageNode != null)
resourceRendererNode.AddNode(imageNode);
var sequencesNode = resourceNode.LastChildMatching("Sequences");
if (sequencesNode != null)
resourceRendererNode.AddNode(sequencesNode);
var paletteNode = resourceNode.LastChildMatching("Palette");
if (paletteNode != null)
resourceRendererNode.AddNode(paletteNode);
var nameNode = resourceNode.LastChildMatching("Name");
if (nameNode != null)
resourceRendererNode.AddNode(nameNode);
}
else
yield return "Unable to process definition:\n" +
resourceNode.Value.ToLines(resourceNode.Key).JoinWith("\n") + "\n\n" +
"This override has been removed and must be manually reimplemented if still needed.";
}
actorNode.RemoveNodes("ResourceType");
foreach (var resourceRendererNode in actorNode.ChildrenMatching("ResourceRenderer"))
resourceRendererNode.RemoveNodes("RenderTypes");
foreach (var resourceRendererNode in actorNode.ChildrenMatching("D2kResourceRenderer"))
resourceRendererNode.RemoveNodes("RenderTypes");
}
}
}

View File

@@ -91,6 +91,7 @@ namespace OpenRA.Mods.Common.UpdateRules
new RemovePlaceBuildingPalette(),
new ReplaceShadowPalette(),
new ReplaceResourceValueModifiers(),
new RemoveResourceType(),
})
};