mirror of
https://github.com/kwsch/NHSE.git
synced 2026-09-11 12:45:36 -05:00
Update FieldItemEditor for 3.0.0 (#716)
Updates the Field Item Editor to render layers based on the entire map, and the per-patch positioning of each layer. Import/export will gracefully handle upgrade/downgrade, and viewport import/export will gracefully update tiles rather than a per-acre basis. Performance has also been slightly improved; no allocation is done anymore when updating the image.
This commit is contained in:
@@ -16,6 +16,6 @@ public static int GetAcreTileColor(ushort acre, int x, int y)
|
||||
var shift = (4 * ((y * 64) + x));
|
||||
var ofs = baseOfs + shift;
|
||||
var tile = AcreTiles[ofs];
|
||||
return CollisionUtil.Dict[tile].ToArgb();
|
||||
return TileCollisionUtil.Dict[tile].ToArgb();
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class ItemMutator : BatchMutator<Item>
|
||||
public sealed class ItemMutator : BatchMutator<Item>
|
||||
{
|
||||
public readonly ItemReflection Reflect = ItemReflection.Default;
|
||||
private const char CONST_POINTER = '*';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class ItemProcessor(BatchMutator<Item> mut) : BatchProcessor<Item>(mut)
|
||||
public sealed class ItemProcessor(BatchMutator<Item> mut) : BatchProcessor<Item>(mut)
|
||||
{
|
||||
protected override bool CanModify(Item item) => true;
|
||||
protected override bool Finalize(Item item) => true;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class ItemReflection
|
||||
public sealed class ItemReflection
|
||||
{
|
||||
public static ItemReflection Default { get; } = new();
|
||||
|
||||
|
||||
@@ -1,23 +1,13 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed class FieldItemColumn
|
||||
{
|
||||
/// <summary> X Coordinate within the Field Item Layer </summary>
|
||||
public readonly int X;
|
||||
|
||||
/// <summary> Y Coordinate within the Field Item Layer </summary>
|
||||
public readonly int Y;
|
||||
|
||||
/// <summary> Offset relative to the start of the Field Item Layer </summary>
|
||||
public readonly int Offset;
|
||||
|
||||
public readonly byte[] Data;
|
||||
|
||||
public FieldItemColumn(int x, int y, int offset, byte[] data)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Offset = offset;
|
||||
Data = data;
|
||||
}
|
||||
}
|
||||
/// <summary>
|
||||
/// Represents a list of item data tiles to be injected to a Field Item Layer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Extension tiles underneath the actual item root are included; extension tiles to the right are not.
|
||||
/// </remarks>
|
||||
/// <param name="RelativeX">X Coordinate within the Field Item Layer</param>
|
||||
/// <param name="RelativeY">Y Coordinate within the Field Item Layer</param>
|
||||
/// <param name="Offset">Offset relative to the start of the Field Item Layer</param>
|
||||
/// <param name="Data">Data for this column</param>
|
||||
public sealed record FieldItemColumn(int RelativeX, int RelativeY, int Offset, byte[] Data);
|
||||
@@ -7,10 +7,12 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Converts <see cref="Item"/> into columns of writable Item tiles.
|
||||
/// </summary>
|
||||
public static class FieldItemDropper
|
||||
/// <param name="AcreWidth">Field item acre width. 7 on pre-3.0.0 saves, 9 on 3.0.0+</param>
|
||||
/// <param name="AcreHeight">Always 6.</param>
|
||||
public sealed record FieldItemDropper(int AcreWidth, int AcreHeight = 6)
|
||||
{
|
||||
private const int MapHeight = FieldItemLayer.FieldItemHeight;
|
||||
private const int MapWidth = FieldItemLayer.FieldItemWidth;
|
||||
private int MapHeight => AcreWidth * LayerFieldItem.TilesPerAcreDim;
|
||||
private int MapWidth => AcreHeight * LayerFieldItem.TilesPerAcreDim;
|
||||
|
||||
// Each dropped item is a 2x2 square, with the top left tile being the root node, and the other 3 being extensions pointing back to the root.
|
||||
|
||||
@@ -21,7 +23,7 @@ public static class FieldItemDropper
|
||||
/// <param name="yCount">Count of items tall the overall spawn-rectangle is.</param>
|
||||
/// <param name="borderX">Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.</param>
|
||||
/// <param name="borderY">Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.</param>
|
||||
public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int borderX, int borderY)
|
||||
public bool CanFitDropped(int x, int y, int totalCount, int yCount, int borderX, int borderY)
|
||||
{
|
||||
return CanFitDropped(x, y, totalCount, yCount, borderX, borderX, borderY, borderY);
|
||||
}
|
||||
@@ -39,7 +41,7 @@ public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int b
|
||||
/// <param name="topY">Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.</param>
|
||||
/// <param name="botY">Excluded outer tile count. Useful for enforcing that beach acre tiles are skipped.</param>
|
||||
/// <returns>True if can fit, false if not.</returns>
|
||||
public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int leftX, int rightX, int topY, int botY)
|
||||
public bool CanFitDropped(int x, int y, int totalCount, int yCount, int leftX, int rightX, int topY, int botY)
|
||||
{
|
||||
var xCount = totalCount / yCount;
|
||||
if (x < leftX || (x + (xCount * 2)) > MapWidth - rightX)
|
||||
@@ -50,13 +52,13 @@ public static bool CanFitDropped(int x, int y, int totalCount, int yCount, int l
|
||||
return totalCount < (MapHeight * MapWidth / 32);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FieldItemColumn> InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList<Item> item)
|
||||
public IReadOnlyList<FieldItemColumn> InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList<Item> item)
|
||||
{
|
||||
int yStride = (item.Count > 16) ? 16 : item.Count;
|
||||
return InjectItemsAsDropped(mapX, mapY, item, yStride);
|
||||
}
|
||||
|
||||
public static IReadOnlyList<FieldItemColumn> InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList<Item> item, int yStride)
|
||||
public IReadOnlyList<FieldItemColumn> InjectItemsAsDropped(int mapX, int mapY, IReadOnlyList<Item> item, int yStride)
|
||||
{
|
||||
var xStride = item.Count / yStride;
|
||||
List<FieldItemColumn> result = new(yStride * xStride);
|
||||
@@ -108,10 +110,7 @@ private static byte[] GetColumnExtension(ReadOnlySpan<Item> items)
|
||||
return Item.SetArray(col);
|
||||
}
|
||||
|
||||
private static int GetTileOffset(int x, int y)
|
||||
{
|
||||
return Item.SIZE * (y + (x * MapHeight));
|
||||
}
|
||||
private int GetTileOffset(int relX, int relY) => Item.SIZE * (relY + (relX * MapHeight));
|
||||
|
||||
private static Item GetDroppedItem(Item item)
|
||||
{
|
||||
|
||||
141
NHSE.Core/Editing/FieldItem/FieldItemFlagUpgrade.cs
Normal file
141
NHSE.Core/Editing/FieldItem/FieldItemFlagUpgrade.cs
Normal file
@@ -0,0 +1,141 @@
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to upgrade or downgrade field item flag data between different formats.
|
||||
/// </summary>
|
||||
public static class FieldItemFlagUpgrade
|
||||
{
|
||||
private const int ColumnCountOld = 7;
|
||||
private const int ColumnCountNew = 9; // +1 column on each side
|
||||
private const int RowCount = 6;
|
||||
|
||||
private const byte SizeDim = LayerFieldItem.TilesPerAcreDim;
|
||||
|
||||
// Tile dimensions across the entire map
|
||||
private const int TileRowCount = RowCount * SizeDim; // 192 tile rows
|
||||
private const int TilesPerRowOld = ColumnCountOld * SizeDim; // 224 tiles per row
|
||||
private const int TilesPerRowNew = ColumnCountNew * SizeDim; // 288 tiles per row
|
||||
|
||||
// Bytes per tile-row (1 bit per tile, 8 tiles per byte)
|
||||
private const int BytesPerRowOld = TilesPerRowOld / 8; // 28 bytes
|
||||
private const int BytesPerRowNew = TilesPerRowNew / 8; // 36 bytes
|
||||
|
||||
// Bytes for one acre column per row (32 tiles / 8 bits per byte = 4 bytes)
|
||||
private const int BytesPerAcreColumnPerRow = SizeDim / 8; // 4 bytes
|
||||
|
||||
// Total sizes
|
||||
private const int FlagSizeOld = TileRowCount * BytesPerRowOld; // 5,376 bytes
|
||||
private const int FlagSizeNew = TileRowCount * BytesPerRowNew; // 6,912 bytes
|
||||
|
||||
// Active flags are stored in row-major order.
|
||||
// Since the upgrade adds a column on each side, it's not possible to simply prepend and append default columns.
|
||||
// For each row of data, we need to add default flags at the start and end of the row.
|
||||
// Repeat for all rows.
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an update is needed based on the current size and expected size.
|
||||
/// </summary>
|
||||
/// <param name="current">Current size of the data.</param>
|
||||
/// <param name="expect">Desired size of the data.</param>
|
||||
/// <returns><see langword="true"/> if an update is needed; otherwise <see langword="false"/>.</returns>
|
||||
public static bool IsUpdateNeeded(long current, int expect) => expect switch
|
||||
{
|
||||
FlagSizeOld => current == FlagSizeNew,
|
||||
FlagSizeNew => current == FlagSizeOld,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Detects and performs an update on the field item flag data if needed.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to update.</param>
|
||||
/// <param name="expect">Desired size of the data.</param>
|
||||
/// <returns><see langword="true"/> if an update was performed; otherwise <see langword="false"/>.</returns>
|
||||
/// <remarks>Pre-check <see cref="IsUpdateNeeded(long, int)"/> to ensure a conversion is available.</remarks>
|
||||
public static bool DetectUpdate(ref byte[] data, int expect)
|
||||
{
|
||||
if (expect == FlagSizeNew && data.Length == FlagSizeOld)
|
||||
data = Inflate(data);
|
||||
else if (expect == FlagSizeOld && data.Length == FlagSizeNew)
|
||||
data = Deflate(data);
|
||||
else // No change needed/supported.
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Inflate(ReadOnlySpan{byte}, Span{byte})"/>
|
||||
public static byte[] Inflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length != FlagSizeOld)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected old size {FlagSizeOld}.");
|
||||
|
||||
var result = new byte[FlagSizeNew];
|
||||
Inflate(data, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inflates old field item flag data to the new format.
|
||||
/// </summary>
|
||||
/// <param name="data">Old field item flag data.</param>
|
||||
/// <param name="result">Span to write new field item flag data to.</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static void Inflate(ReadOnlySpan<byte> data, Span<byte> result)
|
||||
{
|
||||
if (data.Length != FlagSizeOld)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected old size {FlagSizeOld}.");
|
||||
if (result.Length < FlagSizeNew)
|
||||
throw new ArgumentException($"Result length {result.Length} is less than expected new size {FlagSizeNew}.");
|
||||
|
||||
// For each tile row:
|
||||
// - 4 bytes of zeros for new left column (result is already zeroed)
|
||||
// - Copy 28 bytes from old data (existing flags)
|
||||
// - 4 bytes of zeros for new right column (result is already zeroed)
|
||||
|
||||
for (int row = 0; row < TileRowCount; row++)
|
||||
{
|
||||
var srcOffset = row * BytesPerRowOld;
|
||||
var dstOffset = (row * BytesPerRowNew) + BytesPerAcreColumnPerRow;
|
||||
data.Slice(srcOffset, BytesPerRowOld).CopyTo(result[dstOffset..]);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Deflate(ReadOnlySpan{byte}, Span{byte})"/>
|
||||
public static byte[] Deflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length != FlagSizeNew)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected new size {FlagSizeNew}.");
|
||||
|
||||
var result = new byte[FlagSizeOld];
|
||||
Deflate(data, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deflates new field item flag data to the old format.
|
||||
/// </summary>
|
||||
/// <param name="data">New field item flag data.</param>
|
||||
/// <param name="result">Span to write old field item flag data to.</param>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static void Deflate(ReadOnlySpan<byte> data, Span<byte> result)
|
||||
{
|
||||
if (data.Length != FlagSizeNew)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected new size {FlagSizeNew}.");
|
||||
if (result.Length < FlagSizeOld)
|
||||
throw new ArgumentException($"Result length {result.Length} is less than expected old size {FlagSizeOld}.");
|
||||
|
||||
// For each tile row:
|
||||
// - Skip 4 bytes for new left column
|
||||
// - Copy 28 bytes to result (existing flags)
|
||||
// - Skip 4 bytes for new right column
|
||||
|
||||
for (int row = 0; row < TileRowCount; row++)
|
||||
{
|
||||
var srcOffset = (row * BytesPerRowNew) + BytesPerAcreColumnPerRow;
|
||||
var dstOffset = row * BytesPerRowOld;
|
||||
data.Slice(srcOffset, BytesPerRowOld).CopyTo(result[dstOffset..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
NHSE.Core/Editing/FieldItem/FieldItemUpgrade.cs
Normal file
128
NHSE.Core/Editing/FieldItem/FieldItemUpgrade.cs
Normal file
@@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Provides functionality to upgrade or downgrade field item data between different formats.
|
||||
/// </summary>
|
||||
public static class FieldItemUpgrade
|
||||
{
|
||||
private const int ColumnCountOld = 7;
|
||||
private const int ColumnCountNew = 9; // +1 column on each side
|
||||
private const int RowCount = 6;
|
||||
|
||||
private const byte SizeDim = LayerFieldItem.TilesPerAcreDim;
|
||||
private const int TilesPerAcre = SizeDim * SizeDim;
|
||||
private const int FieldItemSizeSingleColumn = RowCount * TilesPerAcre * Item.SIZE;
|
||||
private const int FieldItemSizeOld = ColumnCountOld * FieldItemSizeSingleColumn;
|
||||
private const int FieldItemSizeNew = ColumnCountNew * FieldItemSizeSingleColumn;
|
||||
|
||||
// Items are stored in column-major order.
|
||||
// Since the upgrade adds a column on each side, it's easy to prepend and append default columns.
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an update is needed based on the current size and expected size.
|
||||
/// </summary>
|
||||
/// <param name="current">Current size of the data.</param>
|
||||
/// <param name="expect">Desired size of the data.</param>
|
||||
/// <returns><see langword="true"/> if an update is needed; otherwise <see langword="false"/>.</returns>
|
||||
public static bool IsUpdateNeeded(long current, int expect) => expect switch
|
||||
{
|
||||
FieldItemSizeOld => current == FieldItemSizeNew,
|
||||
FieldItemSizeNew => current == FieldItemSizeOld,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Detects and performs an update on the field item data if needed.
|
||||
/// </summary>
|
||||
/// <param name="data">Data to update.</param>
|
||||
/// <param name="expect">Desired size of the data.</param>
|
||||
/// <returns><see langword="true"/> if an update was performed; otherwise <see langword="false"/>.</returns>
|
||||
/// <remarks>Pre-check <see cref="IsUpdateNeeded(long, int)"/> to ensure a conversion is available.</remarks>
|
||||
public static bool DetectUpdate(ref byte[] data, int expect)
|
||||
{
|
||||
if (expect == FieldItemSizeNew && data.Length == FieldItemSizeOld)
|
||||
data = Inflate(data);
|
||||
else if (expect == FieldItemSizeOld && data.Length == FieldItemSizeNew)
|
||||
data = Deflate(data);
|
||||
else // No change needed/supported.
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Inflate(ReadOnlySpan{byte}, Span{byte})"/>
|
||||
public static byte[] Inflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length != FieldItemSizeOld)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected old size {FieldItemSizeOld}.");
|
||||
|
||||
var result = new byte[FieldItemSizeNew];
|
||||
Inflate(data, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inflates old field item data to the new format.
|
||||
/// </summary>
|
||||
/// <param name="data">Old field item data.</param>
|
||||
/// <param name="result">Span to write new field item data to.</param>
|
||||
/// <returns>New field item data.</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static void Inflate(ReadOnlySpan<byte> data, Span<byte> result)
|
||||
{
|
||||
if (data.Length != FieldItemSizeOld)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected old size {FieldItemSizeOld}.");
|
||||
if (result.Length < FieldItemSizeNew)
|
||||
throw new ArgumentException($"Result length {result.Length} is less than expected new size {FieldItemSizeNew}.");
|
||||
|
||||
// The first acre column is default field items.
|
||||
// Then, the existing data is present.
|
||||
// Finally, an acre column is default field items.
|
||||
|
||||
// Prepare a default column of no items.
|
||||
Span<byte> defaultColumn = stackalloc byte[FieldItemSizeSingleColumn];
|
||||
FillColumnWithItem(defaultColumn, Item.NONE);
|
||||
|
||||
// First default column
|
||||
defaultColumn.CopyTo(result);
|
||||
// Existing data
|
||||
data.CopyTo(result[FieldItemSizeSingleColumn..]);
|
||||
// Last default column
|
||||
defaultColumn.CopyTo(result[^FieldItemSizeSingleColumn..]);
|
||||
}
|
||||
|
||||
private static void FillColumnWithItem(Span<byte> defaultColumn, ulong tileValue)
|
||||
{
|
||||
if (!BitConverter.IsLittleEndian)
|
||||
tileValue = System.Buffers.Binary.BinaryPrimitives.ReverseEndianness(tileValue);
|
||||
var cast = MemoryMarshal.Cast<byte, ulong>(defaultColumn);
|
||||
foreach (ref var value in cast)
|
||||
value = tileValue;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="Deflate(ReadOnlySpan{byte}, Span{byte})"/>
|
||||
public static byte[] Deflate(ReadOnlySpan<byte> data)
|
||||
{
|
||||
if (data.Length != FieldItemSizeNew)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected new size {FieldItemSizeNew}.");
|
||||
return data.Slice(FieldItemSizeSingleColumn, FieldItemSizeOld).ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deflates new field item data to the old format.
|
||||
/// </summary>
|
||||
/// <param name="data">New field item data.</param>
|
||||
/// <param name="result">Span to write old field item data to.</param>
|
||||
/// <returns>Old field item data.</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public static void Deflate(ReadOnlySpan<byte> data, Span<byte> result)
|
||||
{
|
||||
if (data.Length != FieldItemSizeNew)
|
||||
throw new ArgumentException($"Data length {data.Length} does not match expected new size {FieldItemSizeNew}.");
|
||||
if (result.Length < FieldItemSizeOld)
|
||||
throw new ArgumentException($"Result length {result.Length} is less than expected old size {FieldItemSizeOld}.");
|
||||
data.Slice(FieldItemSizeSingleColumn, FieldItemSizeOld).CopyTo(result);
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ private static Item CreateItem(byte[] convert, int requestIndex, IConfigItem con
|
||||
try
|
||||
{
|
||||
if (convert.Length != Item.SIZE)
|
||||
throw new Exception();
|
||||
throw new Exception($"Invalid item byte length (expected {Item.SIZE}, got {convert.Length}).");
|
||||
item = convert.ToClass<Item>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -165,7 +165,8 @@ public Museum Museum
|
||||
set => value.Data.CopyTo(Data[Offsets.Museum..]);
|
||||
}
|
||||
|
||||
public const int AcreWidth = 7 + (2 * 1); // 1 on each side cannot be traversed
|
||||
// Acre Layout/Selection of which baselayer is selected for an acre.
|
||||
private const int AcreWidth = 7 + (2 * 1); // 1 on each side cannot be traversed
|
||||
private const int AcreHeight = 6 + (2 * 1); // 1 on each side cannot be traversed
|
||||
private const int AcreMax = AcreWidth * AcreHeight;
|
||||
private const int AcreSizeAll = AcreMax * 2;
|
||||
@@ -193,29 +194,50 @@ public void SetAcreBytes(ReadOnlySpan<byte> data)
|
||||
data.CopyTo(Data[Offsets.OutsideField..]);
|
||||
}
|
||||
|
||||
public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, MapGrid.MapTileCount16x16 * TerrainTile.SIZE));
|
||||
|
||||
#pragma warning disable CA1822 // Mark members as static
|
||||
public byte FieldItemAcreWidth => Offsets.FieldItemAcreWidth; // 3.0.0 updated from 7 => 9
|
||||
// ReSharper disable once MemberCanBeMadeStatic.Global
|
||||
public byte FieldItemAcreHeight => 6; // always 6
|
||||
private int FieldItemAcreCount => FieldItemAcreWidth * FieldItemAcreHeight;
|
||||
#pragma warning restore CA1822 // Mark members as static
|
||||
|
||||
|
||||
private const int TotalTerrainTileCount = LayerTerrain.TilesPerAcreDim * LayerTerrain.TilesPerAcreDim * (7 * 6);
|
||||
private int TotalFieldItemTileCount => LayerFieldItem.TilesPerAcreDim * LayerFieldItem.TilesPerAcreDim * FieldItemAcreCount;
|
||||
|
||||
public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, TotalTerrainTileCount * TerrainTile.SIZE));
|
||||
public void SetTerrainTiles(IReadOnlyList<TerrainTile> array) => TerrainTile.SetArray(array).CopyTo(Data[Offsets.LandMakingMap..]);
|
||||
|
||||
public const int MapDesignNone = 0xF800;
|
||||
public const ushort MapDesignNone = 0xF800;
|
||||
|
||||
public Memory<byte> MapDesignTileData => Raw.Slice(Offsets.MyDesignMap, 112 * 96 * sizeof(ushort));
|
||||
public ushort[] GetMapDesignTiles() => MemoryMarshal.Cast<byte, ushort>(MapDesignTileData.Span).ToArray();
|
||||
public void SetMapDesignTiles(ReadOnlySpan<ushort> value) => MemoryMarshal.Cast<ushort, byte>(value).CopyTo(MapDesignTileData.Span);
|
||||
|
||||
private const int FieldItemLayerSize = MapGrid.MapTileCount32x32 * Item.SIZE;
|
||||
private const int FieldItemFlagSize = MapGrid.MapTileCount32x32 / 8; // bitflags
|
||||
public void ClearDesignTiles()
|
||||
{
|
||||
var tiles = GetMapDesignTiles();
|
||||
tiles.AsSpan().Fill(MapDesignNone);
|
||||
SetMapDesignTiles(tiles);
|
||||
}
|
||||
|
||||
private int FieldItemLayer1 => Offsets.FieldItem;
|
||||
private int FieldItemLayer2 => Offsets.FieldItem + FieldItemLayerSize;
|
||||
public int FieldItemFlag1 => Offsets.FieldItem + (FieldItemLayerSize * 2);
|
||||
public int FieldItemFlag2 => Offsets.FieldItem + (FieldItemLayerSize * 2) + FieldItemFlagSize;
|
||||
private int FieldItemLayerSize => TotalFieldItemTileCount * Item.SIZE;
|
||||
private int FieldItemFlagSize => TotalFieldItemTileCount / sizeof(byte); // bitflags
|
||||
|
||||
private int FieldItemLayer0 => Offsets.FieldItem;
|
||||
private int FieldItemLayer1 => Offsets.FieldItem + FieldItemLayerSize;
|
||||
public int FieldItemFlag0 => Offsets.FieldItem + (FieldItemLayerSize * 2);
|
||||
public int FieldItemFlag1 => Offsets.FieldItem + (FieldItemLayerSize * 2) + FieldItemFlagSize;
|
||||
public Memory<byte> FieldItemFlag0Data => Data.Slice(FieldItemFlag0, FieldItemFlagSize).ToArray();
|
||||
public Memory<byte> FieldItemFlag1Data => Data.Slice(FieldItemFlag1, FieldItemFlagSize).ToArray();
|
||||
|
||||
public Item[] GetFieldItemLayer0() => Item.GetArray(Data.Slice(FieldItemLayer0, FieldItemLayerSize));
|
||||
public void SetFieldItemLayer0(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer0..]);
|
||||
|
||||
public Item[] GetFieldItemLayer1() => Item.GetArray(Data.Slice(FieldItemLayer1, FieldItemLayerSize));
|
||||
public void SetFieldItemLayer1(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer1..]);
|
||||
|
||||
public Item[] GetFieldItemLayer2() => Item.GetArray(Data.Slice(FieldItemLayer2, FieldItemLayerSize));
|
||||
public void SetFieldItemLayer2(IReadOnlyList<Item> array) => Item.SetArray(array).CopyTo(Data[FieldItemLayer2..]);
|
||||
|
||||
public ushort OutsideFieldTemplateUniqueId
|
||||
{
|
||||
get => ReadUInt16LittleEndian(Data[(Offsets.OutsideField + AcreSizeAll)..]);
|
||||
|
||||
@@ -46,12 +46,12 @@ protected EncryptedFilePair(ISaveFileProvider provider, string name)
|
||||
RawHeader = hd;
|
||||
RawData = md;
|
||||
|
||||
Info = RawHeader[..FileHeaderInfo.SIZE].ToClass<FileHeaderInfo>();
|
||||
Info = Header[..FileHeaderInfo.SIZE].ToArray().ToClass<FileHeaderInfo>();
|
||||
}
|
||||
|
||||
public void Save(uint seed)
|
||||
{
|
||||
var encrypt = Encryption.Encrypt(RawData, seed, RawHeader);
|
||||
var encrypt = Encryption.Encrypt(Data, seed, Header);
|
||||
Provider.WriteFile(NameData, encrypt.Data.Span);
|
||||
Provider.WriteFile(NameHeader, encrypt.Header.Span);
|
||||
}
|
||||
|
||||
@@ -11,11 +11,10 @@ namespace NHSE.Core;
|
||||
/// Creates a HorizonSave from a file provider.
|
||||
/// </remarks>
|
||||
/// <param name="provider">Provider for reading/writing save files.</param>
|
||||
public class HorizonSave(ISaveFileProvider provider)
|
||||
public sealed class HorizonSave(ISaveFileProvider provider)
|
||||
{
|
||||
public readonly MainSave Main = new(provider);
|
||||
public readonly Player[] Players = Player.ReadMany(provider);
|
||||
private readonly ISaveFileProvider Provider = provider;
|
||||
public readonly IReadOnlyList<Player> Players = Player.ReadMany(provider);
|
||||
|
||||
public override string ToString() => $"{Players[0].Personal.TownName} - {Players[0]}";
|
||||
|
||||
@@ -57,7 +56,7 @@ public void Save(uint seed)
|
||||
pair.Save(seed);
|
||||
}
|
||||
}
|
||||
Provider.Flush();
|
||||
provider.Flush();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -62,7 +62,7 @@ public abstract class MainSaveOffsets
|
||||
public abstract int PlayerHouseSize { get; }
|
||||
public abstract int PlayerRoomSize { get; }
|
||||
|
||||
public virtual int AcreColumnCount => 7;
|
||||
public virtual byte FieldItemAcreWidth => 7;
|
||||
|
||||
public abstract IVillager ReadVillager(Memory<byte> data);
|
||||
public abstract IVillagerHouse ReadVillagerHouse(Memory<byte> data);
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets10 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets10 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x108;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets11 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets11 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets110 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets110 : MainSaveOffsets
|
||||
{
|
||||
public override int PatternCount => PatternCount2;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
/// <remarks>Same as <see cref="MainSaveOffsets110"/></remarks>
|
||||
public class MainSaveOffsets111 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets111 : MainSaveOffsets
|
||||
{
|
||||
public override int PatternCount => PatternCount2;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets12 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets12 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets13 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets13 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets14 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets14 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets15 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets15 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets16 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets16 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets17 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets17 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
/// <remarks>Same as <see cref="MainSaveOffsets17"/></remarks>.
|
||||
public class MainSaveOffsets18 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets18 : MainSaveOffsets
|
||||
{
|
||||
#region GSaveLand
|
||||
public const int GSaveLandStart = 0x110;
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets19 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets19 : MainSaveOffsets
|
||||
{
|
||||
public override int PatternCount => PatternCount2;
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
/// <remarks>Same as <see cref="MainSaveOffsets110"/></remarks>
|
||||
public class MainSaveOffsets20 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets20 : MainSaveOffsets
|
||||
{
|
||||
public override int PatternCount => PatternCount2;
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="MainSaveOffsets"/>
|
||||
/// </summary>
|
||||
public class MainSaveOffsets30 : MainSaveOffsets
|
||||
public sealed class MainSaveOffsets30 : MainSaveOffsets
|
||||
{
|
||||
public override int PatternCount => PatternCount2;
|
||||
|
||||
@@ -35,7 +35,7 @@ public class MainSaveOffsets30 : MainSaveOffsets
|
||||
public const int GSaveMainFieldStart = GSaveLandStart + 0x22f3f0;
|
||||
|
||||
// Map size increased to accommodate the Hotel, by adding 2 columns of acres!
|
||||
public override int AcreColumnCount => 9; // from 7 to 9
|
||||
public override byte FieldItemAcreWidth => 9; // from 7 to 9
|
||||
// does this actually impact anything? We'll eventually find out if so; I hope it is just 2 columns of unused.
|
||||
|
||||
// Layer0: 54000 => 6C000
|
||||
|
||||
@@ -8,7 +8,7 @@ namespace NHSE.Core;
|
||||
/// Interact-able structure that can be entered by the player.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Explicit, Size = SIZE, Pack = 1)]
|
||||
public class Building
|
||||
public sealed class Building
|
||||
{
|
||||
public const int SIZE = 0x14;
|
||||
|
||||
|
||||
@@ -35,4 +35,13 @@ public enum BuildingType : ushort
|
||||
Incline = 27,
|
||||
ReddsTreasureTrawler = 28,
|
||||
Studio = 29,
|
||||
}
|
||||
|
||||
Hotel = 42,
|
||||
}
|
||||
public static class BuildingUtil
|
||||
{
|
||||
public static (int Width, int Height) GetDimensions(this BuildingType type) => type switch
|
||||
{
|
||||
_ => (2, 2),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Simple design pattern
|
||||
/// </summary>
|
||||
public class DesignPattern : IVillagerOrigin
|
||||
public sealed class DesignPattern(Memory<byte> Raw) : IVillagerOrigin
|
||||
{
|
||||
public const int Width = 32;
|
||||
public const int Height = 32;
|
||||
@@ -20,11 +20,8 @@ public class DesignPattern : IVillagerOrigin
|
||||
private const int PixelCount = 0x400; // Width * Height
|
||||
//private const int PixelDataSize = PixelCount / 2; // 4bit|4bit pixel packing
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public DesignPattern(Memory<byte> data) => Raw = data;
|
||||
|
||||
public uint Hash
|
||||
{
|
||||
get => ReadUInt32LittleEndian(Data);
|
||||
@@ -119,7 +116,13 @@ public static int GetColorOffset(int index)
|
||||
/// </summary>
|
||||
public byte[] GetBitmap()
|
||||
{
|
||||
byte[] data = new byte[4 * Width * Height];
|
||||
var result = new byte[4 * Width * Height];
|
||||
LoadBitmap(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void LoadBitmap(Span<byte> data)
|
||||
{
|
||||
for (int i = 0; i < PixelCount; i++)
|
||||
{
|
||||
var choice = this[i];
|
||||
@@ -132,7 +135,6 @@ public byte[] GetBitmap()
|
||||
data[ofs + 0] = Data[palette + 2];
|
||||
data[ofs + 3] = 0xFF; // opaque
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -141,6 +143,12 @@ public byte[] GetBitmap()
|
||||
public byte[] GetPaletteBitmap()
|
||||
{
|
||||
var result = new byte[3 * PaletteColorCount];
|
||||
LoadPaletteBitmap(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void LoadPaletteBitmap(Span<byte> result)
|
||||
{
|
||||
for (int i = 0; i < PaletteColorCount; i++)
|
||||
{
|
||||
var ofs = PaletteDataStart + (i * 3);
|
||||
@@ -148,6 +156,5 @@ public byte[] GetPaletteBitmap()
|
||||
result[(i * 3) + 1] = Data[ofs + 1];
|
||||
result[(i * 3) + 0] = Data[ofs + 2];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Advanced design pattern with 4 sheets arranged in a square.
|
||||
/// </summary>
|
||||
public class DesignPatternPRO : IVillagerOrigin
|
||||
public sealed class DesignPatternPRO(Memory<byte> Raw) : IVillagerOrigin
|
||||
{
|
||||
public const int Width = 32;
|
||||
public const int Height = 32;
|
||||
@@ -21,11 +21,8 @@ public class DesignPatternPRO : IVillagerOrigin
|
||||
private const int PixelCount = 0x400; // Width * Height
|
||||
private const int SheetDataSize = PixelCount / 2; // 4bit|4bit pixel packing
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public DesignPatternPRO(Memory<byte> data) => Raw = data;
|
||||
|
||||
public uint Hash
|
||||
{
|
||||
get => ReadUInt32LittleEndian(Data);
|
||||
@@ -114,7 +111,13 @@ public static int GetColorOffset(int index)
|
||||
/// </summary>
|
||||
public byte[] GetBitmap(int sheet)
|
||||
{
|
||||
byte[] data = new byte[4 * Width * Height];
|
||||
var result = new byte[4 * Width * Height];
|
||||
LoadBitmap(sheet, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void LoadBitmap(int sheet, Span<byte> data)
|
||||
{
|
||||
for (int i = 0; i < PixelCount; i++)
|
||||
{
|
||||
var choice = GetPixelAtIndex(sheet, i);
|
||||
@@ -127,7 +130,6 @@ public byte[] GetBitmap(int sheet)
|
||||
data[ofs + 0] = Data[palette + 2];
|
||||
data[ofs + 3] = 0xFF; // opaque
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,6 +138,12 @@ public byte[] GetBitmap(int sheet)
|
||||
public byte[] GetPaletteBitmap()
|
||||
{
|
||||
var result = new byte[3 * PaletteColorCount];
|
||||
LoadPaletteBitmap(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private void LoadPaletteBitmap(Span<byte> result)
|
||||
{
|
||||
for (int i = 0; i < PaletteColorCount; i++)
|
||||
{
|
||||
var ofs = PaletteDataStart + (i * 3);
|
||||
@@ -143,6 +151,5 @@ public byte[] GetPaletteBitmap()
|
||||
result[(i * 3) + 1] = Data[ofs + 1];
|
||||
result[(i * 3) + 0] = Data[ofs + 2];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +75,7 @@ public static void DumpPlayerHouses(this IReadOnlyList<IPlayerHouse> houses, IRe
|
||||
/// <param name="path">Path to dump to</param>
|
||||
public static void DumpPlayerHouses(this HorizonSave sav, string path)
|
||||
{
|
||||
var count = Math.Min(sav.Players.Length, MainSaveOffsets.PlayerCount);
|
||||
var count = Math.Min(sav.Players.Count, MainSaveOffsets.PlayerCount);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var p = sav.Players[i];
|
||||
|
||||
@@ -225,6 +225,7 @@ public void CopyFrom(Item item)
|
||||
|
||||
public static Item[] GetArray(ReadOnlySpan<byte> data) => data.GetArray<Item>(SIZE);
|
||||
public static byte[] SetArray(IReadOnlyList<Item> data) => data.SetArray(SIZE);
|
||||
public static byte[] SetArray(ReadOnlySpan<Item> data) => data.SetArray(SIZE);
|
||||
|
||||
public ushort GetWrappedItemName() => WrappingType switch
|
||||
{
|
||||
|
||||
@@ -3,10 +3,9 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class ItemArrayEditor<T> where T : Item, ICopyableItem<T>
|
||||
public sealed record ItemArrayEditor<T>(IReadOnlyList<T> Items)
|
||||
where T : Item, ICopyableItem<T>
|
||||
{
|
||||
public readonly IReadOnlyList<T> Items;
|
||||
public ItemArrayEditor(IReadOnlyList<T> items) => Items = items;
|
||||
public int ItemSize => Items[0].Size;
|
||||
public int TotalSize => Items.Count * ItemSize;
|
||||
|
||||
|
||||
@@ -5,38 +5,19 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Metadata for an item's customization permissions
|
||||
/// </summary>
|
||||
public class ItemRemakeInfo
|
||||
public sealed record ItemRemakeInfo(
|
||||
short Index,
|
||||
ushort ItemUniqueID,
|
||||
sbyte ReBodyPatternNum,
|
||||
byte[] ReBodyPatternColors0,
|
||||
byte[] ReBodyPatternColors1,
|
||||
byte[] ReFabricPatternColors0,
|
||||
byte[] ReFabricPatternColors1,
|
||||
bool ReFabricPattern0VisibleOff)
|
||||
{
|
||||
public const int BodyColorCountMax = 8;
|
||||
public const int NoColor = (int)ItemCustomColor.None; // 14
|
||||
|
||||
public readonly short Index;
|
||||
public readonly ushort ItemUniqueID;
|
||||
public readonly sbyte ReBodyPatternNum; // count of body colors
|
||||
|
||||
public readonly byte[] ReBodyPatternColors0;
|
||||
public readonly byte[] ReBodyPatternColors1;
|
||||
|
||||
public readonly byte[] ReFabricPatternColors0;
|
||||
public readonly byte[] ReFabricPatternColors1;
|
||||
|
||||
public readonly bool ReFabricPattern0VisibleOff;
|
||||
|
||||
public ItemRemakeInfo(short index, ushort id, sbyte count, byte[] bc0, byte[] bc1, byte[] fc0, byte[] fc1, bool fp0)
|
||||
{
|
||||
Index = index;
|
||||
ItemUniqueID = id;
|
||||
ReBodyPatternNum = count;
|
||||
|
||||
ReBodyPatternColors0 = bc0;
|
||||
ReBodyPatternColors1 = bc1;
|
||||
|
||||
ReFabricPatternColors0 = fc0;
|
||||
ReFabricPatternColors1 = fc1;
|
||||
|
||||
ReFabricPattern0VisibleOff = fp0;
|
||||
}
|
||||
|
||||
private const string Invalid = nameof(Invalid);
|
||||
|
||||
public bool HasBodyColor(int variant) => ReBodyPatternColors0[variant] != NoColor || ReBodyPatternColors1[variant] != NoColor;
|
||||
|
||||
37
NHSE.Core/Structures/Map/AcreCoordinate.cs
Normal file
37
NHSE.Core/Structures/Map/AcreCoordinate.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Navigation metadata for acre coordinates.
|
||||
/// </summary>
|
||||
[DebuggerDisplay("{Name} ({X}, {Y})")]
|
||||
public readonly record struct AcreCoordinate(char XChar, char YChar, byte X, byte Y)
|
||||
{
|
||||
public const int CountAcreExteriorWidth = 9;
|
||||
public const int CountAcreExteriorHeight = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Entire grid including exterior acre coordinates (bordered by acres of deep sea->shoreline=>terrain).
|
||||
/// </summary>
|
||||
public static readonly AcreCoordinate[] Exterior = GetGridWithExterior(CountAcreExteriorWidth, CountAcreExteriorHeight);
|
||||
|
||||
public string Name => $"{YChar}{XChar}";
|
||||
|
||||
private static AcreCoordinate[] GetGridWithExterior([ConstantExpected] int width, [ConstantExpected] int height)
|
||||
{
|
||||
var result = new AcreCoordinate[width * height];
|
||||
int i = 0;
|
||||
for (byte y = 0; y < height; y++)
|
||||
{
|
||||
for (byte x = 0; x < width; x++)
|
||||
{
|
||||
var xn = (x == 0) ? '<' : x == width - 1 ? '>' : (char)('0' + x - 1);
|
||||
var yn = (y == 0) ? '^' : y == height - 1 ? 'V' : (char)('A' + y - 1);
|
||||
result[i++] = new AcreCoordinate(xn, yn, x, y);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
14
NHSE.Core/Structures/Map/AcreSelectionGrid.cs
Normal file
14
NHSE.Core/Structures/Map/AcreSelectionGrid.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Basic logic implementation for interacting with the manipulatable map grid.
|
||||
/// </summary>
|
||||
public abstract record AcreSelectionGrid(TileGridViewport TileInfo)
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the specified relative x/y coordinates are within the bounds of this layer.
|
||||
/// </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>
|
||||
public bool Contains(int relX, int relY) => TileInfo.Contains(relX, relY);
|
||||
}
|
||||
33
NHSE.Core/Structures/Map/Layers/ILayerBuilding.cs
Normal file
33
NHSE.Core/Structures/Map/Layers/ILayerBuilding.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for interacting with a map's building layer.
|
||||
/// </summary>
|
||||
public interface ILayerBuilding
|
||||
{
|
||||
/// <summary>
|
||||
/// Instantiated list of all buildings on the map.
|
||||
/// </summary>
|
||||
IReadOnlyList<Building> Buildings { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Converts relative building coordinates to absolute coordinates in the map grid.
|
||||
/// </summary>
|
||||
/// <param name="relX">Relative building X coordinate</param>
|
||||
/// <param name="relY">Relative building Y coordinate</param>
|
||||
/// <returns>Map absolute X/Y coordinates</returns>
|
||||
(int X, int Y) GetCoordinatesAbsolute(ushort relX, ushort relY);
|
||||
|
||||
/// <summary>
|
||||
/// Converts absolute map grid coordinates to relative building coordinates.
|
||||
/// </summary>
|
||||
/// <param name="absX">Map absolute X coordinate</param>
|
||||
/// <param name="absY">Map absolute Y coordinate</param>
|
||||
/// <returns>Relative building X/Y coordinates</returns>
|
||||
(int X, int Y) GetCoordinatesRelative(int absX, int absY);
|
||||
|
||||
Building this[int i] { get; }
|
||||
int Count { get; }
|
||||
}
|
||||
48
NHSE.Core/Structures/Map/Layers/ILayerFieldItemFlag.cs
Normal file
48
NHSE.Core/Structures/Map/Layers/ILayerFieldItemFlag.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for managing field item flags within a layer.
|
||||
/// </summary>
|
||||
public interface ILayerFieldItemFlag
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the active state of the field item flag at the specified relative coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">Relative X coordinate within the array.</param>
|
||||
/// <param name="relY">Relative Y coordinate within the array.</param>
|
||||
/// <returns>The active state of the field item flag.</returns>
|
||||
bool GetIsActive(int relX, int relY);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the active state of the field item flag at the specified relative coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">Relative X coordinate within the array.</param>
|
||||
/// <param name="relY">Relative Y coordinate within the array.</param>
|
||||
/// <param name="value">The active state to set.</param>
|
||||
void SetIsActive(int relX, int relY, bool value = true);
|
||||
|
||||
bool this[int relX, int relY]
|
||||
{
|
||||
get => GetIsActive(relX, relY);
|
||||
set => SetIsActive(relX, relY, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates all field item flags.
|
||||
/// </summary>
|
||||
void DeactivateAll();
|
||||
|
||||
/// <summary>
|
||||
/// Saves the (in)active flags into the provided destination span.
|
||||
/// </summary>
|
||||
/// <param name="dest">Destination span to save the flags into.</param>
|
||||
void Save(Span<byte> dest);
|
||||
|
||||
/// <summary>
|
||||
/// Imports the (in)active flags from the provided source span.
|
||||
/// </summary>
|
||||
/// <param name="src">Source span containing the flags to import.</param>
|
||||
void Import(Span<byte> src);
|
||||
}
|
||||
27
NHSE.Core/Structures/Map/Layers/ILayerFieldItemSet.cs
Normal file
27
NHSE.Core/Structures/Map/Layers/ILayerFieldItemSet.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public interface ILayerFieldItemSet
|
||||
{
|
||||
LayerFieldItem Layer0 { get; }
|
||||
LayerFieldItem Layer1 { get; }
|
||||
|
||||
bool IsOccupied(int relX, int relY);
|
||||
|
||||
Item GetItem(int relX, int relY, bool baseLayer);
|
||||
void SetItem(int relX, int relY, bool baseLayer, Item value);
|
||||
|
||||
Item this[int relX, int relY, bool baseLayer]
|
||||
{
|
||||
get => GetItem(relX, relY, baseLayer);
|
||||
set => SetItem(relX, relY, baseLayer, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists out all coordinates of tiles present in <see cref="Layer1"/> that don't have anything underneath in <see cref="Layer0"/> to support them.
|
||||
/// </summary>
|
||||
List<string> GetUnsupportedTiles(int totalWidth, int totalHeight);
|
||||
|
||||
List<string> GetUnsupportedTiles() => GetUnsupportedTiles(Layer0.TileInfo.TotalWidth, Layer0.TileInfo.TotalHeight);
|
||||
}
|
||||
@@ -1,207 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public abstract class ItemLayer : MapGrid
|
||||
{
|
||||
public readonly Item[] Tiles;
|
||||
|
||||
protected ItemLayer(Item[] tiles, int w, int h) : this(tiles, w, h, w, h)
|
||||
{
|
||||
}
|
||||
|
||||
protected ItemLayer(Item[] tiles, int w, int h, int gw, int gh) : base(gw, gh, w, h)
|
||||
{
|
||||
Tiles = tiles;
|
||||
Debug.Assert(MaxWidth * MaxHeight == tiles.Length);
|
||||
}
|
||||
|
||||
public Item GetTile(in int x, in int y) => this[GetTileIndex(x, y)];
|
||||
|
||||
public Item this[int index]
|
||||
{
|
||||
get => Tiles[index];
|
||||
set => Tiles[index] = value;
|
||||
}
|
||||
|
||||
public byte[] DumpAll()
|
||||
{
|
||||
var result = new byte[Tiles.Length * Item.SIZE];
|
||||
for (int i = 0; i < Tiles.Length; i++)
|
||||
Tiles[i].ToBytesClass().CopyTo(result, i * Item.SIZE);
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ImportAll(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var tiles = Item.GetArray(data);
|
||||
for (int i = 0; i < tiles.Length; i++)
|
||||
Tiles[i].CopyFrom(tiles[i]);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 >= MaxWidth)
|
||||
w = MaxWidth - x;
|
||||
if (y + h - 1 >= MaxHeight)
|
||||
h = MaxHeight - 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 >= MaxWidth)
|
||||
return PlacedItemPermission.OutOfBounds;
|
||||
if (y + h - 1 >= MaxHeight)
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
39
NHSE.Core/Structures/Map/Layers/LayerBuilding.cs
Normal file
39
NHSE.Core/Structures/Map/Layers/LayerBuilding.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for interacting with a map's building layer.
|
||||
/// </summary>
|
||||
public sealed record LayerBuilding : ILayerBuilding
|
||||
{
|
||||
public required IReadOnlyList<Building> Buildings { get; init; }
|
||||
|
||||
// Although there is terrain in the Top Row and Left Column, no buildings can be placed there.
|
||||
// Buildings can only be placed below that line; per the map layout, there is 2 acres worth of buffer, then our origin starts.
|
||||
|
||||
/// <summary>
|
||||
/// Compared to Item Tiles, building tiles are a 16x16 resolution (2x2 item tiles).
|
||||
/// When converting between building coordinates and absolute coordinates, we need to account for this.
|
||||
/// </summary>
|
||||
private const int BuildingResolution = 2;
|
||||
private const int BuildingTilesPerAcre = 16;
|
||||
private const int AcreBufferEdge = 2;
|
||||
|
||||
public (int X, int Y) GetCoordinatesAbsolute(ushort relX, ushort relY)
|
||||
{
|
||||
int absX = (relX * BuildingResolution) + (AcreBufferEdge * BuildingTilesPerAcre);
|
||||
int absY = (relY * BuildingResolution) + (AcreBufferEdge * BuildingTilesPerAcre);
|
||||
return (absX, absY);
|
||||
}
|
||||
|
||||
public (int X, int Y) GetCoordinatesRelative(int absX, int absY)
|
||||
{
|
||||
int relX = (absX - (AcreBufferEdge * BuildingTilesPerAcre)) / BuildingResolution;
|
||||
int relY = (absY - (AcreBufferEdge * BuildingTilesPerAcre)) / BuildingResolution;
|
||||
return (relX, relY);
|
||||
}
|
||||
|
||||
public Building this[int i] => Buildings[i];
|
||||
public int Count => Buildings.Count;
|
||||
}
|
||||
@@ -3,47 +3,17 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class FieldItemLayer : ItemLayer
|
||||
public sealed record LayerFieldItem(Item[] Tiles, byte AcreWidth, byte AcreHeight)
|
||||
: LayerItem(Tiles, GetViewport(AcreWidth, AcreHeight))
|
||||
{
|
||||
private static TileGridViewport GetViewport(byte width, byte height) => new(TilesPerAcreDim, TilesPerAcreDim, width, height);
|
||||
|
||||
public const int TilesPerAcreDim = 32;
|
||||
public const int FieldItemWidth = TilesPerAcreDim * AcreWidth;
|
||||
public const int FieldItemHeight = TilesPerAcreDim * AcreHeight;
|
||||
|
||||
public FieldItemLayer(Item[] tiles) : base(tiles, FieldItemWidth, FieldItemHeight, TilesPerAcreDim, TilesPerAcreDim)
|
||||
{
|
||||
}
|
||||
|
||||
public Item GetTile(int acreX, int acreY, int gridX, int gridY) => this[GetTileIndex(acreX, acreY, gridX, gridY)];
|
||||
public Item GetAcreTile(int acreIndex, int tileIndex) => this[GetAcreTileIndex(acreIndex, tileIndex)];
|
||||
|
||||
public byte[] DumpAcre(int acre)
|
||||
{
|
||||
int count = GridTileCount;
|
||||
var result = new byte[Item.SIZE * count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var tile = GetAcreTile(acre, i);
|
||||
var bytes = tile.ToBytesClass();
|
||||
bytes.CopyTo(result, i * Item.SIZE);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ImportAcre(int acre, ReadOnlySpan<byte> data)
|
||||
{
|
||||
int count = GridTileCount;
|
||||
var tiles = Item.GetArray(data);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var tile = GetAcreTile(acre, i);
|
||||
tile.CopyFrom(tiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public int ClearFieldPlanted(Func<FieldItemKind, bool> criteria) => ClearFieldPlanted(0, 0, MaxWidth, MaxHeight, criteria);
|
||||
public int RemoveAll(Func<Item, bool> criteria) => RemoveAll(0, 0, MaxWidth, MaxHeight, criteria);
|
||||
public int RemoveAll(HashSet<ushort> items) => RemoveAll(0, 0, MaxWidth, MaxHeight, z => items.Contains(z.DisplayItemId));
|
||||
public int RemoveAll(ushort item) => RemoveAll(0, 0, MaxWidth, MaxHeight, z => z.DisplayItemId == item);
|
||||
public int ClearFieldPlanted(Func<FieldItemKind, bool> criteria) => ClearFieldPlanted(0, 0, TileInfo.TotalWidth, TileInfo.TotalHeight, criteria);
|
||||
public int RemoveAll(Func<Item, bool> criteria) => RemoveAll(0, 0, TileInfo.TotalWidth, TileInfo.TotalHeight, criteria);
|
||||
public int RemoveAll(HashSet<ushort> items) => RemoveAll(0, 0, TileInfo.TotalWidth, TileInfo.TotalHeight, z => items.Contains(z.DisplayItemId));
|
||||
public int RemoveAll(ushort item) => RemoveAll(0, 0, TileInfo.TotalWidth, TileInfo.TotalHeight, z => z.DisplayItemId == item);
|
||||
|
||||
public int ClearFieldPlanted(int xmin, int ymin, int width, int height, Func<FieldItemKind, bool> criteria)
|
||||
{
|
||||
@@ -54,6 +24,9 @@ public int ClearFieldPlanted(int xmin, int ymin, int width, int height, Func<Fie
|
||||
{
|
||||
for (int y = ymin; y < ymin + height; y++)
|
||||
{
|
||||
if (!Contains(x, y))
|
||||
continue;
|
||||
|
||||
var t = GetTile(x, y);
|
||||
var disp = t.DisplayItemId;
|
||||
if (!fi.TryGetValue(disp, out var val))
|
||||
@@ -75,6 +48,8 @@ public int ModifyAll(int xmin, int ymin, int width, int height, Func<Item, bool>
|
||||
{
|
||||
for (int y = ymin; y < ymin + height; y++)
|
||||
{
|
||||
if (!Contains(x, y))
|
||||
continue;
|
||||
var t = GetTile(x, y);
|
||||
if (!criteria(t))
|
||||
continue;
|
||||
@@ -119,4 +94,10 @@ bool IsFlowerWaterable(Item item)
|
||||
|
||||
return ModifyAll(xmin, ymin, width, height, IsFlowerWaterable, z => z.Water(all));
|
||||
}
|
||||
|
||||
public Item this[int relX, int relY]
|
||||
{
|
||||
get => GetTile(relX, relY);
|
||||
set => SetTile(relX, relY, value);
|
||||
}
|
||||
}
|
||||
62
NHSE.Core/Structures/Map/Layers/LayerFieldItemFlag.cs
Normal file
62
NHSE.Core/Structures/Map/Layers/LayerFieldItemFlag.cs
Normal file
@@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Logic for managing field item flags within a layer.
|
||||
/// </summary>
|
||||
public sealed class LayerFieldItemFlag(Memory<byte> raw, int width, int height) : ILayerFieldItemFlag
|
||||
{
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public bool GetIsActive(int relX, int relY)
|
||||
=> FlagUtil.GetFlag(Data, GetLayerFlagIndex(relX, relY));
|
||||
public void SetIsActive(int relX, int relY, bool value)
|
||||
=> FlagUtil.SetFlag(Data, GetLayerFlagIndex(relX, relY), value);
|
||||
|
||||
public bool this[int relX, int relY]
|
||||
{
|
||||
get => GetIsActive(relX, relY);
|
||||
set => SetIsActive(relX, relY, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Although the Field Item Tiles are arranged y-column (y-x) based, the 'IsActive' flags are arranged x-row (x-y) based.
|
||||
/// </summary>
|
||||
private int GetLayerFlagIndex(int x, int y) => (y * width) + x;
|
||||
|
||||
public void DeactivateAll() => Data.Clear();
|
||||
|
||||
public void Save(Span<byte> dest) => Data.CopyTo(dest);
|
||||
|
||||
public void Import(Span<byte> src) => src.CopyTo(Data);
|
||||
|
||||
public bool IsInLayer(int tileX, int tileY) => !((uint)tileX >= width || (uint)tileY >= height);
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostic check of the active flags against the tiles.
|
||||
/// </summary>
|
||||
/// <param name="tiles">Tiles to check against.</param>
|
||||
public void DebugCheckTileActiveFlags(LayerItem tiles)
|
||||
{
|
||||
// this doesn't set anything; just diagnostic
|
||||
|
||||
// Although the Tiles are arranged y-column (y-x) based, the 'isActive' flags are arranged x-row (x-y) based.
|
||||
// We can turn the isActive flag off if the item is not a root or the item cannot be animated.
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
var tile = tiles.GetTile(x, y);
|
||||
var isActive = GetIsActive(x, y);
|
||||
if (!isActive)
|
||||
continue;
|
||||
|
||||
bool empty = tile.IsNone;
|
||||
if (empty)
|
||||
Debug.WriteLine($"Flag at ({x},{y}) is not a root object.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
NHSE.Core/Structures/Map/Layers/LayerFieldItemSet.cs
Normal file
59
NHSE.Core/Structures/Map/Layers/LayerFieldItemSet.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the <see cref="Item"/> data for the player's outside overworld.
|
||||
/// </summary>
|
||||
public sealed class LayerFieldItemSet : ILayerFieldItemSet
|
||||
{
|
||||
/// <summary>
|
||||
/// Base layer of items
|
||||
/// </summary>
|
||||
public required LayerFieldItem Layer0 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Layer of items that are supported by <see cref="Layer0"/>
|
||||
/// </summary>
|
||||
public required LayerFieldItem Layer1 { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Lists out all coordinates of tiles present in <see cref="Layer1"/> that don't have anything underneath in <see cref="Layer0"/> to support them.
|
||||
/// </summary>
|
||||
public List<string> GetUnsupportedTiles(int totalWidth, int totalHeight)
|
||||
{
|
||||
var result = new List<string>();
|
||||
for (int x = 0; x < totalWidth; x++)
|
||||
{
|
||||
for (int y = 0; y < totalHeight; y++)
|
||||
{
|
||||
// If there is an item on this layer...
|
||||
var tile = Layer1.GetTile(x, y);
|
||||
if (tile.IsNone)
|
||||
continue;
|
||||
|
||||
// Then there must be something underneath it.
|
||||
var support = Layer0.GetTile(x, y);
|
||||
if (!support.IsNone)
|
||||
continue; // dunno how to check if the tile can actually have an item put on top of it...
|
||||
|
||||
result.Add($"{x:000},{y:000}");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool IsOccupied(int relX, int relY) => !Layer0.GetTile(relX, relY).IsNone || !Layer1.GetTile(relX, relY).IsNone;
|
||||
|
||||
public Item GetItem(int relX, int relY, bool baseLayer)
|
||||
{
|
||||
var layer = baseLayer ? Layer0 : Layer1;
|
||||
return layer.GetTile(relX, relY);
|
||||
}
|
||||
|
||||
public void SetItem(int relX, int relY, bool baseLayer, Item value)
|
||||
{
|
||||
var layer = baseLayer ? Layer0 : Layer1;
|
||||
layer[relX, relY] = value;
|
||||
}
|
||||
}
|
||||
414
NHSE.Core/Structures/Map/Layers/LayerItem.cs
Normal file
414
NHSE.Core/Structures/Map/Layers/LayerItem.cs
Normal file
@@ -0,0 +1,414 @@
|
||||
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, w, h))
|
||||
#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;
|
||||
}
|
||||
}
|
||||
129
NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs
Normal file
129
NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Configures how a layer rests within the Map's grid, relative to a "chunk" or "acre".
|
||||
/// </summary>
|
||||
/// <param name="CountWidth">Number of acres in the width direction.</param>
|
||||
/// <param name="CountHeight">Number of acres in the height direction.</param>
|
||||
/// <param name="ShiftWidth">Horizontal acre shift from the map's origin.</param>
|
||||
/// <param name="ShiftHeight">Vertical acre shift from the map's origin.</param>
|
||||
/// <param name="TilesPerAcre">Number of tiles per acre in one dimension (16 or 32).</param>
|
||||
/// <param name="TileBitShift">Bit shift value to convert between tiles and acres (4 for 16 tiles, 5 for 32 tiles).</param>
|
||||
/// <param name="MetaTileSize">Size of tile compared to the smallest tile possible (2 for 16 tiles, 1 for 32 tiles).</param>
|
||||
public readonly record struct LayerPositionConfig(
|
||||
byte CountWidth, byte CountHeight,
|
||||
byte ShiftWidth, byte ShiftHeight,
|
||||
[ConstantExpected] byte TilesPerAcre, byte TileBitShift,
|
||||
[ConstantExpected(Max = 2, Min = 1)] byte MetaTileSize)
|
||||
{
|
||||
// Maps in Animal Crossing: New Horizons are made up of acres that are 9 tiles wide and 8 tiles high.
|
||||
// 5 columns in the center are land, surrounded by 2 tiles of beach and 2 tiles of sea on each side.
|
||||
// 4 rows in the center are land, surrounded by 2 rows of beach and 2 rows of sea on each side.
|
||||
// +-----------+
|
||||
// | ~~~~~~~~~ |
|
||||
// | ~*******~ |
|
||||
// | ~*=====*~ |
|
||||
// | ~*=====*~ |
|
||||
// | ~*=====*~ |
|
||||
// | ~*=====*~ |
|
||||
// | ~*******~ |
|
||||
// | ~~~~~~~~~ |
|
||||
// +-----------+
|
||||
|
||||
// Main Island Map Config - True Dimensions
|
||||
private const byte MapAcreWidth = 9; // 2 sea, 2 beach, 5 land
|
||||
private const byte MapAcreHeight = 8; // 2 sea, 2 beach, 4 land
|
||||
|
||||
// Optimize some calculations away by using bit-shift instead of mul/div, as we're always a multiple of 2.
|
||||
private const byte Grid32 = 32;
|
||||
private const byte Grid16 = 16;
|
||||
private const byte Shift32 = 5; // div32 is same as sh 5
|
||||
private const byte Shift16 = 4; // div16 is same as sh 4
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="LayerPositionConfig"/> instance, centering the layer within the acre.
|
||||
/// </summary>
|
||||
/// <param name="width">Width of the layer in acres.</param>
|
||||
/// <param name="height">Height of the layer in acres.</param>
|
||||
/// <param name="tilesPerAcre">Number of tiles per acre (16 or 32).</param>
|
||||
/// <param name="metaTileSize">Size of tile compared to the smallest tile possible (2 for 16 tiles, 1 for 32 tiles).</param>
|
||||
/// <param name="shiftW">Acre-wise horizontal shift from the map origin.</param>
|
||||
/// <param name="shiftH">Acre-wise vertical shift from the map origin.</param>
|
||||
/// <returns>A new <see cref="LayerPositionConfig"/> instance.</returns>
|
||||
public static LayerPositionConfig Create(byte width, byte height,
|
||||
[ConstantExpected(Min = Grid16, Max = Grid32)] byte tilesPerAcre,
|
||||
[ConstantExpected(Min = 1, Max = 2)] byte metaTileSize, byte shiftW, byte shiftH)
|
||||
{
|
||||
var bitShift = tilesPerAcre == Grid16 ? Shift16 : Shift32;
|
||||
#pragma warning disable CA1857
|
||||
return new LayerPositionConfig(width, height, shiftW, shiftH, tilesPerAcre, bitShift, metaTileSize);
|
||||
#pragma warning restore CA1857
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the absolute map coordinates based on the specified relative X and Y coordinates within the layer.
|
||||
/// </summary>
|
||||
/// <param name="relX">The relative X-coordinate within the layer.</param>
|
||||
/// <param name="relY">The relative Y-coordinate within the layer.</param>
|
||||
public (int X, int Y) GetCoordinatesAbsolute(int relX, int relY)
|
||||
{
|
||||
var absX = relX + ((ShiftWidth * MetaTileSize) << TileBitShift);
|
||||
var absY = relY + ((ShiftHeight * MetaTileSize) << TileBitShift);
|
||||
return (absX, absY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute coordinates of the layer's origin (0,0) in the map.
|
||||
/// </summary>
|
||||
public (int X, int Y) GetCoordinatesAbsolute() => GetCoordinatesAbsolute(0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the relative coordinates within the layer, based on the specified absolute X and Y coordinates.
|
||||
/// </summary>
|
||||
/// <param name="absX">The absolute X coordinate to convert.</param>
|
||||
/// <param name="absY">The absolute Y coordinate to convert.</param>
|
||||
/// <returns>A tuple containing the X and Y coordinates relative to the layer.</returns>
|
||||
public (int X, int Y) GetCoordinatesRelative(int absX, int absY)
|
||||
{
|
||||
var relX = absX - ((ShiftWidth * MetaTileSize) << TileBitShift);
|
||||
var relY = absY - ((ShiftHeight * MetaTileSize) << TileBitShift);
|
||||
return (relX, relY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified absolute X and Y coordinates are within the valid bounds of the map.
|
||||
/// </summary>
|
||||
/// <param name="relX">The absolute X coordinate to validate. Must be within the horizontal bounds of the map.</param>
|
||||
/// <param name="relY">The absolute Y coordinate to validate. Must be within the vertical bounds of the map.</param>
|
||||
/// <returns><see langword="true"/> if the coordinates are valid; otherwise, <see langword="false"/>.</returns>
|
||||
public bool IsCoordinateValidRelative(int relX, int relY)
|
||||
{
|
||||
if ((uint)relX >= CountWidth << TileBitShift)
|
||||
return false;
|
||||
if ((uint)relY >= CountHeight << TileBitShift)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Layer total width in tiles.
|
||||
/// </summary>
|
||||
public int LayerTotalWidth => CountWidth * TilesPerAcre;
|
||||
|
||||
/// <summary>
|
||||
/// Layer total height in tiles.
|
||||
/// </summary>
|
||||
public int LayerTotalHeight => CountHeight * TilesPerAcre;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total width of the map, in tiles.
|
||||
/// </summary>
|
||||
public int MapTotalWidth => MapAcreWidth * TilesPerAcre;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total height of the map, in tiles.
|
||||
/// </summary>
|
||||
public int MapTotalHeight => MapAcreHeight * TilesPerAcre;
|
||||
}
|
||||
@@ -3,27 +3,27 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class RoomItemLayer : ItemLayer
|
||||
public sealed record LayerRoomItem : LayerItem
|
||||
{
|
||||
public const int SIZE = Width * Height * Item.SIZE;
|
||||
private const int Width = 20;
|
||||
private const int Height = 20;
|
||||
private const byte Width = 20;
|
||||
private const byte Height = 20;
|
||||
|
||||
public RoomItemLayer(ReadOnlySpan<byte> data) : this(Item.GetArray(data)) { }
|
||||
public RoomItemLayer(Item[] tiles) : base(tiles, Width, Height) { }
|
||||
public LayerRoomItem(ReadOnlySpan<byte> data) : this(Item.GetArray(data)) { }
|
||||
public LayerRoomItem(Item[] tiles) : base(tiles, Width, Height) { }
|
||||
|
||||
public static RoomItemLayer[] GetArray(ReadOnlySpan<byte> data)
|
||||
public static LayerRoomItem[] GetArray(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var result = new RoomItemLayer[data.Length / SIZE];
|
||||
var result = new LayerRoomItem[data.Length / SIZE];
|
||||
for (int i = 0; i < result.Length; i++)
|
||||
{
|
||||
var slice = data.Slice(i * SIZE, SIZE);
|
||||
result[i] = new RoomItemLayer(slice);
|
||||
result[i] = new LayerRoomItem(slice);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] SetArray(IReadOnlyList<RoomItemLayer> data)
|
||||
public static byte[] SetArray(IReadOnlyList<LayerRoomItem> data)
|
||||
{
|
||||
var result = new byte[data.Count * SIZE];
|
||||
for (int i = 0; i < data.Count; i++)
|
||||
306
NHSE.Core/Structures/Map/Layers/LayerTerrain.cs
Normal file
306
NHSE.Core/Structures/Map/Layers/LayerTerrain.cs
Normal file
@@ -0,0 +1,306 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Grid of <see cref="TerrainTile"/>
|
||||
/// </summary>
|
||||
public sealed record LayerTerrain : AcreSelectionGrid
|
||||
{
|
||||
/// <summary>
|
||||
/// Terrain tiles in this layer, stored in column-major order.
|
||||
/// </summary>
|
||||
public TerrainTile[] Tiles { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying memory buffer containing the base acre template data (such as sea, beach, interior).
|
||||
/// </summary>
|
||||
public Memory<byte> BaseAcres { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 16x16 tiles per acre.
|
||||
/// </summary>
|
||||
public const byte TilesPerAcreDim = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Interior acre-count width (without the deep-sea border).
|
||||
/// </summary>
|
||||
private const byte CountAcreWidth = 7;
|
||||
|
||||
/// <summary>
|
||||
/// Interior acre-count height (without the deep-sea border).
|
||||
/// </summary>
|
||||
private const byte CountAcreHeight = 6;
|
||||
|
||||
private static TileGridViewport Viewport => new(TilesPerAcreDim, TilesPerAcreDim, CountAcreWidth, CountAcreHeight);
|
||||
|
||||
public LayerTerrain(TerrainTile[] tiles, Memory<byte> acres) : base(Viewport)
|
||||
{
|
||||
BaseAcres = acres;
|
||||
Tiles = tiles;
|
||||
Debug.Assert(TileInfo.TotalCount == tiles.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the terrain tile at the specified coordinates.
|
||||
/// </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>
|
||||
/// <remarks>
|
||||
/// An exception is thrown if the requested coordinates are out of the layer's range.
|
||||
/// </remarks>
|
||||
public TerrainTile GetTile(int relX, int relY) => this[TileInfo.GetTileIndex(relX, relY)];
|
||||
|
||||
private TerrainTile GetTileSafe(int relX, int relY)
|
||||
{
|
||||
if (Contains(relX, relY))
|
||||
return new TerrainTile();
|
||||
return GetTile(relX, relY);
|
||||
}
|
||||
|
||||
private bool SetTileSafe(int relX, int relY, TerrainTile tile)
|
||||
{
|
||||
if (Contains(relX, relY))
|
||||
return false;
|
||||
GetTile(relX, relY).CopyFrom(tile);
|
||||
return true;
|
||||
}
|
||||
|
||||
public TerrainTile this[int index]
|
||||
{
|
||||
get => Tiles[index];
|
||||
set => Tiles[index] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flattens the terrain tiles into a contiguous byte array.
|
||||
/// </summary>
|
||||
public byte[] DumpAll() => TerrainTile.SetArray(Tiles);
|
||||
|
||||
/// <summary>
|
||||
/// Imports terrain tiles from a contiguous byte array.
|
||||
/// </summary>
|
||||
/// <param name="data">Byte array containing terrain tile data.</param>
|
||||
public void ImportAll(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var tiles = TerrainTile.GetArray(data);
|
||||
for (int i = 0; i < tiles.Length; i++)
|
||||
Tiles[i].CopyFrom(tiles[i]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the serialized data for the acre at the specified relative coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">The relative X-coordinate of the top-left tile of the acre to dump.</param>
|
||||
/// <param name="relY">The relative Y-coordinate of the top-left tile of the acre to dump.</param>
|
||||
/// <returns>
|
||||
/// A byte array containing the serialized terrain data of the specified acre.
|
||||
/// The array length is determined by the terrain tile size and the view count.
|
||||
/// </returns>
|
||||
public byte[] DumpAcre(int relX, int relY)
|
||||
{
|
||||
int count = TileInfo.ViewCount;
|
||||
var result = new byte[TerrainTile.SIZE * count];
|
||||
DumpAcre(result, relX, relY);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the serialized data for an acre of tiles, starting at the specified relative coordinates, into the provided buffer.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If a tile at the specified coordinates is out of range, a default value is written for that tile.
|
||||
/// The data is written in column-major order, with each tile's bytes written sequentially into the buffer.
|
||||
/// </remarks>
|
||||
/// <param name="result">
|
||||
/// The buffer that receives the serialized tile data.
|
||||
/// Must be large enough to hold the data for the entire acre.
|
||||
/// </param>
|
||||
/// <param name="relX">The relative X-coordinate of the top-left tile of the acre to dump.</param>
|
||||
/// <param name="relY">The relative Y-coordinate of the top-left tile of the acre to dump.</param>
|
||||
public void DumpAcre(Span<byte> result, int relX, int relY)
|
||||
{
|
||||
// Store columns first. If the x,y is out of range, store default.
|
||||
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 terrain tile data into the layer at the specified relative coordinates.
|
||||
/// </summary>
|
||||
/// <param name="data">
|
||||
/// A read-only span of bytes containing the serialized terrain tile data to import.
|
||||
/// The data must be in the format expected by TerrainTile.GetArray.
|
||||
/// </param>
|
||||
/// <param name="relX">The X-coordinate, relative to the layer origin, where the imported tiles will be placed.</param>
|
||||
/// <param name="relY">The Y-coordinate, relative to the layer origin, where the imported tiles will be placed.</param>
|
||||
/// <returns>The number of tiles successfully imported. Returns 0 if no tiles were imported.</returns>
|
||||
public int ImportAcre(ReadOnlySpan<byte> data, int relX, int relY)
|
||||
{
|
||||
int count = 0;
|
||||
int i = 0;
|
||||
var tiles = TerrainTile.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;
|
||||
SetTileSafe(tileX, tileY, tiles[i++]);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all tiles to the specified tile.
|
||||
/// </summary>
|
||||
/// <param name="tile"></param>
|
||||
/// <param name="interiorOnly">If true, only sets the interior tiles, skipping the outermost ring of beach/rock acres.</param>
|
||||
public void SetAll(TerrainTile tile, in bool interiorOnly = true)
|
||||
{
|
||||
if (interiorOnly)
|
||||
{
|
||||
// skip outermost ring of tiles
|
||||
int xmin = TileInfo.ViewWidth;
|
||||
int ymin = TileInfo.ViewHeight;
|
||||
int xmax = TileInfo.TotalWidth - TileInfo.ViewWidth;
|
||||
int ymax = TileInfo.TotalHeight - TileInfo.ViewHeight;
|
||||
for (int x = xmin; x < xmax; x++)
|
||||
{
|
||||
for (int y = ymin; y < ymax; y++)
|
||||
GetTile(x, y).CopyFrom(tile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var t in Tiles)
|
||||
t.CopyFrom(tile);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all road tiles (not the terrain itself, but the road atop) to the specified tile.
|
||||
/// </summary>
|
||||
/// <param name="tile">Road tile info to copy from.</param>
|
||||
/// <param name="interiorOnly">If true, only sets the interior tiles, skipping the outermost ring of beach/rock acres.</param>
|
||||
public void SetAllRoad(TerrainTile tile, bool interiorOnly = true)
|
||||
{
|
||||
if (interiorOnly)
|
||||
{
|
||||
// skip outermost ring of tiles
|
||||
int xmin = TileInfo.ViewWidth;
|
||||
int ymin = TileInfo.ViewHeight;
|
||||
int xmax = TileInfo.TotalWidth - TileInfo.ViewWidth;
|
||||
int ymax = TileInfo.TotalHeight - TileInfo.ViewHeight;
|
||||
for (int x = xmin; x < xmax; x++)
|
||||
{
|
||||
for (int y = ymin; y < ymax; y++)
|
||||
GetTile(x, y).CopyRoadFrom(tile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var t in Tiles)
|
||||
t.CopyRoadFrom(tile);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="GetTileColor(ushort,int,int,int,int)"/>
|
||||
public int GetTileColor(int relX, int relY, int insideX, int insideY)
|
||||
{
|
||||
var acre = GetAcreTemplate(relX, relY);
|
||||
return GetTileColor(acre, relX, relY, insideX, insideY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base acre tile color at the specified terrain coordinates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the acre has a predefined appearance, that is used; otherwise, the terrain-based appearance is used.
|
||||
/// </remarks>
|
||||
/// <param name="acre">Base acre underneath the terrain tile.</param>
|
||||
/// <param name="relX">Relative X coordinate in terrain tiles.</param>
|
||||
/// <param name="relY">Relative Y coordinate in terrain tiles.</param>
|
||||
/// <param name="insideX">Inside X coordinate of the terrain tile (16px max).</param>
|
||||
/// <param name="insideY">Inside Y coordinate of the terrain tile (16px max).</param>
|
||||
/// <returns>ARGB color value.</returns>
|
||||
public int GetTileColor(ushort acre, int relX, int relY, int insideX, int insideY)
|
||||
{
|
||||
if (acre != 0) // predefined appearance
|
||||
{
|
||||
var c = AcreTileColor.GetAcreTileColor(acre, relX % 16, relY % 16);
|
||||
if (c != -0x1000000) // transparent
|
||||
return c;
|
||||
}
|
||||
|
||||
// dynamic (terrain-based) appearance
|
||||
var tile = GetTile(relX, relY);
|
||||
return TerrainTileColor.GetTileColor(tile, insideX, insideY).ToArgb();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base acre tile color at the specified terrain coordinates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the acre has a predefined appearance, that is used; otherwise, the terrain-based appearance is used.
|
||||
/// </remarks>
|
||||
/// <param name="acre">Base acre underneath the terrain tile.</param>
|
||||
/// <param name="tile">Terrain tile to render.</param>
|
||||
/// <param name="relX">Relative X coordinate in terrain tiles.</param>
|
||||
/// <param name="relY">Relative Y coordinate in terrain tiles.</param>
|
||||
/// <param name="insideX">Inside X coordinate of the terrain tile (16px max).</param>
|
||||
/// <param name="insideY">Inside Y coordinate of the terrain tile (16px max).</param>
|
||||
/// <returns>ARGB color value.</returns>
|
||||
public int GetTileColor(ushort acre, TerrainTile tile, int relX, int relY, int insideX, int insideY)
|
||||
{
|
||||
// If acre is entirely transparent (interior acre), dynamic (terrain-based) appearance
|
||||
if (acre == 0)
|
||||
return TerrainTileColor.GetTileColor(tile, insideX, insideY).ToArgb();
|
||||
|
||||
// For beaches, a slim edge is customizable (indicative by a transparent value).
|
||||
// Check if pre-defined appearance governs
|
||||
var color = AcreTileColor.GetAcreTileColor(acre, relX % 16, relY % 16);
|
||||
if (color == -0x1000000) // transparent (dynamic)
|
||||
return TerrainTileColor.GetTileColor(tile, insideX, insideY).ToArgb();
|
||||
|
||||
return color; // pre-defined appearance
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base acre template at the specified terrain coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">Relative X coordinate in terrain tiles.</param>
|
||||
/// <param name="relY">Relative Y coordinate in terrain tiles.</param>
|
||||
/// <returns>Base acre underneath the terrain tile.</returns>
|
||||
public ushort GetAcreTemplate(int relX, int relY)
|
||||
{
|
||||
// Acres are 16x16 tiles, and the acre data has a 1-acre deep-sea border around it.
|
||||
var acreX = 1 + (relX / 16);
|
||||
var acreY = 1 + (relY / 16);
|
||||
|
||||
var acreIndex = ((CountAcreWidth + 2) * acreY) + acreX;
|
||||
var span = GetBaseAcreSpan(acreIndex);
|
||||
return ReadUInt16LittleEndian(span);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the base acre template span at the specified index.
|
||||
/// </summary>
|
||||
/// <param name="index">Index of the acre.</param>
|
||||
/// <returns>Span of bytes representing the base acre template.</returns>
|
||||
public Span<byte> GetBaseAcreSpan(int index) => BaseAcres.Span.Slice(index * 2, 2);
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using static System.Buffers.Binary.BinaryPrimitives;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Grid of <see cref="TerrainTile"/>
|
||||
/// </summary>
|
||||
public class TerrainLayer : MapGrid
|
||||
{
|
||||
public TerrainTile[] Tiles { get; init; }
|
||||
public byte[] BaseAcres { get; init; }
|
||||
|
||||
public TerrainLayer(TerrainTile[] tiles, byte[] acres) : base(16, 16, AcreWidth * 16, AcreHeight * 16)
|
||||
{
|
||||
BaseAcres = acres;
|
||||
Tiles = tiles;
|
||||
Debug.Assert(MaxTileCount == tiles.Length);
|
||||
}
|
||||
|
||||
public TerrainTile GetTile(int x, int y) => this[GetTileIndex(x, y)];
|
||||
public TerrainTile GetTile(int acreX, int acreY, int gridX, int gridY) => this[GetTileIndex(acreX, acreY, gridX, gridY)];
|
||||
public TerrainTile GetAcreTile(int acreIndex, int tileIndex) => this[GetAcreTileIndex(acreIndex, tileIndex)];
|
||||
|
||||
public TerrainTile this[int index]
|
||||
{
|
||||
get => Tiles[index];
|
||||
set => Tiles[index] = value;
|
||||
}
|
||||
|
||||
public byte[] DumpAll()
|
||||
{
|
||||
var result = new byte[Tiles.Length * TerrainTile.SIZE];
|
||||
for (int i = 0; i < Tiles.Length; i++)
|
||||
Tiles[i].ToBytesClass().CopyTo(result, i * TerrainTile.SIZE);
|
||||
return result;
|
||||
}
|
||||
|
||||
public byte[] DumpAcre(int acre)
|
||||
{
|
||||
int count = GridTileCount;
|
||||
var result = new byte[TerrainTile.SIZE * count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var tile = GetAcreTile(acre, i);
|
||||
var bytes = tile.ToBytesClass();
|
||||
bytes.CopyTo(result, i * TerrainTile.SIZE);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public void ImportAll(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var tiles = TerrainTile.GetArray(data);
|
||||
for (int i = 0; i < tiles.Length; i++)
|
||||
Tiles[i].CopyFrom(tiles[i]);
|
||||
}
|
||||
|
||||
public void ImportAcre(int acre, ReadOnlySpan<byte> data)
|
||||
{
|
||||
int count = GridTileCount;
|
||||
var tiles = TerrainTile.GetArray(data);
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var tile = GetAcreTile(acre, i);
|
||||
tile.CopyFrom(tiles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAll(TerrainTile tile, in bool interiorOnly)
|
||||
{
|
||||
if (interiorOnly)
|
||||
{
|
||||
// skip outermost ring of tiles
|
||||
int xmin = GridWidth;
|
||||
int ymin = GridHeight;
|
||||
int xmax = MaxWidth - GridWidth;
|
||||
int ymax = MaxHeight - GridHeight;
|
||||
for (int x = xmin; x < xmax; x++)
|
||||
{
|
||||
for (int y = ymin; y < ymax; y++)
|
||||
GetTile(x, y).CopyFrom(tile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var t in Tiles)
|
||||
t.CopyFrom(tile);
|
||||
}
|
||||
}
|
||||
|
||||
public void SetAllRoad(TerrainTile tile, in bool interiorOnly)
|
||||
{
|
||||
if (interiorOnly)
|
||||
{
|
||||
// skip outermost ring of tiles
|
||||
int xmin = GridWidth;
|
||||
int ymin = GridHeight;
|
||||
int xmax = MaxWidth - GridWidth;
|
||||
int ymax = MaxHeight - GridHeight;
|
||||
for (int x = xmin; x < xmax; x++)
|
||||
{
|
||||
for (int y = ymin; y < ymax; y++)
|
||||
GetTile(x, y).CopyRoadFrom(tile);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var t in Tiles)
|
||||
t.CopyRoadFrom(tile);
|
||||
}
|
||||
}
|
||||
|
||||
public void GetBuildingCoordinate(ushort bx, ushort by, int scale, out int x, out int y)
|
||||
{
|
||||
// Although there is terrain in the Top Row and Left Column, no buildings can be placed there.
|
||||
// Adjust the building coordinates down-right by an acre.
|
||||
int buildingShift = GridWidth;
|
||||
x = (int)(((bx / 2f) - buildingShift) * scale);
|
||||
y = (int)(((by / 2f) - buildingShift) * scale);
|
||||
}
|
||||
|
||||
public void GetBuildingRelativeCoordinates(int topX, int topY, int acreScale, ushort bx, ushort by, out int relX, out int relY)
|
||||
{
|
||||
GetBuildingCoordinate(bx, by, acreScale, out var x, out var y);
|
||||
relX = x - (topX * acreScale);
|
||||
relY = y - (topY * acreScale);
|
||||
}
|
||||
|
||||
public bool IsWithinGrid(int acreScale, int relX, int relY)
|
||||
{
|
||||
if ((uint)relX >= GridWidth * acreScale)
|
||||
return false;
|
||||
|
||||
if ((uint)relY >= GridHeight * acreScale)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public int GetTileColor(int x, in int y, int relativeX, int relativeY)
|
||||
{
|
||||
var acre = GetTileAcre(x, y);
|
||||
if (acre != 0)
|
||||
{
|
||||
var c = AcreTileColor.GetAcreTileColor(acre, x % 16, y % 16);
|
||||
if (c != -0x1000000) // transparent
|
||||
return c;
|
||||
}
|
||||
|
||||
var tile = GetTile(x, y);
|
||||
return TerrainTileColor.GetTileColor(tile, relativeX, relativeY).ToArgb();
|
||||
}
|
||||
|
||||
private ushort GetTileAcre(int x, int y)
|
||||
{
|
||||
var acreX = 1 + (x / 16);
|
||||
var acreY = 1 + (y / 16);
|
||||
|
||||
var acreIndex = ((AcreWidth + 2) * acreY) + acreX;
|
||||
var ofs = acreIndex * 2;
|
||||
return ReadUInt16LittleEndian(BaseAcres.AsSpan(ofs));
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Manages the <see cref="Item"/> data for the player's outside overworld.
|
||||
/// </summary>
|
||||
public class FieldItemManager
|
||||
{
|
||||
/// <summary>
|
||||
/// Base layer of items
|
||||
/// </summary>
|
||||
public readonly FieldItemLayer Layer1;
|
||||
|
||||
/// <summary>
|
||||
/// Layer of items that are supported by <see cref="Layer1"/>
|
||||
/// </summary>
|
||||
public readonly FieldItemLayer Layer2;
|
||||
|
||||
/// <summary>
|
||||
/// Reference to the save file that will be updated when <see cref="Save"/> is called.
|
||||
/// </summary>
|
||||
public readonly MainSave SAV;
|
||||
|
||||
public FieldItemManager(MainSave sav)
|
||||
{
|
||||
Layer1 = new FieldItemLayer(sav.GetFieldItemLayer1());
|
||||
Layer2 = new FieldItemLayer(sav.GetFieldItemLayer2());
|
||||
SAV = sav;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores all values for the <see cref="FieldItemManager"/> back to the <see cref="SAV"/>.
|
||||
/// </summary>
|
||||
public void Save()
|
||||
{
|
||||
SAV.SetFieldItemLayer1(Layer1.Tiles);
|
||||
SAV.SetFieldItemLayer2(Layer2.Tiles);
|
||||
|
||||
SetTileActiveFlags(Layer1, SAV.FieldItemFlag1);
|
||||
SetTileActiveFlags(Layer2, SAV.FieldItemFlag2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lists out all coordinates of tiles present in <see cref="Layer2"/> that don't have anything underneath in <see cref="Layer1"/> to support them.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public List<string> GetUnsupportedTiles()
|
||||
{
|
||||
var result = new List<string>();
|
||||
for (int x = 0; x < FieldItemLayer.FieldItemWidth; x++)
|
||||
{
|
||||
for (int y = 0; y < FieldItemLayer.FieldItemHeight; y++)
|
||||
{
|
||||
var tile = Layer2.GetTile(x, y);
|
||||
if (tile.IsNone)
|
||||
continue;
|
||||
|
||||
var support = Layer1.GetTile(x, y);
|
||||
if (!support.IsNone)
|
||||
continue; // dunno how to check if the tile can actually have an item put on top of it...
|
||||
|
||||
result.Add($"{x:000},{y:000}");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void SetTileActiveFlags(ItemLayer tiles, int ofs)
|
||||
{
|
||||
// this doesn't set anything; just diagnostic
|
||||
|
||||
// Although the Tiles are arranged y-column (y-x) based, the 'isActive' flags are arranged x-row (x-y) based.
|
||||
// We can turn the isActive flag off if the item is not a root or the item cannot be animated.
|
||||
for (int x = 0; x < FieldItemLayer.FieldItemWidth; x++)
|
||||
{
|
||||
for (int y = 0; y < FieldItemLayer.FieldItemHeight; y++)
|
||||
{
|
||||
var tile = tiles.GetTile(x, y);
|
||||
var isActive = GetIsActive(ofs, x, y);
|
||||
if (!isActive)
|
||||
continue;
|
||||
|
||||
bool empty = tile.IsNone;
|
||||
if (empty)
|
||||
Debug.WriteLine($"Flag at ({x},{y}) is not a root object.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool GetIsActive(bool baseLayer, int x, int y) => GetIsActive(baseLayer ? SAV.FieldItemFlag1 : SAV.FieldItemFlag2, x, y);
|
||||
public void SetIsActive(bool baseLayer, int x, int y, bool value) => SetIsActive(baseLayer ? SAV.FieldItemFlag1 : SAV.FieldItemFlag2, x, y, value);
|
||||
|
||||
private bool GetIsActive(int ofs, int x, int y) => FlagUtil.GetFlag(SAV.Data, ofs, (y * FieldItemLayer.FieldItemWidth) + x);
|
||||
private void SetIsActive(int ofs, int x, int y, bool value) => FlagUtil.SetFlag(SAV.Data, ofs, (y * FieldItemLayer.FieldItemWidth) + x, value);
|
||||
|
||||
public bool IsOccupied(int x, int y) => !Layer1.GetTile(x, y).IsNone || !Layer2.GetTile(x, y).IsNone;
|
||||
}
|
||||
129
NHSE.Core/Structures/Map/Managers/MapEditor.cs
Normal file
129
NHSE.Core/Structures/Map/Managers/MapEditor.cs
Normal file
@@ -0,0 +1,129 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed class MapEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Master interactor for mutating the map.
|
||||
/// </summary>
|
||||
public required MapMutator Mutator { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount of pixel upscaling compared to a 1px = 1 tile map.
|
||||
/// </summary>
|
||||
public int MapScale { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of pixel upscaling compared to a 1px = 1 tile map.
|
||||
/// </summary>
|
||||
public int ViewScale { get; set; } = 16;
|
||||
|
||||
/// <summary>
|
||||
/// Converts an upscaled coordinate to a tile coordinate.
|
||||
/// </summary>
|
||||
/// <param name="mX">X coordinate (mouse on upscaled image).</param>
|
||||
/// <param name="mY">Y coordinate (mouse on upscaled image).</param>
|
||||
public (int X, int Y) GetCursorCoordinates(in int mX, in int mY)
|
||||
{
|
||||
var x = mX / MapScale;
|
||||
var y = mY / MapScale;
|
||||
return (x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new instance of the MapEditor class initialized from the specified save file.
|
||||
/// </summary>
|
||||
/// <param name="sav">The save file containing the data used to initialize the MapEditor. Cannot be null.</param>
|
||||
/// <returns>A MapEditor instance populated with data from the provided save file.</returns>
|
||||
public static MapEditor FromSaveFile(MainSave sav)
|
||||
=> new() { Mutator = MapMutator.FromSaveFile(sav) };
|
||||
|
||||
|
||||
public int X => Mutator.View.X;
|
||||
public int Y => Mutator.View.Y;
|
||||
public LayerTerrain Terrain => Mutator.Manager.LayerTerrain;
|
||||
public ILayerBuilding Buildings => Mutator.Manager.LayerBuildings;
|
||||
public ILayerFieldItemSet Items => Mutator.Manager.FieldItems;
|
||||
|
||||
/// <summary>
|
||||
/// Converts building map coordinates to view pixel coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">Building map X coordinate.</param>
|
||||
/// <param name="relY">Building map Y coordinate.</param>
|
||||
/// <returns>View coordinates.</returns>
|
||||
public (int X, int Y) GetViewCoordinatesBuilding(uint relX, uint relY)
|
||||
=> GetViewCoordinates((int)relX, (int)relY, Mutator.Manager.ConfigBuildings);
|
||||
|
||||
public (int X, int Y) GetViewCoordinatesTerrain(int relX, int relY)
|
||||
=> GetViewCoordinates(relX, relY, Mutator.Manager.ConfigTerrain);
|
||||
|
||||
public (int X, int Y) GetViewCoordinatesFieldItem(int relX, int relY)
|
||||
=> GetViewCoordinates(relX, relY, Mutator.Manager.ConfigItems);
|
||||
|
||||
public (int X, int Y) GetViewCoordinates(int posX, int posY, LayerPositionConfig shifter)
|
||||
{
|
||||
// Get absolute coordinates from the layer.
|
||||
var (x, y) = shifter.GetCoordinatesAbsolute(posX, posY);
|
||||
|
||||
// Shift to view coordinates
|
||||
x -= X;
|
||||
y -= Y;
|
||||
// Scale to pixel coordinates
|
||||
x *= ViewScale;
|
||||
y *= ViewScale;
|
||||
return (x, y);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// From a map pixel coordinate (<see cref="MapScale"/>), get the clamped map tile coordinate.
|
||||
/// </summary>
|
||||
/// <param name="x">Upscaled Map pixel X coordinate.</param>
|
||||
/// <param name="y">Upscaled Map pixel Y coordinate.</param>
|
||||
/// <param name="type">Option to adjust the coordinates to a desired type.</param>
|
||||
/// <returns></returns>
|
||||
public (int X, int Y) GetMapCoordinates(int x, int y, MapViewCoordinateRequest type)
|
||||
{
|
||||
x /= MapScale;
|
||||
y /= MapScale;
|
||||
|
||||
if (x < 0) x = 0;
|
||||
if (y < 0) y = 0;
|
||||
|
||||
// Adjust the view coordinate
|
||||
if (type == MapViewCoordinateRequest.Centered)
|
||||
{
|
||||
// Reticle size is GridWidth, center = /2
|
||||
var shift = (LayerFieldItem.TilesPerAcreDim * MapScale) / 2;
|
||||
x -= shift;
|
||||
y -= shift;
|
||||
}
|
||||
else if (type == MapViewCoordinateRequest.SnapAcre)
|
||||
{
|
||||
// Snap to the nearest acre
|
||||
x -= x % LayerFieldItem.TilesPerAcreDim;
|
||||
y -= y % LayerFieldItem.TilesPerAcreDim;
|
||||
}
|
||||
|
||||
var view = Mutator.View;
|
||||
// Clamp to viewport dimensions, and center to nearest acre if desired.
|
||||
// Clamp to boundaries so that we always have a full grid to view.
|
||||
return view.EnforceEdgeBuffer(x, y);
|
||||
}
|
||||
}
|
||||
|
||||
public enum MapViewCoordinateRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// No adjustment to the coordinates.
|
||||
/// </summary>
|
||||
None = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Snap the coordinates to the nearest acre boundary.
|
||||
/// </summary>
|
||||
SnapAcre,
|
||||
|
||||
/// <summary>
|
||||
/// Center the view around the requested (x,y).
|
||||
/// </summary>
|
||||
Centered,
|
||||
}
|
||||
16
NHSE.Core/Structures/Map/Managers/MapManager.cs
Normal file
16
NHSE.Core/Structures/Map/Managers/MapManager.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed class MapTileManager
|
||||
{
|
||||
public required LayerPositionConfig ConfigTerrain { get; init; }
|
||||
public required LayerPositionConfig ConfigItems { get; init; }
|
||||
public required LayerPositionConfig ConfigBuildings { get; init; }
|
||||
|
||||
public required ILayerFieldItemSet FieldItems { get; init; }
|
||||
public required ILayerFieldItemFlag LayerItemFlag0 { get; init; }
|
||||
public required ILayerFieldItemFlag LayerItemFlag1 { get; init; }
|
||||
public required ILayerBuilding LayerBuildings { get; init; }
|
||||
|
||||
public required MapStatePlaza Plaza { get; init; }
|
||||
public required LayerTerrain LayerTerrain { get; set; }
|
||||
}
|
||||
98
NHSE.Core/Structures/Map/Managers/MapMutator.cs
Normal file
98
NHSE.Core/Structures/Map/Managers/MapMutator.cs
Normal file
@@ -0,0 +1,98 @@
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed record MapMutator
|
||||
{
|
||||
public MapViewState View { get; init; } = new();
|
||||
public required MapTileManager Manager { get; init; }
|
||||
|
||||
// Mutability State Tracking
|
||||
public uint ItemLayerIndex { get; set => field = value & 1; }
|
||||
|
||||
public LayerFieldItem CurrentLayer => ItemLayerIndex == 0 ? Manager.FieldItems.Layer0 : Manager.FieldItems.Layer1;
|
||||
|
||||
/// <inheritdoc cref="ModifyFieldItems(Func{int,int,int,int,int},in bool,LayerFieldItem)"/>
|
||||
public int ModifyFieldItems(Func<int, int, int, int, int> action, in bool wholeMap)
|
||||
=> ModifyFieldItems(action, wholeMap, CurrentLayer);
|
||||
|
||||
/// <inheritdoc cref="ReplaceFieldItems(Item,Item,bool,LayerFieldItem)"/>
|
||||
public int ReplaceFieldItems(Item oldItem, Item newItem, in bool wholeMap)
|
||||
=> ReplaceFieldItems(oldItem, newItem, wholeMap, CurrentLayer);
|
||||
|
||||
/// <summary>
|
||||
/// Modifies field items in the specified <paramref name="layerField"/> using the provided <paramref name="action"/> function.
|
||||
/// </summary>
|
||||
/// <param name="action">Range selector (xmin, ymin, width, height) to use.</param>
|
||||
/// <param name="wholeMap">If true, the modification is applied across the entire map; otherwise, only within the current view.</param>
|
||||
/// <param name="layerField">The layer field item to perform the modification on.</param>
|
||||
/// <returns>The number of items modified.</returns>
|
||||
public int ModifyFieldItems(Func<int, int, int, int, int> action, in bool wholeMap, LayerFieldItem layerField)
|
||||
{
|
||||
int xMin, yMin, width, height;
|
||||
if (wholeMap)
|
||||
{
|
||||
(xMin, yMin) = (0, 0);
|
||||
var info = layerField.TileInfo;
|
||||
(width, height) = info.DimTotal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert absolute to relative coordinates
|
||||
(xMin, yMin) = Manager.ConfigItems.GetCoordinatesRelative(View.X, View.Y);
|
||||
if (!Manager.ConfigItems.IsCoordinateValidRelative(xMin, yMin))
|
||||
return 0;
|
||||
|
||||
var info = layerField.TileInfo;
|
||||
(width, height) = info.DimAcre;
|
||||
}
|
||||
return action(xMin, yMin, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Replaces all instances of <paramref name="oldItem"/> with <paramref name="newItem"/> in the specified <paramref name="layerField"/>.
|
||||
/// </summary>
|
||||
/// <param name="oldItem">Item to be replaced.</param>
|
||||
/// <param name="newItem">Item to replace with.</param>
|
||||
/// <param name="wholeMap">If true, the replacement is done across the entire map; otherwise, only within the current view.</param>
|
||||
/// <param name="layerField">The layer field item to perform the replacement on.</param>
|
||||
/// <returns>The number of items replaced.</returns>
|
||||
private int ReplaceFieldItems(Item oldItem, Item newItem, bool wholeMap, LayerFieldItem layerField)
|
||||
{
|
||||
int xMin, yMin, width, height;
|
||||
if (wholeMap)
|
||||
{
|
||||
(xMin, yMin) = (0, 0);
|
||||
var info = layerField.TileInfo;
|
||||
(width, height) = info.DimTotal;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Convert absolute to relative coordinates
|
||||
(xMin, yMin) = Manager.ConfigItems.GetCoordinatesRelative(View.X, View.Y);
|
||||
if (!Manager.ConfigItems.IsCoordinateValidRelative(xMin, yMin))
|
||||
return 0;
|
||||
|
||||
var info = layerField.TileInfo;
|
||||
(width, height) = info.DimAcre;
|
||||
}
|
||||
|
||||
return layerField.ReplaceAll(oldItem, newItem, xMin, yMin, width, height);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="MapMutator"/> from the provided <see cref="MainSave"/> file.
|
||||
/// </summary>
|
||||
/// <param name="sav">The save file containing the data used to initialize the MapMutator. Cannot be null.</param>
|
||||
/// <returns>A MapMutator instance populated with data from the provided save file.</returns>
|
||||
public static MapMutator FromSaveFile(MainSave sav)
|
||||
=> new() { Manager = MapTileManagerUtil.FromSaveFile(sav) };
|
||||
|
||||
/// <summary>
|
||||
/// Creates a separate view-mutator with shared map objects.
|
||||
/// </summary>
|
||||
public MapMutator CreateCopy() => this with
|
||||
{
|
||||
View = View with { },
|
||||
};
|
||||
}
|
||||
18
NHSE.Core/Structures/Map/Managers/MapStatePlaza.cs
Normal file
18
NHSE.Core/Structures/Map/Managers/MapStatePlaza.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Value storage for plaza position in the map.
|
||||
/// </summary>
|
||||
public sealed class MapStatePlaza
|
||||
{
|
||||
/// <summary>
|
||||
/// Plaza Position X coordinate.
|
||||
/// </summary>
|
||||
|
||||
public required uint X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Plaza Position Z coordinate.
|
||||
/// </summary>
|
||||
public required uint Z { get; set; }
|
||||
}
|
||||
42
NHSE.Core/Structures/Map/Managers/MapTileManagerUtil.cs
Normal file
42
NHSE.Core/Structures/Map/Managers/MapTileManagerUtil.cs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
public static class MapTileManagerUtil
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a <see cref="MapTileManager"/> from the provided <see cref="MainSave"/>.
|
||||
/// </summary>
|
||||
public static MapTileManager FromSaveFile(MainSave sav) => new()
|
||||
{
|
||||
ConfigTerrain = LayerPositionConfig.Create(7, 6, 16, 1, 1, 1),
|
||||
ConfigBuildings = LayerPositionConfig.Create(9, 7, 16, 1, 0, 0),
|
||||
ConfigItems = LayerPositionConfig.Create(sav.FieldItemAcreWidth, sav.FieldItemAcreHeight, 32, 1,
|
||||
(byte)((9 - sav.FieldItemAcreWidth) / 2), 1),
|
||||
|
||||
LayerTerrain = new LayerTerrain(sav.GetTerrainTiles(), sav.GetAcreBytes()),
|
||||
LayerBuildings = new LayerBuilding { Buildings = sav.Buildings },
|
||||
FieldItems = new LayerFieldItemSet
|
||||
{
|
||||
Layer0 = new LayerFieldItem(sav.GetFieldItemLayer0(), sav.FieldItemAcreWidth, sav.FieldItemAcreHeight),
|
||||
Layer1 = new LayerFieldItem(sav.GetFieldItemLayer1(), sav.FieldItemAcreWidth, sav.FieldItemAcreHeight),
|
||||
},
|
||||
LayerItemFlag0 = new LayerFieldItemFlag(sav.FieldItemFlag0Data, sav.FieldItemAcreWidth, sav.FieldItemAcreHeight),
|
||||
LayerItemFlag1 = new LayerFieldItemFlag(sav.FieldItemFlag1Data, sav.FieldItemAcreWidth, sav.FieldItemAcreHeight),
|
||||
Plaza = new MapStatePlaza { X = sav.EventPlazaLeftUpX, Z = sav.EventPlazaLeftUpZ },
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Sets the values from the provided <see cref="MapTileManager"/> into the provided <see cref="MainSave"/>.
|
||||
/// </summary>
|
||||
public static void SetManager(this MapTileManager mgr, MainSave sav)
|
||||
{
|
||||
sav.Buildings = mgr.LayerBuildings.Buildings;
|
||||
sav.SetTerrainTiles(mgr.LayerTerrain.Tiles);
|
||||
sav.SetFieldItemLayer0(mgr.FieldItems.Layer0.Tiles);
|
||||
sav.SetFieldItemLayer1(mgr.FieldItems.Layer1.Tiles);
|
||||
mgr.LayerItemFlag0.Save(sav.FieldItemFlag0Data.Span);
|
||||
mgr.LayerItemFlag1.Save(sav.FieldItemFlag1Data.Span);
|
||||
sav.SetAcreBytes(mgr.LayerTerrain.BaseAcres.Span);
|
||||
sav.EventPlazaLeftUpX = mgr.Plaza.X;
|
||||
sav.EventPlazaLeftUpZ = mgr.Plaza.Z;
|
||||
}
|
||||
}
|
||||
154
NHSE.Core/Structures/Map/Managers/MapViewState.cs
Normal file
154
NHSE.Core/Structures/Map/Managers/MapViewState.cs
Normal file
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public sealed record MapViewState
|
||||
{
|
||||
private const int MaxX = LayerFieldItem.TilesPerAcreDim * AcreCoordinate.CountAcreExteriorWidth;
|
||||
private const int MaxY = LayerFieldItem.TilesPerAcreDim * AcreCoordinate.CountAcreExteriorHeight;
|
||||
private const int EdgeBuffer = LayerFieldItem.TilesPerAcreDim;
|
||||
|
||||
/// <summary>
|
||||
/// Amount the X/Y coordinates change when using arrow movement.
|
||||
/// </summary>
|
||||
[Range(1, 8)] public uint ArrowViewInterval { get; set; } = 2;
|
||||
|
||||
[Range(0, 1)] public int ItemLayerIndex
|
||||
{
|
||||
get;
|
||||
set
|
||||
{
|
||||
if (value is not (0 or 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(value), "Item layer index must be 0 or 1.");
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Top-left-origin X coordinate of the view.
|
||||
/// </summary>
|
||||
public int X
|
||||
{
|
||||
get;
|
||||
private set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)value, (uint)MaxX);
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Top-left-origin Y coordinate of the view.
|
||||
/// </summary>
|
||||
public int Y
|
||||
{
|
||||
get;
|
||||
private set
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)value, (uint)MaxY);
|
||||
field = value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool CanUp => Y != 0;
|
||||
public bool CanDown => Y != MaxY;
|
||||
public bool CanLeft => X != 0;
|
||||
public bool CanRight => X != MaxX;
|
||||
|
||||
/// <summary>
|
||||
/// Moves the view up by <see cref="ArrowViewInterval"/> tiles.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ArrowUp()
|
||||
{
|
||||
if (!CanUp)
|
||||
return false;
|
||||
Y = Math.Max(0, Y - (int)ArrowViewInterval);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the view left by <see cref="ArrowViewInterval"/> tiles.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ArrowLeft()
|
||||
{
|
||||
if (!CanLeft)
|
||||
return false;
|
||||
X = Math.Max(0, X - (int)ArrowViewInterval);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the view right by <see cref="ArrowViewInterval"/> tiles.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ArrowRight()
|
||||
{
|
||||
if (!CanRight)
|
||||
return false;
|
||||
X = Math.Min(MaxX - EdgeBuffer, X + (int)ArrowViewInterval);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Moves the view down by <see cref="ArrowViewInterval"/> tiles.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool ArrowDown()
|
||||
{
|
||||
if (!CanDown)
|
||||
return false;
|
||||
Y = Math.Min(MaxY - EdgeBuffer, Y + (int)ArrowViewInterval);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the requested coordinates (sanity checked).
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool SetViewTo(in int absX, in int absY)
|
||||
{
|
||||
var (x, y) = (X, Y);
|
||||
X = Math.Clamp(absX, 0, MaxX - EdgeBuffer);
|
||||
Y = Math.Clamp(absY, 0, MaxY - EdgeBuffer);
|
||||
return x != X || y != Y;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drags the view by the specified delta amounts.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the view changed; otherwise, <see langword="false"/>.</returns>
|
||||
public bool DragView(int dX, int dY) => SetViewTo(X + dX, Y + dY);
|
||||
|
||||
/// <summary>
|
||||
/// Sets the view to the top-left of the specified acre.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Acres are ordered Y-down-first.
|
||||
/// </remarks>
|
||||
/// <param name="acre">Acre index to set the view to.</param>
|
||||
public void SetViewToAcre(int acre)
|
||||
{
|
||||
var acreX = acre % (MaxX / EdgeBuffer);
|
||||
var acreY = acre / (MaxX / EdgeBuffer);
|
||||
SetViewTo(acreX * EdgeBuffer, acreY * EdgeBuffer);
|
||||
}
|
||||
|
||||
public (int X, int Y) EnforceEdgeBuffer(int x, int y)
|
||||
{
|
||||
x = Math.Clamp(x, 0, MaxX - EdgeBuffer);
|
||||
y = Math.Clamp(y, 0, MaxY - EdgeBuffer);
|
||||
return (x, y);
|
||||
}
|
||||
|
||||
public bool IsWithinView(int x, int y, int tileStride)
|
||||
{
|
||||
if (x < X || x >= X + tileStride)
|
||||
return false;
|
||||
if (y < Y || y >= Y + tileStride)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
public enum RoomLayerSurface
|
||||
public enum RoomLayerSurface : byte
|
||||
{
|
||||
Floor = 0,
|
||||
FloorSupported = 1,
|
||||
|
||||
@@ -4,14 +4,18 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class RoomItemManager
|
||||
public sealed class RoomManager
|
||||
{
|
||||
public readonly RoomItemLayer[] Layers;
|
||||
public readonly LayerRoomItem[] Layers;
|
||||
|
||||
public readonly IPlayerRoom Room;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="RoomLayerSurface"/>
|
||||
/// </summary>
|
||||
private const int LayerCount = 8;
|
||||
|
||||
public RoomItemManager(IPlayerRoom room)
|
||||
public RoomManager(IPlayerRoom room)
|
||||
{
|
||||
Layers = room.GetItemLayers();
|
||||
Room = room;
|
||||
@@ -20,14 +24,14 @@ public RoomItemManager(IPlayerRoom room)
|
||||
|
||||
public void Save() => Room.SetItemLayers(Layers);
|
||||
|
||||
public bool IsOccupied(int layer, int x, int y)
|
||||
public bool IsOccupied(RoomLayerSurface layer, int x, int y)
|
||||
{
|
||||
if ((uint)layer >= LayerCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(layer));
|
||||
|
||||
var l = Layers[layer];
|
||||
var l = Layers[(int)layer];
|
||||
var tile = l.GetTile(x, y);
|
||||
return !tile.IsNone || (layer == (int)RoomLayerSurface.FloorSupported && IsOccupied((int)RoomLayerSurface.Floor, x, y));
|
||||
return !tile.IsNone || (layer == RoomLayerSurface.FloorSupported && IsOccupied(RoomLayerSurface.Floor, x, y));
|
||||
}
|
||||
|
||||
public List<string> GetUnsupportedTiles()
|
||||
@@ -35,9 +39,11 @@ public List<string> GetUnsupportedTiles()
|
||||
var lBase = Layers[(int)RoomLayerSurface.Floor];
|
||||
var lSupport = Layers[(int)RoomLayerSurface.FloorSupported];
|
||||
var result = new List<string>();
|
||||
for (int x = 0; x < lBase.MaxWidth; x++)
|
||||
|
||||
var (width, height) = lBase.TileInfo.DimTotal;
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
for (int y = 0; y < lBase.MaxHeight; y++)
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
var tile = lSupport.GetTile(x, y);
|
||||
if (tile.IsNone)
|
||||
@@ -1,41 +0,0 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Basic logic implementation for interacting with the manipulatable map grid.
|
||||
/// </summary>
|
||||
public abstract class MapGrid : TileGrid
|
||||
{
|
||||
public static readonly AcreCoordinate[] Acres = AcreCoordinate.GetGrid(AcreWidth, AcreHeight);
|
||||
|
||||
public const int AcreWidth = 7;
|
||||
public const int AcreHeight = 6;
|
||||
public const int AcreCount = AcreWidth * AcreHeight;
|
||||
|
||||
public const int MapTileCount16x16 = 16 * 16 * AcreCount;
|
||||
public const int MapTileCount32x32 = 32 * 32 * AcreCount;
|
||||
|
||||
protected MapGrid(int gw, int gh, int mw, int mh) : base(gw, gh, mw, mh) { }
|
||||
|
||||
protected int GetTileIndex(int acreX, int acreY, int gridX, int gridY)
|
||||
{
|
||||
var x = (acreX * GridWidth) + gridX;
|
||||
var y = (acreY * GridHeight) + gridY;
|
||||
return GetTileIndex(x, y);
|
||||
}
|
||||
|
||||
protected int GetAcreTileIndex(int acreIndex, int tileIndex)
|
||||
{
|
||||
var acre = Acres[acreIndex];
|
||||
var x = (tileIndex % GridWidth);
|
||||
var y = (tileIndex / GridHeight);
|
||||
return GetTileIndex(acre.X, acre.Y, x, y);
|
||||
}
|
||||
|
||||
public int GetAcre(int x, int y) => (x / GridWidth) + ((y / GridHeight) * AcreWidth);
|
||||
|
||||
public void GetViewAnchorCoordinates(int acre, out int x, out int y)
|
||||
{
|
||||
x = (acre % AcreWidth) * GridWidth;
|
||||
y = (acre / AcreWidth) * GridHeight;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace NHSE.Core;
|
||||
namespace NHSE.Core;
|
||||
|
||||
public enum OutsideAcre : ushort
|
||||
{
|
||||
@@ -223,38 +220,4 @@ public enum OutsideAcre : ushort
|
||||
FldOutEGarden00 = 315,
|
||||
FldOutNGardenLFront00 = 316,
|
||||
FldOutNGardenRFront00 = 317,
|
||||
}
|
||||
|
||||
public static class CollisionUtil
|
||||
{
|
||||
public static readonly Dictionary<byte, Color> Dict = new()
|
||||
{
|
||||
{00, Color.FromArgb( 70, 120, 64)}, // Grass
|
||||
{01, Color.FromArgb(128, 215, 195)}, // River
|
||||
{03, Color.FromArgb(192, 192, 192)}, // Stone
|
||||
{04, Color.FromArgb(240, 230, 170)}, // Sand
|
||||
{05, Color.FromArgb(128, 215, 195)}, // Sea
|
||||
{06, Color.FromArgb(255, 128, 128)}, // Wood
|
||||
{07, Color.FromArgb(0 , 0, 0)}, // Null
|
||||
{08, Color.FromArgb(32 , 32, 32)}, // Building
|
||||
{09, Color.FromArgb(255, 0, 0)}, // ??
|
||||
{10, Color.FromArgb(48 , 48, 48)}, // Door
|
||||
{12, Color.FromArgb(128, 215, 195)}, // Water at mouths of river
|
||||
{15, Color.FromArgb(128, 215, 195)}, // Strip of water between river mouth and river
|
||||
{22, Color.FromArgb(190, 98, 98)}, // Wood (thin)
|
||||
{28, Color.FromArgb(255, 0, 0)}, // ?? this one isn't even in ColGroundAttributeParam...
|
||||
{29, Color.FromArgb(232, 222, 162)}, // Edge of beach, next to sea
|
||||
{41, Color.FromArgb(118, 122, 132)}, // Rocks at top of map
|
||||
{42, Color.FromArgb(128, 133, 147)}, // Taller regions, rocks at top of map
|
||||
{43, Color.Cyan}, // Tide pool
|
||||
{44, Color.FromArgb( 62, 112, 56)}, // Edge connecting grass and beach
|
||||
{45, Color.FromArgb(118, 122, 132)}, // Some kind of rock
|
||||
{46, Color.FromArgb(120, 207, 187)}, // Edge of sea, next to beach
|
||||
{47, Color.FromArgb(128, 128, 0)}, // Sandstone
|
||||
{49, Color.FromArgb(190, 98, 98)}, // Pier
|
||||
{51, Color.FromArgb(32 , 152, 32)}, // "Grass-growing building"??
|
||||
{70, Color.FromArgb(109, 113, 124)}, // Kapp'n's island rock
|
||||
{149, Color.FromArgb(179, 207, 252)}, // Ice (traversable)
|
||||
{150, Color.FromArgb(61 , 119, 212)}, // Ice (tall, with collision)
|
||||
};
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Navigation metadata for acre coordinates.
|
||||
/// </summary>
|
||||
public class AcreCoordinate
|
||||
{
|
||||
public readonly string Name;
|
||||
public readonly int X, Y;
|
||||
|
||||
public AcreCoordinate(int x, int y) : this((char)('0' + x), (char)('A' + y), x, y) { }
|
||||
|
||||
public AcreCoordinate(char xName, char yName, int x, int y)
|
||||
{
|
||||
Name = $"{yName}{xName}";
|
||||
X = x;
|
||||
Y = y;
|
||||
}
|
||||
|
||||
public static AcreCoordinate[] GetGrid(int width, int height)
|
||||
{
|
||||
var result = new AcreCoordinate[width * height];
|
||||
int i = 0;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
result[i++] = new AcreCoordinate(x, y);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static AcreCoordinate[] GetGridWithExterior(int width, int height)
|
||||
{
|
||||
var result = new AcreCoordinate[width * height];
|
||||
int i = 0;
|
||||
for (int y = 0; y < height; y++)
|
||||
{
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
var xn = (x == 0) ? '<' : x == width - 1 ? '>' : (char)('0' + x - 1);
|
||||
var yn = (y == 0) ? '^' : y == height - 1 ? 'V' : (char)('A' + y - 1);
|
||||
result[i++] = new AcreCoordinate(xn, yn, x, y);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -3,20 +3,20 @@
|
||||
/// <summary>
|
||||
/// Flagging various issues when trying to place an item.
|
||||
/// </summary>
|
||||
public enum PlacedItemPermission
|
||||
public enum PlacedItemPermission : byte
|
||||
{
|
||||
/// <summary>
|
||||
/// Item does not have any of its tiles overlapping with any other items.
|
||||
/// </summary>
|
||||
NoCollision,
|
||||
NoCollision = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Item tiles are overlapping with another item.
|
||||
/// </summary>
|
||||
Collision,
|
||||
Collision = 1,
|
||||
|
||||
/// <summary>
|
||||
/// Item tiles would overflow out-of-bounds.
|
||||
/// </summary>
|
||||
OutOfBounds,
|
||||
OutOfBounds = 2,
|
||||
}
|
||||
@@ -9,7 +9,7 @@ namespace NHSE.Core;
|
||||
/// Represents a Terraform-able terrain tile.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public class TerrainTile
|
||||
public sealed class TerrainTile
|
||||
{
|
||||
// tile[2] (u16 model, u16 variation, u16 angle)
|
||||
// u16 elevation
|
||||
@@ -76,9 +76,9 @@ public void CopyRoadFrom(TerrainTile tile)
|
||||
LandMakingAngleRoad = tile.LandMakingAngleRoad;
|
||||
}
|
||||
|
||||
public bool Rotate() => UnitModelRoad != 0 ? RotateRoad() : RotateTerrain();
|
||||
public bool TryRotate() => UnitModelRoad != 0 ? TryRotateRoad() : TryRotateTerrain();
|
||||
|
||||
private bool RotateTerrain()
|
||||
private bool TryRotateTerrain()
|
||||
{
|
||||
if (UnitModel == TerrainUnitModel.Base)
|
||||
return false;
|
||||
@@ -88,7 +88,7 @@ private bool RotateTerrain()
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool RotateRoad()
|
||||
private bool TryRotateRoad()
|
||||
{
|
||||
var rot = LandMakingAngleRoad;
|
||||
rot = (ushort) ((rot + 1) & 3);
|
||||
|
||||
38
NHSE.Core/Structures/Map/TileCollisionUtil.cs
Normal file
38
NHSE.Core/Structures/Map/TileCollisionUtil.cs
Normal file
@@ -0,0 +1,38 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public static class TileCollisionUtil
|
||||
{
|
||||
public static readonly Dictionary<byte, Color> Dict = new()
|
||||
{
|
||||
{00, Color.FromArgb( 70, 120, 64)}, // Grass
|
||||
{01, Color.FromArgb(128, 215, 195)}, // River
|
||||
{03, Color.FromArgb(192, 192, 192)}, // Stone
|
||||
{04, Color.FromArgb(240, 230, 170)}, // Sand
|
||||
{05, Color.FromArgb(128, 215, 195)}, // Sea
|
||||
{06, Color.FromArgb(255, 128, 128)}, // Wood
|
||||
{07, Color.FromArgb(0 , 0, 0)}, // Null
|
||||
{08, Color.FromArgb(32 , 32, 32)}, // Building
|
||||
{09, Color.FromArgb(255, 0, 0)}, // ??
|
||||
{10, Color.FromArgb(48 , 48, 48)}, // Door
|
||||
{12, Color.FromArgb(128, 215, 195)}, // Water at mouths of river
|
||||
{15, Color.FromArgb(128, 215, 195)}, // Strip of water between river mouth and river
|
||||
{22, Color.FromArgb(190, 98, 98)}, // Wood (thin)
|
||||
{28, Color.FromArgb(255, 0, 0)}, // ?? this one isn't even in ColGroundAttributeParam...
|
||||
{29, Color.FromArgb(232, 222, 162)}, // Edge of beach, next to sea
|
||||
{41, Color.FromArgb(118, 122, 132)}, // Rocks at top of map
|
||||
{42, Color.FromArgb(128, 133, 147)}, // Taller regions, rocks at top of map
|
||||
{43, Color.Cyan}, // Tide pool
|
||||
{44, Color.FromArgb( 62, 112, 56)}, // Edge connecting grass and beach
|
||||
{45, Color.FromArgb(118, 122, 132)}, // Some kind of rock
|
||||
{46, Color.FromArgb(120, 207, 187)}, // Edge of sea, next to beach
|
||||
{47, Color.FromArgb(128, 128, 0)}, // Sandstone
|
||||
{49, Color.FromArgb(190, 98, 98)}, // Pier
|
||||
{51, Color.FromArgb(32 , 152, 32)}, // "Grass-growing building"??
|
||||
{70, Color.FromArgb(109, 113, 124)}, // Kapp'n's island rock
|
||||
{149, Color.FromArgb(179, 207, 252)}, // Ice (traversable)
|
||||
{150, Color.FromArgb(61 , 119, 212)}, // Ice (tall, with collision)
|
||||
};
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Basic logic implementation for interacting with the manipulatable tile grid.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Certain <see cref="TileGrid"/> use this as a viewport on a subsection of the entire tile-set.
|
||||
/// </remarks>
|
||||
public abstract class TileGrid
|
||||
{
|
||||
/// <summary> Amount of viewable tiles wide </summary>
|
||||
public readonly int GridWidth;
|
||||
|
||||
/// <summary> Amount of viewable tiles high </summary>
|
||||
public readonly int GridHeight;
|
||||
|
||||
/// <summary> Max amount of tiles wide the entire grid is </summary>
|
||||
public readonly int MaxWidth;
|
||||
|
||||
/// <summary> Max amount of tiles high the entire grid is </summary>
|
||||
public readonly int MaxHeight;
|
||||
|
||||
protected TileGrid(in int gw, in int gh, in int mw, in int mh)
|
||||
{
|
||||
GridWidth = gw;
|
||||
GridHeight = gh;
|
||||
MaxWidth = mw;
|
||||
MaxHeight = mh;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Amount of tiles present in the grid.
|
||||
/// </summary>
|
||||
public int GridTileCount => GridWidth * GridHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of ALL tiles present in the entire grid (including the grid).
|
||||
/// </summary>
|
||||
public int MaxTileCount => MaxWidth * MaxHeight;
|
||||
|
||||
protected int GetTileIndex(in int x, in int y) => (MaxHeight * x) + y;
|
||||
|
||||
public void ClampCoordinatesInsideGrid(ref int x, ref int y) => ClampCoordinatesTo(ref x, ref y, MaxWidth - 1, MaxHeight - 1);
|
||||
|
||||
public void ClampCoordinatesTopLeft(ref int x, ref int y)
|
||||
{
|
||||
int maxX = MaxWidth - GridWidth;
|
||||
int maxY = MaxHeight - GridHeight;
|
||||
ClampCoordinatesTo(ref x, ref y, maxX, maxY);
|
||||
}
|
||||
|
||||
private static void ClampCoordinatesTo(ref int x, ref int y, int maxX, int maxY)
|
||||
{
|
||||
x = Math.Max(0, Math.Min(x, maxX));
|
||||
y = Math.Max(0, Math.Min(y, maxY));
|
||||
}
|
||||
|
||||
public void GetViewAnchorCoordinates(ref int x, ref int y, in bool centerReticle)
|
||||
{
|
||||
// If we aren't snapping the reticle to the nearest acre
|
||||
// we want to put the middle of the reticle rectangle where the cursor is.
|
||||
// Adjust the view coordinate
|
||||
if (!centerReticle)
|
||||
{
|
||||
// Reticle size is GridWidth, center = /2
|
||||
x -= GridWidth / 2;
|
||||
y -= GridWidth / 2;
|
||||
}
|
||||
|
||||
// Clamp to viewport dimensions, and center to nearest acre if desired.
|
||||
// Clamp to boundaries so that we always have 16x16 to view.
|
||||
ClampCoordinatesTopLeft(ref x, ref y);
|
||||
}
|
||||
}
|
||||
78
NHSE.Core/Structures/Map/TileGridViewport.cs
Normal file
78
NHSE.Core/Structures/Map/TileGridViewport.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Basic configuration of a narrow view for interacting with the larger manipulatable tile grid.
|
||||
/// </summary>
|
||||
/// <param name="ViewWidth">Viewable amount of viewable tiles wide</param>
|
||||
/// <param name="ViewHeight">Viewable amount of viewable tiles high</param>
|
||||
/// <param name="Columns">Columns of view available</param>
|
||||
/// <param name="Rows">Rows of view available</param>
|
||||
public readonly record struct TileGridViewport([ConstantExpected] byte ViewWidth, [ConstantExpected] byte ViewHeight, byte Columns, byte Rows)
|
||||
{
|
||||
/// <summary>
|
||||
/// Total width of the entire grid (including the view).
|
||||
/// </summary>
|
||||
public int TotalWidth => Columns * ViewWidth;
|
||||
|
||||
/// <summary>
|
||||
/// Total height of the entire grid (including the view).
|
||||
/// </summary>
|
||||
public int TotalHeight => Rows * ViewHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of tiles present in the grid.
|
||||
/// </summary>
|
||||
public int ViewCount => ViewWidth * ViewHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of ALL tiles present in the entire grid (including the grid).
|
||||
/// </summary>
|
||||
public int TotalCount => TotalWidth * TotalHeight;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the dimensions of the viewable area (an acre-worth).
|
||||
/// </summary>
|
||||
public (int X, int Y) DimAcre => (ViewWidth, ViewHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the total dimensions of the entire grid (including the view).
|
||||
/// </summary>
|
||||
public (int X, int Y) DimTotal => (TotalWidth, TotalHeight);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the absolute index of the absolute tile in the grid based on the x/y coordinates.
|
||||
/// </summary>
|
||||
/// <param name="relX">Relative X-coordinate of the tile in the grid</param>
|
||||
/// <param name="relY">Relative Y-coordinate of the tile in the grid</param>
|
||||
/// <returns>Absolute index of the tile in the grid</returns>
|
||||
public int GetTileIndex(in int relX, in int relY) => (TotalHeight * relX) + relY;
|
||||
|
||||
/// <summary>
|
||||
/// Clamps the specified relative X and Y coordinates so that they remain within the valid bounds of the area.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method to prevent coordinates from exceeding the valid area,
|
||||
/// which may help avoid out-of-bounds errors when working with grid-based data or images.
|
||||
/// </remarks>
|
||||
/// <param name="relX">The relative X coordinate to clamp.</param>
|
||||
/// <param name="relY">The relative Y coordinate to clamp.</param>
|
||||
public void ClampInside(ref int relX, ref int relY)
|
||||
=> ClampCoordinatesTo(ref relX, ref relY, TotalWidth - 1, TotalHeight - 1);
|
||||
|
||||
private static void ClampCoordinatesTo(ref int relX, ref int relY, int maxX, int maxY)
|
||||
{
|
||||
relX = Math.Clamp(relX, 0, maxX);
|
||||
relY = Math.Clamp(relY, 0, maxY);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified relative coordinates are within the bounds of the area.
|
||||
/// </summary>
|
||||
/// <param name="relX">The horizontal coordinate, relative to the left edge of the area.</param>
|
||||
/// <param name="relY">The vertical coordinate, relative to the top edge of the area.</param>
|
||||
/// <returns><see langword="true"/> if the specified coordinates are within the bounds; otherwise, <see langword="false"/>.</returns>
|
||||
public bool Contains(int relX, int relY) => !((uint)relX >= TotalWidth || (uint)relY >= TotalHeight);
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 4)]
|
||||
public class GSaveFg
|
||||
public sealed class GSaveFg
|
||||
{
|
||||
public const int SIZE = 0x928;
|
||||
private const int _7b9816fbCount = 0x900;
|
||||
|
||||
@@ -13,49 +13,14 @@ public struct GSavePlayerManpu : IReactionStore
|
||||
private const int MaxCount = 64;
|
||||
private const int WheelCount = 8;
|
||||
|
||||
/// <summary>
|
||||
/// List of known Reaction IDs
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, SizeConst = MaxCount)]
|
||||
public Reaction[] ManpuBit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Emotions that are currently bound to the Reaction Wheel.
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, SizeConst = WheelCount)]
|
||||
public Reaction[] UIList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags indicating if a Reaction (at the same index?) is newly learned or not.
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = MaxCount)]
|
||||
public bool[] NewFlag { get; set; }
|
||||
|
||||
public void AddMissingReactions()
|
||||
{
|
||||
var all = Enum.GetValues<Reaction>();
|
||||
foreach (var react in all)
|
||||
AddReaction(react);
|
||||
}
|
||||
|
||||
// returns true if failed
|
||||
public bool AddReaction(Reaction react)
|
||||
{
|
||||
if (react.ToString().StartsWith("UNUSED"))
|
||||
return true;
|
||||
|
||||
var index = Array.IndexOf(ManpuBit, react);
|
||||
if (index >= 0)
|
||||
return false;
|
||||
|
||||
var empty = EmptyIndex;
|
||||
if (empty < 0)
|
||||
return true;
|
||||
ManpuBit[empty] = react;
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly int EmptyIndex => Array.FindIndex(ManpuBit, z => z == 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -68,56 +33,64 @@ public struct GSavePlayerManpu15 : IReactionStore
|
||||
private const int MaxCount = 256; // up from 64
|
||||
private const int WheelCount = 8;
|
||||
|
||||
/// <summary>
|
||||
/// List of known Reaction IDs
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, SizeConst = MaxCount)]
|
||||
public Reaction[] ManpuBit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Emotions that are currently bound to the Reaction Wheel.
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, SizeConst = WheelCount)]
|
||||
public Reaction[] UIList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags indicating if a Reaction (at the same index?) is newly learned or not.
|
||||
/// </summary>
|
||||
[field: MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = MaxCount)]
|
||||
public bool[] NewFlag { get; set; }
|
||||
|
||||
public void AddMissingReactions()
|
||||
{
|
||||
var all = Enum.GetValues<Reaction>();
|
||||
foreach (var react in all)
|
||||
AddReaction(react);
|
||||
}
|
||||
|
||||
// returns true if failed
|
||||
public bool AddReaction(Reaction react)
|
||||
{
|
||||
if (react.ToString().StartsWith("UNUSED"))
|
||||
return true;
|
||||
|
||||
var index = Array.IndexOf(ManpuBit, react);
|
||||
if (index >= 0)
|
||||
return false;
|
||||
|
||||
var empty = EmptyIndex;
|
||||
if (empty < 0)
|
||||
return true;
|
||||
ManpuBit[empty] = react;
|
||||
return false;
|
||||
}
|
||||
|
||||
private readonly int EmptyIndex => Array.FindIndex(ManpuBit, z => z == 0);
|
||||
}
|
||||
|
||||
public interface IReactionStore
|
||||
{
|
||||
/// <summary>
|
||||
/// List of Reaction IDs the player currently knows.
|
||||
/// </summary>
|
||||
Reaction[] ManpuBit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Emotions that are currently bound to the Reaction Wheel.
|
||||
/// </summary>
|
||||
Reaction[] UIList { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Flags indicating if a Reaction (at the same index?) is newly learned or not.
|
||||
/// </summary>
|
||||
bool[] NewFlag { get; set; }
|
||||
bool AddReaction(Reaction react);
|
||||
void AddMissingReactions();
|
||||
|
||||
/// <summary>
|
||||
/// Adds all possible reaction values from <see cref="Reaction"/>'s defined list.
|
||||
/// </summary>
|
||||
void AddMissingReactions()
|
||||
{
|
||||
var all = Enum.GetValues<Reaction>();
|
||||
foreach (var react in all)
|
||||
TryAddReaction(react);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to add the <see cref="react"/> to the list of reactions.
|
||||
/// </summary>
|
||||
/// <param name="react">Reaction to add to list</param>
|
||||
bool TryAddReaction(Reaction react)
|
||||
{
|
||||
if (react.ToString().StartsWith("UNUSED"))
|
||||
return false; // shouldn't add
|
||||
|
||||
if (ManpuBit.Contains(react))
|
||||
return true; // already have
|
||||
|
||||
var empty = EmptyIndex;
|
||||
if (empty < 0)
|
||||
return true; // full? already have
|
||||
ManpuBit[empty] = react;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// First empty index within the array of reactions.
|
||||
/// </summary>
|
||||
int EmptyIndex => ManpuBit.IndexOf(Reaction.None);
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 1)]
|
||||
public class GSaveVisitorNpc
|
||||
public sealed class GSaveVisitorNpc
|
||||
{
|
||||
public const int SIZE = 0x78;
|
||||
private const int Days = 7;
|
||||
@@ -46,7 +46,7 @@ public struct V3f
|
||||
public float Y { get; set; }
|
||||
public float Z { get; set; }
|
||||
|
||||
public override string ToString() => $"({X},{Y},{Z})";
|
||||
public readonly override string ToString() => $"({X},{Y},{Z})";
|
||||
}
|
||||
|
||||
public enum VisitorNPC
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class MapManager : MapTerrainStructure
|
||||
{
|
||||
public readonly FieldItemManager Items;
|
||||
|
||||
public int MapLayer { get; set; } // 0 or 1
|
||||
|
||||
public MapManager(MainSave sav) : base(sav)
|
||||
{
|
||||
Items = new FieldItemManager(sav);
|
||||
}
|
||||
|
||||
public FieldItemLayer CurrentLayer => MapLayer == 0 ? Items.Layer1 : Items.Layer2;
|
||||
|
||||
public static void ClearDesignTiles(MainSave sav)
|
||||
{
|
||||
var tiles = sav.GetMapDesignTiles();
|
||||
for (int i = 0; i < tiles.Length; i++)
|
||||
tiles[i] = MainSave.MapDesignNone;
|
||||
sav.SetMapDesignTiles(tiles);
|
||||
}
|
||||
|
||||
public static byte[] ExportDesignTiles(MainSave sav)
|
||||
{
|
||||
var tiles = sav.GetMapDesignTiles();
|
||||
var result = new byte[tiles.Length * 2];
|
||||
Buffer.BlockCopy(tiles, 0, result, 0, result.Length);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static void ImportDesignTiles(MainSave sav, ReadOnlySpan<byte> result)
|
||||
{
|
||||
var tiles = sav.GetMapDesignTiles();
|
||||
MemoryMarshal.Cast<byte, ushort>(result).CopyTo(tiles);
|
||||
sav.SetMapDesignTiles(tiles);
|
||||
}
|
||||
}
|
||||
|
||||
public class MapTerrainStructure
|
||||
{
|
||||
public readonly TerrainLayer Terrain;
|
||||
public readonly IReadOnlyList<Building> Buildings;
|
||||
|
||||
public uint PlazaX { get; set; }
|
||||
public uint PlazaY { get; set; }
|
||||
|
||||
public MapTerrainStructure(MainSave sav)
|
||||
{
|
||||
Terrain = new TerrainLayer(sav.GetTerrainTiles(), sav.GetAcreBytes());
|
||||
Buildings = sav.Buildings;
|
||||
PlazaX = sav.EventPlazaLeftUpX;
|
||||
PlazaY = sav.EventPlazaLeftUpZ;
|
||||
}
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class MapView
|
||||
{
|
||||
private const int ViewInterval = 2;
|
||||
public readonly MapManager Map;
|
||||
|
||||
public int MapScale { get; } = 1;
|
||||
public int AcreScale { get; }
|
||||
public int TerrainScale => AcreScale * 2;
|
||||
|
||||
// Top Left Anchor Coordinates
|
||||
public int X { get; set; }
|
||||
public int Y { get; set; }
|
||||
|
||||
protected MapView(MapManager m, int scale = 16)
|
||||
{
|
||||
AcreScale = scale;
|
||||
Map = m;
|
||||
}
|
||||
|
||||
public bool CanUp => Y != 0;
|
||||
public bool CanDown => Y < Map.CurrentLayer.MaxHeight - Map.CurrentLayer.GridHeight;
|
||||
public bool CanLeft => X != 0;
|
||||
public bool CanRight => X < Map.CurrentLayer.MaxWidth - Map.CurrentLayer.GridWidth;
|
||||
|
||||
public bool ArrowUp()
|
||||
{
|
||||
if (Y <= 0)
|
||||
return false;
|
||||
Y -= ViewInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ArrowLeft()
|
||||
{
|
||||
if (X <= 0)
|
||||
return false;
|
||||
X -= ViewInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ArrowRight()
|
||||
{
|
||||
if (X >= Map.CurrentLayer.MaxWidth - 2)
|
||||
return false;
|
||||
X += ViewInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool ArrowDown()
|
||||
{
|
||||
if (Y >= Map.CurrentLayer.MaxHeight - ViewInterval)
|
||||
return false;
|
||||
Y += ViewInterval;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SetViewTo(in int x, in int y)
|
||||
{
|
||||
var info = Map.CurrentLayer;
|
||||
var newX = Math.Max(0, Math.Min(x, info.MaxWidth - info.GridWidth));
|
||||
var newY = Math.Max(0, Math.Min(y, info.MaxHeight - info.GridHeight));
|
||||
bool diff = X != newX || Y != newY;
|
||||
X = newX;
|
||||
Y = newY;
|
||||
return diff;
|
||||
}
|
||||
|
||||
public void SetViewToAcre(in int acre)
|
||||
{
|
||||
var layer = Map.Items.Layer1;
|
||||
layer.GetViewAnchorCoordinates(acre, out var x, out var y);
|
||||
SetViewTo(x, y);
|
||||
}
|
||||
|
||||
public int ModifyFieldItems(Func<int, int, int, int, int> action, in bool wholeMap)
|
||||
{
|
||||
var layer = Map.CurrentLayer;
|
||||
return wholeMap
|
||||
? action(0, 0, layer.MaxWidth, layer.MaxHeight)
|
||||
: action(X, Y, layer.GridWidth, layer.GridHeight);
|
||||
}
|
||||
|
||||
public int ReplaceFieldItems(Item oldItem, Item newItem, in bool wholeMap)
|
||||
{
|
||||
var layer = Map.CurrentLayer;
|
||||
return wholeMap
|
||||
? layer.ReplaceAll(oldItem, newItem, 0, 0, layer.MaxWidth, layer.MaxHeight)
|
||||
: layer.ReplaceAll(oldItem, newItem, X, Y, layer.GridWidth, layer.GridHeight);
|
||||
}
|
||||
|
||||
public void GetCursorCoordinates(in int mX, in int mY, out int x, out int y)
|
||||
{
|
||||
x = mX / MapScale;
|
||||
y = mY / MapScale;
|
||||
}
|
||||
|
||||
public void GetViewAnchorCoordinates(int mX, int mY, out int x, out int y, bool centerReticle)
|
||||
{
|
||||
GetCursorCoordinates(mX, mY, out x, out y);
|
||||
var layer = Map.Items.Layer1;
|
||||
layer.GetViewAnchorCoordinates(ref x, ref y, centerReticle);
|
||||
}
|
||||
}
|
||||
@@ -4,15 +4,12 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class Museum
|
||||
public sealed class Museum(Memory<byte> raw)
|
||||
{
|
||||
public const int SIZE = 0x3404;
|
||||
public const int EntryCount = 1024;
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public Museum(Memory<byte> data) => Raw = data;
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public int MuseumLevel
|
||||
{
|
||||
|
||||
@@ -5,27 +5,17 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class MuseumEditor
|
||||
public sealed record MuseumEditor(Museum Museum)
|
||||
{
|
||||
public readonly Museum Museum;
|
||||
public readonly GSaveDate[] Dates;
|
||||
public readonly Item[] Items;
|
||||
public readonly byte[] Players;
|
||||
|
||||
public MuseumEditor(Museum museum)
|
||||
{
|
||||
Museum = museum;
|
||||
Dates = museum.GetDates();
|
||||
Items = museum.GetItems();
|
||||
Players = museum.GetPlayers();
|
||||
}
|
||||
public readonly GSaveDate[] Dates = Museum.GetDates();
|
||||
public readonly Item[] Items = Museum.GetItems();
|
||||
public readonly byte[] Players = Museum.GetPlayers();
|
||||
|
||||
public void Save()
|
||||
{
|
||||
var museum = Museum;
|
||||
museum.SetDates(Dates);
|
||||
museum.SetItems(Items);
|
||||
museum.SetPlayers(Players);
|
||||
Museum.SetDates(Dates);
|
||||
Museum.SetItems(Items);
|
||||
Museum.SetPlayers(Players);
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetDonationSummary(GameStrings str)
|
||||
|
||||
@@ -3,16 +3,14 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class RecipeBook
|
||||
public sealed class RecipeBook(Memory<byte> raw)
|
||||
{
|
||||
private const int BitFlagArraySize = 0x100;
|
||||
private const int BitFlagArrayCount = 4;
|
||||
public const int SIZE = BitFlagArraySize * BitFlagArrayCount;
|
||||
public const ushort RecipeCount = BitFlagArraySize * 8;
|
||||
|
||||
private readonly Memory<byte> Raw;
|
||||
private Span<byte> Data => Raw.Span;
|
||||
public RecipeBook(Memory<byte> raw) => Raw = raw;
|
||||
private Span<byte> Data => raw.Span;
|
||||
|
||||
public void Save(Span<byte> data) => Data.CopyTo(data);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Used for allowing a struct to be mutated in a PropertyGrid.
|
||||
/// </summary>
|
||||
public class ValueTypeTypeConverter : ExpandableObjectConverter
|
||||
public sealed class ValueTypeTypeConverter : ExpandableObjectConverter
|
||||
{
|
||||
public override bool GetCreateInstanceSupported(ITypeDescriptorContext? context) => true;
|
||||
|
||||
|
||||
@@ -6,51 +6,24 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Multi-milestone definition for tracking game-play achievements.
|
||||
/// </summary>
|
||||
public class LifeSupportAchievement : INamedValue
|
||||
public sealed record LifeSupportAchievement(
|
||||
ushort Index,
|
||||
byte AchievementCount,
|
||||
uint Threshold1,
|
||||
uint Threshold2,
|
||||
uint Threshold3,
|
||||
uint Threshold4,
|
||||
uint Threshold5,
|
||||
short FlagLand,
|
||||
short FlagPlayer,
|
||||
string Name)
|
||||
: INamedValue
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of milestones an achievement can have.
|
||||
/// </summary>
|
||||
public const int MilestoneMax = 6;
|
||||
|
||||
public readonly short FlagLand;
|
||||
public readonly short FlagPlayer;
|
||||
|
||||
public ushort Index { get; }
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary> Total number of milestones for this achievement type. </summary>
|
||||
public readonly int AchievementCount;
|
||||
|
||||
/// <summary> First Milestone's Satisfaction Threshold </summary>
|
||||
public readonly uint Threshold1;
|
||||
|
||||
/// <summary> Second Milestone's Satisfaction Threshold </summary>
|
||||
public readonly uint Threshold2;
|
||||
|
||||
/// <summary> Third Milestone's Satisfaction Threshold </summary>
|
||||
public readonly uint Threshold3;
|
||||
|
||||
/// <summary> Fourth Milestone's Satisfaction Threshold </summary>
|
||||
public readonly uint Threshold4;
|
||||
|
||||
/// <summary> Fifth Milestone's Satisfaction Threshold </summary>
|
||||
public readonly uint Threshold5;
|
||||
|
||||
public LifeSupportAchievement(ushort index, byte max, uint t1, uint t2, uint t3, uint t4, uint t5, short land, short player, string name)
|
||||
{
|
||||
Index = index;
|
||||
AchievementCount = max;
|
||||
Threshold1 = t1;
|
||||
Threshold2 = t2;
|
||||
Threshold3 = t3;
|
||||
Threshold4 = t4;
|
||||
Threshold5 = t5;
|
||||
FlagLand = land;
|
||||
FlagPlayer = player;
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public uint MaxThreshold => Math.Max(Threshold1, Math.Max(Threshold2, Math.Max(Threshold3, Math.Max(Threshold4, Threshold5))));
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
namespace NHSE.Core;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = SIZE)]
|
||||
public class TurnipStonk // GSaveShopKabu
|
||||
public sealed class TurnipStonk // GSaveShopKabu
|
||||
{
|
||||
public const int SIZE = 0x44;
|
||||
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class GSaveMemory : IVillagerOrigin
|
||||
public sealed class GSaveMemory(Memory<byte> raw) : IVillagerOrigin
|
||||
{
|
||||
public const int SIZE = 0x5F0;
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public GSaveMemory(Memory<byte> data) => Raw = data;
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public GSavePlayerId PlayerId
|
||||
{
|
||||
|
||||
@@ -8,6 +8,6 @@ public interface IPlayerRoom
|
||||
byte[] Write();
|
||||
|
||||
string Extension { get; }
|
||||
RoomItemLayer[] GetItemLayers();
|
||||
void SetItemLayers(IReadOnlyList<RoomItemLayer> value);
|
||||
LayerRoomItem[] GetItemLayers();
|
||||
void SetItemLayers(IReadOnlyList<LayerRoomItem> value);
|
||||
}
|
||||
@@ -4,15 +4,12 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class PlayerHouse1 : IPlayerHouse
|
||||
public class PlayerHouse1(Memory<byte> raw) : IPlayerHouse
|
||||
{
|
||||
public const int SIZE = 0x26400;
|
||||
public virtual string Extension => "nhph";
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public PlayerHouse1(Memory<byte> data) => Raw = data;
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public byte[] Write() => Data.ToArray();
|
||||
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class PlayerHouse2 : PlayerHouse1
|
||||
public sealed class PlayerHouse2(Memory<byte> raw) : PlayerHouse1(raw)
|
||||
{
|
||||
public new const int SIZE = 0x28A28;
|
||||
public override string Extension => "nhph2";
|
||||
|
||||
public PlayerHouse2(Memory<byte> data) : base(data) { }
|
||||
|
||||
public override IPlayerRoom GetRoom(int roomIndex)
|
||||
{
|
||||
if ((uint)roomIndex >= MaxRoom)
|
||||
|
||||
@@ -3,15 +3,12 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class PlayerRoom1 : IPlayerRoom
|
||||
public class PlayerRoom1(Memory<byte> raw) : IPlayerRoom
|
||||
{
|
||||
public const int SIZE = 0x65C8;
|
||||
public virtual string Extension => "nhpr";
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
|
||||
public PlayerRoom1(Memory<byte> data) => Raw = data;
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public byte[] Write() => Data.ToArray();
|
||||
|
||||
@@ -23,8 +20,8 @@ public class PlayerRoom1 : IPlayerRoom
|
||||
*/
|
||||
|
||||
public const int LayerCount = 8;
|
||||
public RoomItemLayer[] GetItemLayers() => RoomItemLayer.GetArray(Data[..(LayerCount * RoomItemLayer.SIZE)].ToArray());
|
||||
public void SetItemLayers(IReadOnlyList<RoomItemLayer> value) => RoomItemLayer.SetArray(value).AsSpan().CopyTo(Data);
|
||||
public LayerRoomItem[] GetItemLayers() => LayerRoomItem.GetArray(Data[..(LayerCount * LayerRoomItem.SIZE)].ToArray());
|
||||
public void SetItemLayers(IReadOnlyList<LayerRoomItem> value) => LayerRoomItem.SetArray(value).CopyTo(Data);
|
||||
|
||||
public bool GetIsActive(int layer, int x, int y) => FlagUtil.GetFlag(Data, 0x6400 + (layer * 0x34), (y * 20) + x);
|
||||
public void SetIsActive(int layer, int x, int y, bool value = true) => FlagUtil.SetFlag(Data, 0x6400 + (layer * 0x34), (y * 20) + x, value);
|
||||
|
||||
@@ -4,12 +4,10 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class PlayerRoom2 : PlayerRoom1
|
||||
public sealed class PlayerRoom2(Memory<byte> raw) : PlayerRoom1(raw)
|
||||
{
|
||||
public new const int SIZE = 0x6C24;
|
||||
public new virtual string Extension => "nhpr2";
|
||||
|
||||
public PlayerRoom2(Memory<byte> data) : base(data) { }
|
||||
public override string Extension => "nhpr2";
|
||||
|
||||
/*
|
||||
s_665e9093 ExtraEffectLayerList[2]; // @0x65c8 size 0x320, align 2
|
||||
@@ -49,7 +47,7 @@ public GSaveMusicBoxInfo MusicBoxInfo
|
||||
|
||||
// 3 bytes padding
|
||||
|
||||
public s_e13a81f4 _cfb139b9
|
||||
public s_e13a81f4 Unk_cfb139b9
|
||||
{
|
||||
get => Data.Slice(0x6C10, s_e13a81f4.SIZE).ToStructure<s_e13a81f4>();
|
||||
set => value.ToBytes().CopyTo(Data[0x6C10..]);
|
||||
|
||||
@@ -8,14 +8,12 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Villager object format from 1.0 to update 1.4
|
||||
/// </summary>
|
||||
public sealed class Villager1 : IVillager
|
||||
public sealed class Villager1(Memory<byte> raw) : IVillager
|
||||
{
|
||||
public const int SIZE = 0x12AB0;
|
||||
public string Extension => "nhv";
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
public Villager1(Memory<byte> data) => Raw = data;
|
||||
public Span<byte> Data => raw.Span;
|
||||
public byte[] Write() => Data.ToArray();
|
||||
|
||||
public byte Species { get => Data[0]; set => Data[0] = value; }
|
||||
@@ -34,7 +32,7 @@ public GSaveMemory GetMemory(int index)
|
||||
if ((uint) index >= PlayerMemoryCount)
|
||||
throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
var bytes = Raw.Slice(0x4 + (index * GSaveMemory.SIZE), GSaveMemory.SIZE);
|
||||
var bytes = raw.Slice(0x4 + (index * GSaveMemory.SIZE), GSaveMemory.SIZE);
|
||||
return new GSaveMemory(bytes);
|
||||
}
|
||||
|
||||
@@ -115,7 +113,7 @@ public GSaveRoomFloorWall Room
|
||||
|
||||
public DesignPatternPRO Design
|
||||
{
|
||||
get => new(Raw.Slice(0x12128, DesignPatternPRO.SIZE));
|
||||
get => new(raw.Slice(0x12128, DesignPatternPRO.SIZE));
|
||||
set => value.Data.CopyTo(Data[0x12128..]);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,13 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class VillagerHouse1 : IVillagerHouse
|
||||
public class VillagerHouse1(Memory<byte> raw) : IVillagerHouse
|
||||
{
|
||||
public const int SIZE = 0x1D4;
|
||||
public const int ItemCount = 36;
|
||||
public virtual string Extension => "nhvh";
|
||||
|
||||
public readonly Memory<byte> Raw;
|
||||
public VillagerHouse1(Memory<byte> raw) => Raw = raw;
|
||||
public Span<byte> Data => Raw.Span;
|
||||
public Span<byte> Data => raw.Span;
|
||||
|
||||
public byte[] Write() => Data.ToArray();
|
||||
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
|
||||
namespace NHSE.Core;
|
||||
|
||||
public class VillagerHouse2 : VillagerHouse1
|
||||
public sealed class VillagerHouse2(Memory<byte> raw) : VillagerHouse1(raw)
|
||||
{
|
||||
public new const int SIZE = 0x12E8;
|
||||
public override string Extension => "nhvh2";
|
||||
|
||||
public VillagerHouse2(Memory<byte> data) : base(data) { }
|
||||
|
||||
// 0x1D4-0x12DB -- 0x1108 sized structure
|
||||
// 0x12DC -- 8 byte item
|
||||
// 0x12E4 -- 1 byte
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace NHSE.Core;
|
||||
/// <summary>
|
||||
/// Key Value pair for a displayed <see cref="T:System.String" /> and underlying <see cref="T:System.Int32" /> value.
|
||||
/// </summary>
|
||||
public record ComboItem(string Text, int Value);
|
||||
public sealed record ComboItem(string Text, int Value);
|
||||
|
||||
public static class ComboItemUtil
|
||||
{
|
||||
@@ -25,10 +25,10 @@ public static List<ComboItem> GetArray(ReadOnlySpan<string> items)
|
||||
return result;
|
||||
}
|
||||
|
||||
public static List<ComboItem> GetArray<T>(Type t) where T : struct, IFormattable
|
||||
public static List<ComboItem> GetArray<T>() where T : struct, Enum, IFormattable
|
||||
{
|
||||
var names = Enum.GetNames(t);
|
||||
var values = (T[])Enum.GetValues(t);
|
||||
var names = Enum.GetNames<T>();
|
||||
var values = Enum.GetValues<T>();
|
||||
|
||||
var acres = new List<ComboItem>(names.Length);
|
||||
for (int i = 0; i < names.Length; i++)
|
||||
|
||||
@@ -21,4 +21,19 @@ public static void SetFlag(Span<byte> arr, int offset, int bitIndex, bool value)
|
||||
arr[offset] &= (byte)~(1 << bitIndex);
|
||||
arr[offset] |= (byte)((value ? 1 : 0) << bitIndex);
|
||||
}
|
||||
|
||||
public static bool GetFlag(ReadOnlySpan<byte> arr, int bitIndex)
|
||||
{
|
||||
var b = arr[bitIndex >> 3];
|
||||
var mask = 1 << (bitIndex & 7);
|
||||
return (b & mask) != 0;
|
||||
}
|
||||
|
||||
public static void SetFlag(Span<byte> arr, int bitIndex, bool value)
|
||||
{
|
||||
var offset = bitIndex >> 3;
|
||||
bitIndex &= 7; // ensure bit access is 0-7
|
||||
arr[offset] &= (byte)~(1 << bitIndex);
|
||||
arr[offset] |= (byte)((value ? 1 : 0) << bitIndex);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace NHSE.Core;
|
||||
@@ -82,6 +83,14 @@ public static class StructConverter
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] SetArray<T>(this ReadOnlySpan<T> data, int size) where T : class
|
||||
{
|
||||
var result = new byte[data.Length * size];
|
||||
for (int i = 0; i < data.Length; i++)
|
||||
data[i].ToBytesClass().CopyTo(result, i * size);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] SetArrayStructure<T>(this ReadOnlySpan<T> data, int size) where T : struct
|
||||
{
|
||||
var result = new byte[data.Length * size];
|
||||
|
||||
@@ -3,12 +3,8 @@
|
||||
|
||||
namespace NHSE.Injection;
|
||||
|
||||
public class AutoInjector
|
||||
public sealed record AutoInjector(IDataInjector Injector, Action<InjectionResult> DoRead, Action<InjectionResult> DoWrite)
|
||||
{
|
||||
public readonly IDataInjector Injector;
|
||||
private readonly Action<InjectionResult> AfterRead;
|
||||
private readonly Action<InjectionResult> AfterWrite;
|
||||
|
||||
public bool AutoInjectEnabled { private get; set; }
|
||||
|
||||
public bool ValidateEnabled
|
||||
@@ -17,13 +13,6 @@ public bool ValidateEnabled
|
||||
set => Injector.ValidateEnabled = value;
|
||||
}
|
||||
|
||||
public AutoInjector(IDataInjector inj, Action<InjectionResult> read, Action<InjectionResult> write)
|
||||
{
|
||||
Injector = inj;
|
||||
AfterRead = read;
|
||||
AfterWrite = write;
|
||||
}
|
||||
|
||||
public void Validate() => Injector.Validate();
|
||||
|
||||
public InjectionResult Read(bool force = false)
|
||||
@@ -34,7 +23,7 @@ public InjectionResult Read(bool force = false)
|
||||
try
|
||||
{
|
||||
var result = Injector.Read();
|
||||
AfterRead(result);
|
||||
DoRead(result);
|
||||
return result;
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
@@ -51,7 +40,7 @@ public InjectionResult Write(bool force = false)
|
||||
try
|
||||
{
|
||||
var result = Injector.Write();
|
||||
AfterWrite(result);
|
||||
DoWrite(result);
|
||||
return result;
|
||||
}
|
||||
catch (IndexOutOfRangeException ex)
|
||||
|
||||
@@ -1,35 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NHSE.Core;
|
||||
|
||||
namespace NHSE.Injection;
|
||||
|
||||
public class PocketInjector : IDataInjector
|
||||
public sealed class PocketInjector(IReadOnlyList<Item> items, IRAMReadWriter bot) : IDataInjector
|
||||
{
|
||||
private readonly IReadOnlyList<Item> Items;
|
||||
private readonly IRAMReadWriter Bot;
|
||||
public bool Connected => Bot.Connected;
|
||||
public bool Connected => bot.Connected;
|
||||
|
||||
private byte[]? LastData { get; set; }
|
||||
public uint WriteOffset { private get; set; }
|
||||
public bool ValidateEnabled { get; set; } = true;
|
||||
public bool SpoofInventoryWrite { get; set; }
|
||||
private static readonly Item DroppableOnlyItem = new(0x9C9); // Gold nugget
|
||||
|
||||
public PocketInjector(IReadOnlyList<Item> items, IRAMReadWriter bot)
|
||||
{
|
||||
Items = items;
|
||||
Bot = bot;
|
||||
}
|
||||
private static readonly Item DroppableOnlyItem = new(0x9C9); // Gold nugget
|
||||
|
||||
public bool ReadValidate(out byte[] data)
|
||||
{
|
||||
PlayerItemSet.GetOffsetLength(WriteOffset, out var offset, out var size);
|
||||
data = Bot.ReadBytes(offset, size);
|
||||
data = bot.ReadBytes(offset, size);
|
||||
return Validate(data);
|
||||
}
|
||||
|
||||
private byte[]? LastData;
|
||||
|
||||
public InjectionResult Read()
|
||||
{
|
||||
if (!ReadValidate(out var data))
|
||||
@@ -38,7 +30,7 @@ public InjectionResult Read()
|
||||
if (LastData?.SequenceEqual(data) == true)
|
||||
return InjectionResult.Same;
|
||||
|
||||
PlayerItemSet.ReadPlayerInventory(data, Items);
|
||||
PlayerItemSet.ReadPlayerInventory(data, items);
|
||||
|
||||
LastData = data;
|
||||
|
||||
@@ -52,9 +44,9 @@ public InjectionResult Write()
|
||||
|
||||
var orig = (byte[])data.Clone();
|
||||
|
||||
var items = !SpoofInventoryWrite ? Items : Enumerable.Repeat(DroppableOnlyItem, Items.Count).ToArray();
|
||||
var items1 = !SpoofInventoryWrite ? items : Enumerable.Repeat(DroppableOnlyItem, items.Count).ToArray();
|
||||
|
||||
PlayerItemSet.WritePlayerInventory(data, items);
|
||||
PlayerItemSet.WritePlayerInventory(data, items1);
|
||||
|
||||
if (data.SequenceEqual(orig))
|
||||
return InjectionResult.Same;
|
||||
@@ -63,7 +55,7 @@ public InjectionResult Write()
|
||||
if (size != data.Length)
|
||||
return InjectionResult.FailBadSize;
|
||||
|
||||
Bot.WriteBytes(data, offset);
|
||||
bot.WriteBytes(data, offset);
|
||||
|
||||
LastData = data;
|
||||
|
||||
|
||||
@@ -7,44 +7,35 @@ public static class Decoder
|
||||
private static bool IsNum(char c) => (uint)(c - '0') <= 9;
|
||||
private static bool IsHexUpper(char c) => (uint)(c - 'A') <= 5;
|
||||
|
||||
public static byte[] ConvertHexByteStringToBytes(byte[] bytes)
|
||||
public static byte[] ConvertHexByteStringToBytes(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
var dest = new byte[bytes.Length / 2];
|
||||
for (int i = 0; i < dest.Length; i++)
|
||||
{
|
||||
int ofs = i * 2;
|
||||
var _0 = (char)bytes[ofs + 0];
|
||||
var _1 = (char)bytes[ofs + 1];
|
||||
dest[i] = DecodeTuple(_0, _1);
|
||||
}
|
||||
LoadHexBytesTo(bytes, dest, 2);
|
||||
return dest;
|
||||
}
|
||||
|
||||
public static void LoadHexBytesTo(ReadOnlySpan<byte> str, Span<byte> dest, int tupleSize)
|
||||
{
|
||||
// The input string is 2-char hex values optionally separated.
|
||||
// The destination array should always be larger or equal than the bytes written. Let the runtime bounds check us.
|
||||
// Iterate through the string without allocating.
|
||||
for (int i = 0, j = 0; i < str.Length; i += tupleSize)
|
||||
dest[j++] = DecodeTuple((char)str[i + 0], (char)str[i + 1]);
|
||||
}
|
||||
|
||||
private static byte DecodeTuple(char _0, char _1)
|
||||
{
|
||||
byte result;
|
||||
if (IsNum(_0))
|
||||
result = (byte)((_0 - '0') << 4);
|
||||
else if (IsHexUpper(_0))
|
||||
result = (byte)((_0 - 'A' + 10) << 4);
|
||||
else
|
||||
return (byte)(DecodeChar(_0) << 4 | DecodeChar(_1));
|
||||
|
||||
static int DecodeChar(char x)
|
||||
{
|
||||
if (char.IsAsciiDigit(x))
|
||||
return (byte)(x - '0');
|
||||
if (char.IsAsciiHexDigitUpper(x))
|
||||
return (byte)(x - 'A' + 10);
|
||||
if (char.IsAsciiHexDigitLower(x))
|
||||
return (byte)(x - 'a' + 10);
|
||||
throw new ArgumentOutOfRangeException(nameof(_0));
|
||||
|
||||
if (IsNum(_1))
|
||||
result |= (byte)(_1 - '0');
|
||||
else if (IsHexUpper(_1))
|
||||
result |= (byte)(_1 - 'A' + 10);
|
||||
else
|
||||
throw new ArgumentOutOfRangeException(nameof(_1));
|
||||
return result;
|
||||
}
|
||||
|
||||
public static byte[] StringToByteArray(string hex)
|
||||
{
|
||||
int NumberChars = hex.Length;
|
||||
byte[] bytes = new byte[NumberChars / 2];
|
||||
for (int i = 0; i < NumberChars; i += 2)
|
||||
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
|
||||
return bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
namespace NHSE.Injection;
|
||||
|
||||
public class SysBot : IRAMReadWriter
|
||||
public sealed class SysBot : IRAMReadWriter
|
||||
{
|
||||
public string IP = "192.168.1.65";
|
||||
public int Port = 6000;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
namespace NHSE.Injection;
|
||||
|
||||
public class USBBot : IRAMReadWriter
|
||||
public sealed class USBBot : IRAMReadWriter
|
||||
{
|
||||
private UsbDevice? SwDevice;
|
||||
private UsbEndpointReader? reader;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
namespace NHSE.Parsing;
|
||||
|
||||
public class BCSV
|
||||
public sealed class BCSV
|
||||
{
|
||||
public static readonly BCSVEnumDictionary EnumLookup = new(Resources.specs_130.Split('\n'));
|
||||
public static bool DecodeColumnNames { private get; set; } = true;
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
namespace NHSE.Parsing;
|
||||
|
||||
public class BCSVEnumDictionary
|
||||
public sealed class BCSVEnumDictionary
|
||||
{
|
||||
private readonly Dictionary<uint, string> Lookup = [];
|
||||
private readonly Dictionary<uint, string> _lookup = [];
|
||||
|
||||
public BCSVEnumDictionary(IEnumerable<string> lines)
|
||||
{
|
||||
@@ -35,7 +35,7 @@ private void AddColumnName(string trim)
|
||||
var slice = value.AsSpan(2, value.Length - 3);
|
||||
var hex = StringUtil.GetHexValue(slice);
|
||||
|
||||
if (Lookup.TryGetValue(hex, out var exist))
|
||||
if (_lookup.TryGetValue(hex, out var exist))
|
||||
{
|
||||
if (exist == name)
|
||||
return;
|
||||
@@ -43,7 +43,7 @@ private void AddColumnName(string trim)
|
||||
return;
|
||||
}
|
||||
|
||||
Lookup.Add(hex, name);
|
||||
_lookup.Add(hex, name);
|
||||
}
|
||||
|
||||
private void AddEnumName(string trim)
|
||||
@@ -56,7 +56,7 @@ private void AddEnumName(string trim)
|
||||
return;
|
||||
|
||||
var hash = CRC32.Compute(text);
|
||||
if (Lookup.TryGetValue(hash, out var exist))
|
||||
if (_lookup.TryGetValue(hash, out var exist))
|
||||
{
|
||||
if (exist == text)
|
||||
return;
|
||||
@@ -64,10 +64,10 @@ private void AddEnumName(string trim)
|
||||
return;
|
||||
}
|
||||
|
||||
Lookup.Add(hash, text);
|
||||
_lookup.Add(hash, text);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Dump() => Lookup.Select(z => $"{z.Key:X8}\t{z.Value}");
|
||||
public IEnumerable<string> Dump() => _lookup.Select(z => $"{z.Key:X8}\t{z.Value}");
|
||||
|
||||
public string this[uint key] => Lookup.TryGetValue(key, out var val) ? val : $"0x{key:X8}";
|
||||
public string this[uint key] => _lookup.TryGetValue(key, out var val) ? val : $"0x{key:X8}";
|
||||
}
|
||||
@@ -1,16 +1,6 @@
|
||||
namespace NHSE.Parsing;
|
||||
|
||||
public class BCSVFieldParam
|
||||
public sealed record BCSVFieldParam(uint ColumnKey, int Offset, int Index)
|
||||
{
|
||||
public const int SIZE = 8;
|
||||
public readonly uint ColumnKey;
|
||||
public readonly int Offset;
|
||||
public readonly int Index;
|
||||
|
||||
public BCSVFieldParam(uint key, int offset, int index)
|
||||
{
|
||||
ColumnKey = key;
|
||||
Offset = offset;
|
||||
Index = index;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ public static class GameMSBTDumper
|
||||
/// <param name="dest">Destination folder where the dumps will be saved.</param>
|
||||
/// <param name="csv">Convert all <see cref="MSBT"/> files to CSV for easy viewing.</param>
|
||||
/// <param name="delim">Delimiter when exporting the <see cref="csv"/> files</param>
|
||||
public static void UpdateDumps(string root, string dest, bool csv = true, char delim = '\t')
|
||||
public static void UpdateDumps(string root, string dest, bool csv = false, char delim = '\t')
|
||||
{
|
||||
if (csv)
|
||||
UpdateCSV(root, Path.Combine(dest, "csv"), delim);
|
||||
@@ -74,7 +74,7 @@ public static string[] GetArtList(string msgPath)
|
||||
result.Add($"{itemID:00000}, // {Text}{fake}");
|
||||
}
|
||||
result.Sort();
|
||||
return result.ToArray();
|
||||
return [..result];
|
||||
}
|
||||
|
||||
public static string[] GetVillagerPhraseResource(string msgPath)
|
||||
@@ -82,7 +82,8 @@ public static string[] GetVillagerPhraseResource(string msgPath)
|
||||
var file = Path.Combine(msgPath, "Npc", "STR_NNpcPhrase.msbt");
|
||||
var list = GetLabelList(file);
|
||||
var normal = list.Select(z => $"{z.Label}\t{z.Text}").Order();
|
||||
return normal.ToArray();
|
||||
|
||||
return [..normal];
|
||||
}
|
||||
|
||||
public static string[] GetVillagerListResource(string msgPath)
|
||||
@@ -95,7 +96,7 @@ public static string[] GetVillagerListResource(string msgPath)
|
||||
list = GetLabelList(file);
|
||||
var special = list.Select(z => $"{z.Label}\t{z.Text}").Order();
|
||||
|
||||
return normal.Concat(special).ToArray();
|
||||
return [..normal, ..special];
|
||||
}
|
||||
|
||||
public static Dictionary<ushort, string> GetItemList(string msgPath, string language)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace NHSE.Parsing;
|
||||
|
||||
public class LBL1() : MSBTSection(string.Empty, [])
|
||||
public sealed class LBL1() : MSBTSection(string.Empty, [])
|
||||
{
|
||||
public uint NumberOfGroups;
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user