Misc tweaks

Still a WIP
This commit is contained in:
Kurt
2026-01-17 16:08:50 -06:00
parent f6ac545cba
commit f26a6ac493
19 changed files with 354 additions and 154 deletions

View File

@@ -196,12 +196,19 @@ public void SetAcreBytes(ReadOnlySpan<byte> data)
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);
public void ClearDesignTiles()
{
var tiles = GetMapDesignTiles();
tiles.AsSpan().Fill(MapDesignNone);
SetMapDesignTiles(tiles);
}
private int FieldItemLayerSize => TotalFieldItemTileCount * Item.SIZE;
private int FieldItemFlagSize => TotalFieldItemTileCount / sizeof(byte); // bitflags

View File

@@ -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);
}

View File

@@ -6,7 +6,7 @@ namespace NHSE.Core;
/// <summary>
/// Simple design pattern
/// </summary>
public class DesignPattern : IVillagerOrigin
public 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;
}
}

View File

@@ -6,7 +6,7 @@ namespace NHSE.Core;
/// <summary>
/// Advanced design pattern with 4 sheets arranged in a square.
/// </summary>
public class DesignPatternPRO : IVillagerOrigin
public 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;
}
}

View File

@@ -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
{

View File

@@ -0,0 +1,161 @@
using System;
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>
public readonly record struct MapLayerConfigAcre(
byte CountWidth, byte CountHeight,
byte ShiftWidth, byte ShiftHeight,
[ConstantExpected] byte TilesPerAcre, byte TileBitShift)
{
// 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="MapLayerConfigAcre"/> 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>
/// <returns>A new <see cref="MapLayerConfigAcre"/> instance.</returns>
public static MapLayerConfigAcre Create(byte width, byte height, [ConstantExpected(Min = Grid16, Max = Grid32)] byte tilesPerAcre)
{
var shiftW = (byte)((MapAcreWidth - width) / 2); // centered
var shiftH = (byte)((MapAcreHeight - height) / 2); // centered
var bitShift = tilesPerAcre == Grid16 ? Shift16 : Shift32;
#pragma warning disable CA1857
return new MapLayerConfigAcre(width, height, shiftW, shiftH, tilesPerAcre, bitShift);
#pragma warning restore CA1857
}
/// <summary>
/// Converts absolute coordinates to coordinates relative to the stored layer.
/// </summary>
/// <param name="absX">Absolute X coordinate on the map.</param>
/// <param name="absY">Absolute Y coordinate on the map.</param>
/// <param name="relX">Relative X coordinate in the layer.</param>
/// <param name="relY">Relative Y coordinate in the layer.</param>
/// <returns><see langword="true"/> if the absolute coordinates are within the layer; otherwise, <see langword="false"/>.</returns>
public bool TryGetRelativeCoordinates(int absX, int absY, out int relX, out int relY)
{
relX = 0;
relY = 0;
// Get relative acre
var (acreX, acreY) = GetAbsoluteAcre(absX, absY);
acreX -= ShiftWidth;
acreY -= ShiftHeight;
// Performance: single if-check by using underflow casting to unsigned
if ((uint)acreX >= CountWidth)
return false;
if ((uint)acreY >= CountHeight)
return false;
// Return relative position
relX = absX - (ShiftWidth << TileBitShift);
relY = absY - (ShiftHeight << TileBitShift);
return true;
}
/// <summary>
/// Determines whether the specified absolute X and Y coordinates are within the valid bounds of the map.
/// </summary>
/// <param name="absX">The absolute X coordinate to validate. Must be within the horizontal bounds of the map.</param>
/// <param name="absY">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 IsAbsoluteCoordinateValid(int absX, int absY)
{
if ((uint)absX >= ((uint)MapAcreWidth << TileBitShift))
return false;
if ((uint)absY >= ((uint)MapAcreHeight << TileBitShift))
return false;
return true;
}
/// <summary>
/// Gets the absolute acre coordinates from absolute tile coordinates.
/// </summary>
public (int X, int Y) GetAbsoluteAcre(int absX, int absY)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absX, (uint)MapAcreWidth << TileBitShift);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absY, (uint)MapAcreHeight << TileBitShift);
var acreX = absX >> TileBitShift;
var acreY = absY >> TileBitShift;
return (acreX, acreY);
}
/// <summary>
/// Gets the requested tile index within the layer, given relative tile coordinates.
/// </summary>
/// <returns>The tile index within the layer.</returns>
/// <inheritdoc cref="TryGetRelativeCoordinates(int, int, out int, out int)"/>
public int GetIndexTileRelative(int relX, int relY)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)relX, (uint)CountWidth << TileBitShift);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)relY, (uint)CountHeight << TileBitShift);
// Tile ordering is top-down, left-to-right.
// In other words, Item[1] is X=0,Y=1
return (relX * (CountHeight << TileBitShift)) + relY;
}
/// <summary>
/// Gets the requested tile index within the absolute map boundary, given absolute tile coordinates in the map.
/// </summary>
/// <returns>The tile index within the map.</returns>
/// <inheritdoc cref="TryGetRelativeCoordinates(int, int, out int, out int)"/>
public int GetIndexTileAbsolute(int absX, int absY)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absX, (uint)MapAcreWidth << TileBitShift);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absY, (uint)MapAcreHeight << TileBitShift);
// Tile ordering is top-down, left-to-right.
// In other words, Item[1] is X=0,Y=1
return (absX * (MapAcreHeight << TileBitShift)) + absY;
}
/// <summary>
/// Gets the acre index (not the value selection of the acre) based on the absolute coordinates on the map.
/// </summary>
/// <returns>The acre index within the map.</returns>
/// <inheritdoc cref="TryGetRelativeCoordinates(int, int, out int, out int)"/>
public int GetIndexAcre(int absX, int absY)
{
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absX, MapAcreWidth);
ArgumentOutOfRangeException.ThrowIfGreaterThanOrEqual((uint)absY, MapAcreHeight);
// Acre ordering is top-down, left-to-right.
var (x, y) = GetAbsoluteAcre(absX, absY);
return (x * MapAcreHeight) + y;
}
}

View File

@@ -36,14 +36,15 @@ public TerrainLayer(TerrainTile[] tiles, Memory<byte> acres) : base(Viewport)
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;
}
/// <summary>
/// Flattens the terrain tiles into a contiguous byte array.
/// </summary>
public byte[] DumpAll() => TerrainTile.SetArray(Tiles);
/// <summary>
/// Gets the tiles local to the specified acre as a contiguous byte array.
/// </summary>
/// <param name="acre">Terrain acre index.</param>
public byte[] DumpAcre(int acre)
{
int count = TileInfo.ViewCount;
@@ -58,6 +59,10 @@ public byte[] DumpAcre(int acre)
return result;
}
/// <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);
@@ -65,6 +70,11 @@ public void ImportAll(ReadOnlySpan<byte> data)
Tiles[i].CopyFrom(tiles[i]);
}
/// <summary>
/// Imports terrain tiles for the specified acre from a contiguous byte array.
/// </summary>
/// <param name="acre">Terrain acre index.</param>
/// <param name="data">Byte array containing terrain tile data.</param>
public void ImportAcre(int acre, ReadOnlySpan<byte> data)
{
int count = TileInfo.ViewCount;
@@ -76,7 +86,12 @@ public void ImportAcre(int acre, ReadOnlySpan<byte> data)
}
}
public void SetAll(TerrainTile tile, in bool interiorOnly)
/// <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)
{
@@ -98,7 +113,12 @@ public void SetAll(TerrainTile tile, in bool interiorOnly)
}
}
public void SetAllRoad(TerrainTile tile, in bool interiorOnly)
/// <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)
{
@@ -163,6 +183,7 @@ public int GetTileColor(int x, in int y, int relativeX, int relativeY)
private ushort GetTileAcre(int x, int y)
{
// Acres are 16x16 tiles, and the acre data has a 1-acre deep-sea border around it.
var acreX = 1 + (x / 16);
var acreY = 1 + (y / 16);

View File

@@ -56,7 +56,6 @@ public void Save()
/// <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(int totalWidth, int totalHeight)
{
var result = new List<string>();

View File

@@ -78,5 +78,4 @@ private void SetTopLeftNearest(ref int x, ref int y)
int maxY = TotalHeight - ViewHeight;
ClampCoordinatesTo(ref x, ref y, maxX, maxY);
}
}

View File

@@ -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);
}

View File

@@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Collections.Generic;
namespace NHSE.Core;
@@ -11,29 +9,6 @@ public class MapManager(MainSave sav) : MapTerrainStructure(sav)
public int MapLayer { get; set; } // 0 or 1
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(MainSave sav)

View File

@@ -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();
@@ -24,7 +21,7 @@ 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 void SetItemLayers(IReadOnlyList<RoomItemLayer> value) => RoomItemLayer.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);

View File

@@ -4,13 +4,11 @@
namespace NHSE.Core;
public class PlayerRoom2 : PlayerRoom1
public 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) { }
/*
s_665e9093 ExtraEffectLayerList[2]; // @0x65c8 size 0x320, align 2
GSaveMusicBoxInfo MusicBoxInfo; // @0x6c08 size 0x4, align 2

View File

@@ -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];

View File

@@ -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)

View File

@@ -4,8 +4,15 @@
namespace NHSE.Sprites;
/// <summary>
/// Produces bitmaps for viewing map acres and full maps.
/// </summary>
public sealed class MapViewer : MapView, IDisposable
{
private const byte ScaleAsMap = 2;
private const byte FieldItemWidthOld = 7;
private const byte FieldItemWidthNew = 9;
// Cached acre view objects to remove allocation/GC
private readonly int[] PixelsItemAcre1;
private readonly int[] PixelsItemAcreX;
@@ -53,18 +60,35 @@ public void Dispose()
public Bitmap GetBackgroundTerrain(int index = -1)
{
return TerrainSprite.GetMapWithBuildings(Map, null, PixelsBackgroundMap1, PixelsBackgroundMapX, BackgroundMap, 2, index);
return TerrainSprite.GetMapWithBuildings(Map, null, PixelsBackgroundMap1, PixelsBackgroundMapX, BackgroundMap, ScaleAsMap, index);
}
private Bitmap GetLayerAcre(int topX, int topY, int t)
public Bitmap GetInflatedImage(Bitmap regular)
{
// Insert 1 acre on each side
int columnWidth = (regular.Width / FieldItemWidthOld);
var newWidth = columnWidth * FieldItemWidthNew;
var bmp = new Bitmap(newWidth, regular.Height);
using var g = Graphics.FromImage(bmp);
// Fill with blue
// g.Clear(Color.FromArgb(100, 149, 237));
// Draw regular centered to new bitmap
g.DrawImage(regular, columnWidth, 0, regular.Width, regular.Height);
return bmp;
}
private Bitmap GetLayerAcre(int topX, int topY, int transparency)
{
var layer = Map.CurrentLayer;
return ItemLayerSprite.GetBitmapItemLayerViewGrid(layer, topX, topY, AcreScale, PixelsItemAcre1, PixelsItemAcreX, ScaleAcre, t);
return ItemLayerSprite.GetBitmapItemLayerViewGrid(layer, topX, topY, AcreScale, PixelsItemAcre1, PixelsItemAcreX, ScaleAcre, transparency);
}
public Bitmap GetBackgroundAcre(Font f, byte tbuild, byte tterrain, int index = -1)
public Bitmap GetBackgroundAcre(Font f, byte transparencyBuilding, byte transparencyTerrain, int index = -1)
{
return TerrainSprite.GetAcre(this, f, PixelsBackgroundAcre1, PixelsBackgroundAcreX, BackgroundAcre, index, tbuild, tterrain);
return TerrainSprite.GetAcre(this, f, PixelsBackgroundAcre1, PixelsBackgroundAcreX, BackgroundAcre, index, transparencyBuilding, transparencyTerrain);
}
private Bitmap GetMapWithReticle(int topX, int topY, int t, FieldItemLayer layer)

View File

@@ -30,7 +30,7 @@ public Main()
Show();
WindowState = FormWindowState.Normal;
var args = Environment.GetCommandLineArgs().AsSpan();
var args = Environment.GetCommandLineArgs();
foreach (var arg in args)
{
if (Directory.Exists(arg))

View File

@@ -17,6 +17,7 @@ public sealed partial class FieldItemEditor : Form, IItemLayerEditor
private readonly MapManager Map;
private readonly MapViewer View;
private readonly bool IsExtendedMap30;
private bool Loading;
private int SelectedBuildingIndex;
@@ -37,8 +38,9 @@ public FieldItemEditor(MainSave sav)
InitializeComponent();
this.TranslateInterface(GameInfo.CurrentLanguage);
var scale = (PB_Acre.Width - 2) / 32;
var scale = (PB_Acre.Width - 2) / FieldItemLayer.TilesPerAcreDim; // 1px border
SAV = sav;
IsExtendedMap30 = sav.FieldItemAcreWidth != 7;
Map = new MapManager(sav);
View = new MapViewer(Map, scale);
@@ -57,8 +59,11 @@ public FieldItemEditor(MainSave sav)
private void LoadComboBoxes()
{
// Snap viewport to acre
foreach (var acre in AcreCoordinate.Acres)
CB_Acre.Items.Add(acre.Name);
// Select acre type for current
foreach (var acre in AcreCoordinate.Exterior)
CB_MapAcre.Items.Add(acre.Name);
@@ -113,20 +118,36 @@ private void LoadItemGridAcre()
private void ReloadMapBackground()
{
PB_Map.BackgroundImage = View.GetBackgroundTerrain(SelectedBuildingIndex);
var img = View.GetBackgroundTerrain(SelectedBuildingIndex);
SetMapBackgroundImage(img);
}
private void ReloadMapItemGrid() => SetMapForegroundImage(View.GetMapWithReticle(GetItemTransparency()));
private void SetMapBackgroundImage(Bitmap img)
{
if (IsExtendedMap30)
img = View.GetInflatedImage(img);
PB_Map.BackgroundImage = img;
PB_Map.Invalidate(); // background image reassigning to same img doesn't redraw; force it
}
private void SetMapForegroundImage(Bitmap img)
{
if (IsExtendedMap30)
img = View.GetInflatedImage(img);
PB_Map.Image = img;
}
private void ReloadAcreBackground()
{
var tbuild = (byte)TR_BuildingTransparency.Value;
var tterrain = (byte)TR_Terrain.Value;
PB_Acre.BackgroundImage = View.GetBackgroundAcre(L_Coordinates.Font, tbuild, tterrain, SelectedBuildingIndex);
var img = View.GetBackgroundAcre(L_Coordinates.Font, tbuild, tterrain, SelectedBuildingIndex);
PB_Acre.BackgroundImage = img;
PB_Acre.Invalidate(); // background image reassigning to same img doesn't redraw; force it
}
private void ReloadMapItemGrid() => PB_Map.Image = View.GetMapWithReticle(GetItemTransparency());
private void ReloadAcreItemGrid() => PB_Acre.Image = View.GetLayerAcre(GetItemTransparency());
public void ReloadItems()
@@ -1069,7 +1090,7 @@ private void B_SetAllRoadTiles_Click(object sender, EventArgs e)
private void B_ClearPlacedDesigns_Click(object sender, EventArgs e)
{
MapManager.ClearDesignTiles(SAV);
SAV.ClearDesignTiles();
System.Media.SystemSounds.Asterisk.Play();
}
@@ -1082,7 +1103,7 @@ private void B_ExportPlacedDesigns_Click(object sender, EventArgs e)
return;
string path = sfd.FileName;
var tiles = MapManager.ExportDesignTiles(SAV);
var tiles = SAV.MapDesignTileData.Span;
File.WriteAllBytes(path, tiles);
System.Media.SystemSounds.Asterisk.Play();
}
@@ -1097,7 +1118,7 @@ private void B_ImportPlacedDesigns_Click(object sender, EventArgs e)
string path = ofd.FileName;
var tiles = File.ReadAllBytes(path);
MapManager.ImportDesignTiles(SAV, tiles);
tiles.CopyTo(SAV.MapDesignTileData.Span);
System.Media.SystemSounds.Asterisk.Play();
}

View File

@@ -85,7 +85,7 @@ public static bool LoadMuseum(Museum museum)
}
var data = File.ReadAllBytes(file);
data.AsSpan().CopyTo(museum.Data);
data.CopyTo(museum.Data);
return true;
}