NHSE/NHSE.Core/Structures/Map/Layers/LayerItem.cs
2026-01-27 19:31:39 -06:00

414 lines
17 KiB
C#

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
namespace NHSE.Core;
public abstract record LayerItem : AcreSelectionGrid
{
/// <summary>
/// All items in this layer, stored in column-major order.
/// </summary>
public Item[] Tiles { get; }
#pragma warning disable CA1857
protected LayerItem(Item[] tiles, [ConstantExpected] byte w, [ConstantExpected] byte h) : this(tiles, new(w, h, 1, 1))
#pragma warning restore CA1857
{
}
protected LayerItem(Item[] tiles, TileGridViewport tileTileInfo) : base(tileTileInfo)
{
Tiles = tiles;
Debug.Assert(TileInfo.TotalWidth * TileInfo.TotalHeight == tiles.Length);
}
/// <summary>
/// Retrieves the tile at the specified relative coordinates, or a default value if the coordinates are outside the
/// valid layer bounds.
/// </summary>
/// <param name="relX">The requested tile's X-coordinate, relative to the layer origin.</param>
/// <param name="relY">The requested tile's Y-coordinate, relative to the layer origin.</param>
/// <returns>The tile at the specified coordinates if they are within the layer; otherwise, a value indicating no item.</returns>
public Item GetTileSafe(in int relX, in int relY)
{
if (!Contains(relX, relY))
return Item.NO_ITEM;
return GetTile(relX, relY);
}
/// <summary>
/// Sets the tile at the specified relative coordinates if they are within the valid layer bounds.
/// </summary>
/// <param name="relX">The requested tile's X-coordinate, relative to the layer origin.</param>
/// <param name="relY">The requested tile's Y-coordinate, relative to the layer origin.</param>
/// <param name="tile">The tile to set at the specified coordinates.</param>
/// <returns>Returns true if the tile was set, false if the coordinates were out of bounds.</returns>
public bool SetTileSafe(in int relX, in int relY, Item tile)
{
if (!Contains(relX, relY))
return false;
SetTile(relX, relY, tile);
return true;
}
/// <summary>
/// Dumps the contents of an acre at the specified relative coordinates into a byte array.
/// </summary>
/// <param name="relX">The requested tile's X-coordinate, relative to the layer origin.</param>
/// <param name="relY">The requested tile's Y-coordinate, relative to the layer origin.</param>
/// <returns>A byte array containing the serialized data for the specified acre. The array length is determined by the number
/// of tiles in the view and the size of each item.</returns>
public byte[] DumpAcre(int relX, int relY)
{
int count = TileInfo.ViewCount;
var result = new byte[Item.SIZE * count];
DumpAcre(result, relX, relY);
return result;
}
/// <summary>
/// Writes the serialized data for an acre of tiles into the specified buffer, starting at the given relative X and Y coordinates.
/// </summary>
/// <remarks>
/// If a tile at the specified coordinates is out of range, a default item is written for that position.
/// The data is written in column-major order, with columns stored before rows.
/// </remarks>
/// <param name="result">The buffer that receives the serialized bytes for the acre. Must be large enough to hold all tile data.</param>
/// <param name="relX">The top-left tile's X-coordinate, relative to the layer origin.</param>
/// <param name="relY">The top-left tile's Y-coordinate, relative to the layer origin.</param>
public void DumpAcre(Span<byte> result, int relX, int relY)
{
// Store columns first. If the x,y is out of range, store default item.
int offset = 0;
for (int y = 0; y < TileInfo.ViewHeight; y++)
{
var tileY = relY + y;
for (int x = 0; x < TileInfo.ViewWidth; x++)
{
var tileX = relX + x;
var tile = GetTileSafe(tileX, tileY);
tile.ToBytesClass().CopyTo(result[offset..]);
offset += Item.SIZE;
}
}
}
/// <summary>
/// Imports a block of tile data into the map at the specified relative coordinates.
/// </summary>
/// <remarks>The method attempts to set each tile in the specified region. Only tiles that are valid and
/// within the map bounds are imported; others are ignored. The return value indicates how many tiles were actually
/// updated.</remarks>
/// <param name="data">A read-only span of bytes containing the tile data to import. The data must be formatted as expected by the tile
/// array parser.</param>
/// <param name="relX">The top-left tile's X-coordinate, relative to the layer origin.</param>
/// <param name="relY">The top-left tile's Y-coordinate, relative to the layer origin.</param>
/// <returns>The number of tiles that were successfully imported and set in the map.</returns>
public int ImportAcre(ReadOnlySpan<byte> data, int relX, int relY)
{
int count = 0;
int i = 0;
var tiles = Item.GetArray(data);
for (int y = 0; y < TileInfo.ViewHeight; y++)
{
var tileY = relY + y;
for (int x = 0; x < TileInfo.ViewWidth; x++)
{
var tileX = relX + x;
if (SetTileSafe(tileX, tileY, tiles[i++]))
count++;
}
}
return count;
}
/// <summary>
/// Retrieves the tile at the specified relative coordinates.
/// </summary>
/// <param name="relX">The X-coordinate of the tile, relative to the layer origin.</param>
/// <param name="relY">The Y-coordinate of the tile, relative to the layer origin.</param>
/// <returns>The tile at the specified coordinates.</returns>
public Item GetTile(in int relX, in int relY) => this[TileInfo.GetTileIndex(relX, relY)];
/// <summary>
/// Sets the tile at the specified relative X and Y coordinates to the given tile value.
/// </summary>
/// <param name="relX">The X-coordinate of the tile, relative to the layer origin.</param>
/// <param name="relY">The Y-coordinate of the tile, relative to the layer origin.</param>
/// <param name="tile">The tile value to assign at the specified coordinates.</param>
public void SetTile(in int relX, in int relY, Item tile) => this[TileInfo.GetTileIndex(relX, relY)] = tile;
public Item this[int index]
{
get => Tiles[index];
set => Tiles[index] = value;
}
/// <summary>
/// Serializes all tiles into a single byte array.
/// </summary>
/// <returns>
/// A byte array containing the serialized data of all tiles, concatenated in order.
/// The length of the array is equal to the number of tiles multiplied by the size of each item.
/// </returns>
public byte[] DumpAll()
{
var result = new byte[Tiles.Length * Item.SIZE];
DumpAll(result);
return result;
}
/// <summary>
/// Writes the byte representation of all tiles to the specified buffer.
/// </summary>
/// <param name="result">A buffer that receives the serialized bytes of all tiles.</param>
public void DumpAll(Span<byte> result)
{
for (int i = 0; i < Tiles.Length; i++)
Tiles[i].ToBytesClass().CopyTo(result[(i * Item.SIZE)..]);
}
/// <summary>
/// Imports all tiles from the provided byte data into the layer.
/// </summary>
/// <param name="data">A read-only span of bytes containing the tile data to import.</param>
public void ImportAll(ReadOnlySpan<byte> data)
{
var tiles = Item.GetArray(data);
for (int i = 0; i < tiles.Length; i++)
Tiles[i].CopyFrom(tiles[i]);
}
/// <summary>
/// Removes all items within the specified rectangular region that match the given criteria.
/// </summary>
/// <remarks>
/// The method evaluates the criteria for each item in the specified region and removes only those items for which the criteria returns <see langword="true"/>.
/// Items outside the specified region are not affected.
/// </remarks>
/// <param name="xmin">The relative x-coordinate of the upper-left corner of the region to search.</param>
/// <param name="ymin">The relative y-coordinate of the upper-left corner of the region to search.</param>
/// <param name="width">The width, in tiles, of the region to search. Must be greater than or equal to 0.</param>
/// <param name="height">The height, in tiles, of the region to search. Must be greater than or equal to 0.</param>
/// <param name="criteria">
/// A function that defines the condition each item must satisfy to be removed.
/// The function is invoked for each item in the region.
/// </param>
/// <returns>The number of items that were removed.</returns>
public int RemoveAll(in int xmin, in int ymin, in int width, in int height, Func<Item, bool> criteria)
{
int count = 0;
for (int x = xmin; x < xmin + width; x++)
{
for (int y = ymin; y < ymin + height; y++)
{
var t = GetTile(x, y);
if (!criteria(t))
continue;
t.Delete();
count++;
}
}
return count;
}
/// <summary>
/// Deletes all extension tiles associated with the specified tile at the given coordinates, except for the main (root) tile itself.
/// </summary>
/// <remarks>
/// This method removes only the extension tiles that are part of the same logical group as the specified main tile,
/// leaving the main tile itself intact.
/// Use this method to clean up extension tiles when the main tile is being modified or removed.
/// </remarks>
/// <param name="tile">The main tile whose extension tiles are to be deleted.</param>
/// <param name="x">The relative x-coordinate of the main (root) tile.</param>
/// <param name="y">The relative y-coordinate of the main (root) tile.</param>
public void DeleteExtensionTiles(Item tile, int x, int y)
{
GetTileWidthHeight(tile, x, y, out var w, out var h);
for (int ix = 0; ix < w; ix++)
{
for (int iy = 0; iy < h; iy++)
{
if (iy == 0 && ix == 0)
continue;
var t = GetTile(x + ix, y + iy);
t.Delete();
}
}
}
/// <summary>
/// Marks all tiles covered by the specified multi-tile item, except the origin tile,
/// as extension tiles associated with the given item at the specified coordinates.
/// </summary>
/// <remarks>
/// This method is typically used when placing a multi-tile item to ensure that all tiles it occupies,
/// except for the origin, are correctly marked as extensions.
/// The origin tile at the specified coordinates is not modified by this method.</remarks>
/// <param name="tile">The multi-tile item whose extension tiles are to be set.</param>
/// <param name="x">The relative x-coordinate of the main (root) tile.</param>
/// <param name="y">The relative y-coordinate of the main (root) tile.</param>
public void SetExtensionTiles(Item tile, in int x, in int y)
{
GetTileWidthHeight(tile, x, y, out var w, out var h);
for (byte ix = 0; ix < w; ix++)
{
for (byte iy = 0; iy < h; iy++)
{
if (iy == 0 && ix == 0)
continue;
var t = GetTile(x + ix, y + iy);
t.SetAsExtension(tile, ix, iy);
}
}
}
private void GetTileWidthHeight(Item tile, int x, int y, out int w, out int h)
{
var type = ItemInfo.GetItemSize(tile);
w = type.Width;
h = type.Height;
// Rotation
if ((tile.Rotation & 1) == 1)
(w, h) = (h, w);
// Clamp to grid bounds
if (x + w - 1 >= TileInfo.TotalWidth)
w = TileInfo.TotalWidth - x;
if (y + h - 1 >= TileInfo.TotalHeight)
h = TileInfo.TotalHeight - y;
}
/// <summary>
/// Checks if writing the <see cref="tile"/> at the specified <see cref="x"/> and <see cref="y"/> coordinates will overlap with any existing tiles.
/// </summary>
/// <returns>True if any tile will be overwritten, false if nothing is there.</returns>
public PlacedItemPermission IsOccupied(Item tile, in int x, in int y)
{
var type = ItemInfo.GetItemSize(tile);
var w = type.Width;
var h = type.Height;
if ((tile.Rotation & 1) == 1)
(w, h) = (h, w);
if (x + w - 1 >= TileInfo.TotalWidth)
return PlacedItemPermission.OutOfBounds;
if (y + h - 1 >= TileInfo.TotalHeight)
return PlacedItemPermission.OutOfBounds;
for (byte ix = 0; ix < w; ix++)
{
for (byte iy = 0; iy < h; iy++)
{
var t = GetTile(x + ix, y + iy);
if (!t.IsNone)
return PlacedItemPermission.Collision;
}
}
return PlacedItemPermission.NoCollision;
}
/// <summary>
/// Replaces all occurrences of a specified item with a new item within the defined rectangular region.
/// </summary>
/// <remarks>
/// Only root tiles that exactly match <paramref name="oldItem"/> are replaced.
/// No replacements are made if the item sizes differ.
/// </remarks>
/// <param name="oldItem">The item to search for and replace within the specified region.</param>
/// <param name="newItem">The item to use as a replacement for each occurrence of <paramref name="oldItem"/>.</param>
/// <param name="xmin">The relative x-coordinate of the upper-left corner of the region in which to perform replacements.</param>
/// <param name="ymin">The relative y-coordinate of the upper-left corner of the region in which to perform replacements.</param>
/// <param name="width">The width, in tiles, of the region in which to perform replacements. Must be greater than zero.</param>
/// <param name="height">The height, in tiles, of the region in which to perform replacements. Must be greater than zero.</param>
/// <returns>
/// The number of items replaced within the specified region,
/// or -1 if <paramref name="oldItem"/> and <paramref name="newItem"/> are incompatible.
/// </returns>
public int ReplaceAll(Item oldItem, Item newItem, in int xmin, in int ymin, in int width, in int height)
{
var sizeOld = ItemInfo.GetItemSize(oldItem);
var sizeNew = ItemInfo.GetItemSize(newItem);
if (sizeOld != sizeNew)
return -1;
int count = 0;
for (int x = xmin; x < xmin + width; x++)
{
for (int y = ymin; y < ymin + height; y++)
{
var t = GetTile(x, y);
if (!t.IsRoot)
continue;
if (!t.Equals(oldItem))
continue;
DeleteExtensionTiles(t, x, y);
t.CopyFrom(newItem);
SetExtensionTiles(t, x, y);
count++;
}
}
return count;
}
/// <summary>
/// Removes all invalid or dangling extensions within the specified rectangular region.
/// </summary>
/// <remarks>
/// Only extension tiles that are determined to be invalid or dangling are removed.
/// Valid extension tiles within the region are not changed.
/// </remarks>
/// <param name="xmin">The relative x-coordinate of the upper-left corner of the region in which to perform replacements.</param>
/// <param name="ymin">The relative y-coordinate of the upper-left corner of the region in which to perform replacements.</param>
/// <param name="width">The width of the region, in tiles. Must be greater than zero.</param>
/// <param name="height">The height of the region, in tiles. Must be greater than zero.</param>
/// <returns>The number of extensions that were removed from the specified region.</returns>
public int ClearDanglingExtensions(in int xmin, in int ymin, in int width, in int height)
{
int count = 0;
for (int x = xmin; x < xmin + width; x++)
{
for (int y = ymin; y < ymin + height; y++)
{
var t = GetTile(x, y);
if (IsValidExtension(t, x, y))
continue;
t.Delete();
count++;
}
}
return count;
}
private bool IsValidExtension(Item t, int x, int y)
{
if (!t.IsExtension)
return true;
var parentX = x - t.ExtensionX;
var parentY = y - t.ExtensionY;
try
{
var parent = GetTile(parentX, parentY);
if (parent.ItemId == t.ExtensionItemId)
return true;
}
catch
{
// corrupt?
}
return false;
}
}