mirror of
https://github.com/kwsch/NHSE.git
synced 2026-03-27 03:54:39 -05:00
414 lines
17 KiB
C#
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;
|
|
}
|
|
} |