diff --git a/NHSE.Core/Save/Files/MainSave.cs b/NHSE.Core/Save/Files/MainSave.cs index 6552826..ab9f876 100644 --- a/NHSE.Core/Save/Files/MainSave.cs +++ b/NHSE.Core/Save/Files/MainSave.cs @@ -196,12 +196,19 @@ public void SetAcreBytes(ReadOnlySpan data) public TerrainTile[] GetTerrainTiles() => TerrainTile.GetArray(Data.Slice(Offsets.LandMakingMap, TotalTerrainTileCount * TerrainTile.SIZE)); public void SetTerrainTiles(IReadOnlyList array) => TerrainTile.SetArray(array).CopyTo(Data[Offsets.LandMakingMap..]); - public const int MapDesignNone = 0xF800; + public const ushort MapDesignNone = 0xF800; public Memory MapDesignTileData => Raw.Slice(Offsets.MyDesignMap, 112 * 96 * sizeof(ushort)); public ushort[] GetMapDesignTiles() => MemoryMarshal.Cast(MapDesignTileData.Span).ToArray(); public void SetMapDesignTiles(ReadOnlySpan value) => MemoryMarshal.Cast(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 diff --git a/NHSE.Core/Save/Meta/EncryptedFilePair.cs b/NHSE.Core/Save/Meta/EncryptedFilePair.cs index 14f875a..8f3c96d 100644 --- a/NHSE.Core/Save/Meta/EncryptedFilePair.cs +++ b/NHSE.Core/Save/Meta/EncryptedFilePair.cs @@ -46,12 +46,12 @@ protected EncryptedFilePair(ISaveFileProvider provider, string name) RawHeader = hd; RawData = md; - Info = RawHeader[..FileHeaderInfo.SIZE].ToClass(); + Info = Header[..FileHeaderInfo.SIZE].ToArray().ToClass(); } 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); } diff --git a/NHSE.Core/Structures/Designs/DesignPattern.cs b/NHSE.Core/Structures/Designs/DesignPattern.cs index 89bd3ee..19e5c41 100644 --- a/NHSE.Core/Structures/Designs/DesignPattern.cs +++ b/NHSE.Core/Structures/Designs/DesignPattern.cs @@ -6,7 +6,7 @@ namespace NHSE.Core; /// /// Simple design pattern /// -public class DesignPattern : IVillagerOrigin +public class DesignPattern(Memory 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 Raw; public Span Data => Raw.Span; - public DesignPattern(Memory data) => Raw = data; - public uint Hash { get => ReadUInt32LittleEndian(Data); @@ -119,7 +116,13 @@ public static int GetColorOffset(int index) /// 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 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; } /// @@ -141,6 +143,12 @@ public byte[] GetBitmap() public byte[] GetPaletteBitmap() { var result = new byte[3 * PaletteColorCount]; + LoadPaletteBitmap(result); + return result; + } + + public void LoadPaletteBitmap(Span 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; } } \ No newline at end of file diff --git a/NHSE.Core/Structures/Designs/DesignPatternPRO.cs b/NHSE.Core/Structures/Designs/DesignPatternPRO.cs index 329d060..084cb16 100644 --- a/NHSE.Core/Structures/Designs/DesignPatternPRO.cs +++ b/NHSE.Core/Structures/Designs/DesignPatternPRO.cs @@ -6,7 +6,7 @@ namespace NHSE.Core; /// /// Advanced design pattern with 4 sheets arranged in a square. /// -public class DesignPatternPRO : IVillagerOrigin +public class DesignPatternPRO(Memory 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 Raw; public Span Data => Raw.Span; - public DesignPatternPRO(Memory data) => Raw = data; - public uint Hash { get => ReadUInt32LittleEndian(Data); @@ -114,7 +111,13 @@ public static int GetColorOffset(int index) /// 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 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; } /// @@ -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 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; } } \ No newline at end of file diff --git a/NHSE.Core/Structures/Item/Item.cs b/NHSE.Core/Structures/Item/Item.cs index 5df3f10..e7022aa 100644 --- a/NHSE.Core/Structures/Item/Item.cs +++ b/NHSE.Core/Structures/Item/Item.cs @@ -225,6 +225,7 @@ public void CopyFrom(Item item) public static Item[] GetArray(ReadOnlySpan data) => data.GetArray(SIZE); public static byte[] SetArray(IReadOnlyList data) => data.SetArray(SIZE); + public static byte[] SetArray(ReadOnlySpan data) => data.SetArray(SIZE); public ushort GetWrappedItemName() => WrappingType switch { diff --git a/NHSE.Core/Structures/Map/Layers/MapLayerConfigAcre.cs b/NHSE.Core/Structures/Map/Layers/MapLayerConfigAcre.cs new file mode 100644 index 0000000..c33071f --- /dev/null +++ b/NHSE.Core/Structures/Map/Layers/MapLayerConfigAcre.cs @@ -0,0 +1,161 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace NHSE.Core; + +/// +/// Configures how a layer rests within the Map's grid, relative to a "chunk" or "acre". +/// +/// Number of acres in the width direction. +/// Number of acres in the height direction. +/// Horizontal acre shift from the map's origin. +/// Vertical acre shift from the map's origin. +/// Number of tiles per acre in one dimension (16 or 32). +/// Bit shift value to convert between tiles and acres (4 for 16 tiles, 5 for 32 tiles). +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 + + /// + /// Creates a new instance, centering the layer within the acre. + /// + /// Width of the layer in acres. + /// Height of the layer in acres. + /// Number of tiles per acre (16 or 32). + /// A new instance. + 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 + } + + /// + /// Converts absolute coordinates to coordinates relative to the stored layer. + /// + /// Absolute X coordinate on the map. + /// Absolute Y coordinate on the map. + /// Relative X coordinate in the layer. + /// Relative Y coordinate in the layer. + /// if the absolute coordinates are within the layer; otherwise, . + 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; + } + + /// + /// Determines whether the specified absolute X and Y coordinates are within the valid bounds of the map. + /// + /// The absolute X coordinate to validate. Must be within the horizontal bounds of the map. + /// The absolute Y coordinate to validate. Must be within the vertical bounds of the map. + /// if the coordinates are valid; otherwise, . + 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; + } + + /// + /// Gets the absolute acre coordinates from absolute tile coordinates. + /// + 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); + } + + /// + /// Gets the requested tile index within the layer, given relative tile coordinates. + /// + /// The tile index within the layer. + /// + 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; + } + + /// + /// Gets the requested tile index within the absolute map boundary, given absolute tile coordinates in the map. + /// + /// The tile index within the map. + /// + 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; + } + + /// + /// Gets the acre index (not the value selection of the acre) based on the absolute coordinates on the map. + /// + /// The acre index within the map. + /// + 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; + } +} \ No newline at end of file diff --git a/NHSE.Core/Structures/Map/Layers/TerrainLayer.cs b/NHSE.Core/Structures/Map/Layers/TerrainLayer.cs index ee925ca..d1da700 100644 --- a/NHSE.Core/Structures/Map/Layers/TerrainLayer.cs +++ b/NHSE.Core/Structures/Map/Layers/TerrainLayer.cs @@ -36,14 +36,15 @@ public TerrainLayer(TerrainTile[] tiles, Memory 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; - } + /// + /// Flattens the terrain tiles into a contiguous byte array. + /// + public byte[] DumpAll() => TerrainTile.SetArray(Tiles); + /// + /// Gets the tiles local to the specified acre as a contiguous byte array. + /// + /// Terrain acre index. public byte[] DumpAcre(int acre) { int count = TileInfo.ViewCount; @@ -58,6 +59,10 @@ public byte[] DumpAcre(int acre) return result; } + /// + /// Imports terrain tiles from a contiguous byte array. + /// + /// Byte array containing terrain tile data. public void ImportAll(ReadOnlySpan data) { var tiles = TerrainTile.GetArray(data); @@ -65,6 +70,11 @@ public void ImportAll(ReadOnlySpan data) Tiles[i].CopyFrom(tiles[i]); } + /// + /// Imports terrain tiles for the specified acre from a contiguous byte array. + /// + /// Terrain acre index. + /// Byte array containing terrain tile data. public void ImportAcre(int acre, ReadOnlySpan data) { int count = TileInfo.ViewCount; @@ -76,7 +86,12 @@ public void ImportAcre(int acre, ReadOnlySpan data) } } - public void SetAll(TerrainTile tile, in bool interiorOnly) + /// + /// Sets all tiles to the specified tile. + /// + /// + /// If true, only sets the interior tiles, skipping the outermost ring of beach/rock acres. + 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) + /// + /// Sets all road tiles (not the terrain itself, but the road atop) to the specified tile. + /// + /// Road tile info to copy from. + /// If true, only sets the interior tiles, skipping the outermost ring of beach/rock acres. + 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); diff --git a/NHSE.Core/Structures/Map/Managers/FieldItemManager.cs b/NHSE.Core/Structures/Map/Managers/FieldItemManager.cs index 15b844b..ee847ee 100644 --- a/NHSE.Core/Structures/Map/Managers/FieldItemManager.cs +++ b/NHSE.Core/Structures/Map/Managers/FieldItemManager.cs @@ -56,7 +56,6 @@ public void Save() /// /// Lists out all coordinates of tiles present in that don't have anything underneath in to support them. /// - /// public List GetUnsupportedTiles(int totalWidth, int totalHeight) { var result = new List(); diff --git a/NHSE.Core/Structures/Map/TileGridViewport.cs b/NHSE.Core/Structures/Map/TileGridViewport.cs index d59a3f7..0a95a96 100644 --- a/NHSE.Core/Structures/Map/TileGridViewport.cs +++ b/NHSE.Core/Structures/Map/TileGridViewport.cs @@ -78,5 +78,4 @@ private void SetTopLeftNearest(ref int x, ref int y) int maxY = TotalHeight - ViewHeight; ClampCoordinatesTo(ref x, ref y, maxX, maxY); } - } \ No newline at end of file diff --git a/NHSE.Core/Structures/Misc/GSavePlayerManpu.cs b/NHSE.Core/Structures/Misc/GSavePlayerManpu.cs index b1d7a54..7aad662 100644 --- a/NHSE.Core/Structures/Misc/GSavePlayerManpu.cs +++ b/NHSE.Core/Structures/Misc/GSavePlayerManpu.cs @@ -13,49 +13,14 @@ public struct GSavePlayerManpu : IReactionStore private const int MaxCount = 64; private const int WheelCount = 8; - /// - /// List of known Reaction IDs - /// [field: MarshalAs(UnmanagedType.ByValArray, SizeConst = MaxCount)] public Reaction[] ManpuBit { get; set; } - /// - /// Emotions that are currently bound to the Reaction Wheel. - /// [field: MarshalAs(UnmanagedType.ByValArray, SizeConst = WheelCount)] public Reaction[] UIList { get; set; } - /// - /// Flags indicating if a Reaction (at the same index?) is newly learned or not. - /// [field: MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = MaxCount)] public bool[] NewFlag { get; set; } - - public void AddMissingReactions() - { - var all = Enum.GetValues(); - 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); } /// @@ -68,56 +33,64 @@ public struct GSavePlayerManpu15 : IReactionStore private const int MaxCount = 256; // up from 64 private const int WheelCount = 8; - /// - /// List of known Reaction IDs - /// [field: MarshalAs(UnmanagedType.ByValArray, SizeConst = MaxCount)] public Reaction[] ManpuBit { get; set; } - /// - /// Emotions that are currently bound to the Reaction Wheel. - /// [field: MarshalAs(UnmanagedType.ByValArray, SizeConst = WheelCount)] public Reaction[] UIList { get; set; } - /// - /// Flags indicating if a Reaction (at the same index?) is newly learned or not. - /// [field: MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = MaxCount)] public bool[] NewFlag { get; set; } - - public void AddMissingReactions() - { - var all = Enum.GetValues(); - 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 { + /// + /// List of Reaction IDs the player currently knows. + /// Reaction[] ManpuBit { get; set; } + + /// + /// Emotions that are currently bound to the Reaction Wheel. + /// Reaction[] UIList { get; set; } + + /// + /// Flags indicating if a Reaction (at the same index?) is newly learned or not. + /// bool[] NewFlag { get; set; } - bool AddReaction(Reaction react); - void AddMissingReactions(); + + /// + /// Adds all possible reaction values from 's defined list. + /// + void AddMissingReactions() + { + var all = Enum.GetValues(); + foreach (var react in all) + TryAddReaction(react); + } + + /// + /// Attempts to add the to the list of reactions. + /// + /// Reaction to add to list + 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; + } + + /// + /// First empty index within the array of reactions. + /// + int EmptyIndex => ManpuBit.IndexOf(Reaction.None); } \ No newline at end of file diff --git a/NHSE.Core/Structures/Misc/MapManager.cs b/NHSE.Core/Structures/Misc/MapManager.cs index 81d919f..ba4d6cb 100644 --- a/NHSE.Core/Structures/Misc/MapManager.cs +++ b/NHSE.Core/Structures/Misc/MapManager.cs @@ -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 result) - { - var tiles = sav.GetMapDesignTiles(); - MemoryMarshal.Cast(result).CopyTo(tiles); - sav.SetMapDesignTiles(tiles); - } } public class MapTerrainStructure(MainSave sav) diff --git a/NHSE.Core/Structures/Villager/PlayerRoom1.cs b/NHSE.Core/Structures/Villager/PlayerRoom1.cs index 92a7e3a..35a54f5 100644 --- a/NHSE.Core/Structures/Villager/PlayerRoom1.cs +++ b/NHSE.Core/Structures/Villager/PlayerRoom1.cs @@ -3,15 +3,12 @@ namespace NHSE.Core; -public class PlayerRoom1 : IPlayerRoom +public class PlayerRoom1(Memory raw) : IPlayerRoom { public const int SIZE = 0x65C8; public virtual string Extension => "nhpr"; - public readonly Memory Raw; - public Span Data => Raw.Span; - - public PlayerRoom1(Memory data) => Raw = data; + public Span 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 value) => RoomItemLayer.SetArray(value).AsSpan().CopyTo(Data); + public void SetItemLayers(IReadOnlyList 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); diff --git a/NHSE.Core/Structures/Villager/PlayerRoom2.cs b/NHSE.Core/Structures/Villager/PlayerRoom2.cs index 8098a87..819a96f 100644 --- a/NHSE.Core/Structures/Villager/PlayerRoom2.cs +++ b/NHSE.Core/Structures/Villager/PlayerRoom2.cs @@ -4,13 +4,11 @@ namespace NHSE.Core; -public class PlayerRoom2 : PlayerRoom1 +public class PlayerRoom2(Memory raw) : PlayerRoom1(raw) { public new const int SIZE = 0x6C24; public new virtual string Extension => "nhpr2"; - public PlayerRoom2(Memory data) : base(data) { } - /* s_665e9093 ExtraEffectLayerList[2]; // @0x65c8 size 0x320, align 2 GSaveMusicBoxInfo MusicBoxInfo; // @0x6c08 size 0x4, align 2 diff --git a/NHSE.Core/Util/StructConverter.cs b/NHSE.Core/Util/StructConverter.cs index 196dc78..e4ecfdf 100644 --- a/NHSE.Core/Util/StructConverter.cs +++ b/NHSE.Core/Util/StructConverter.cs @@ -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(this ReadOnlySpan 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(this ReadOnlySpan data, int size) where T : struct { var result = new byte[data.Length * size]; diff --git a/NHSE.Parsing/GameMSBTDumper.cs b/NHSE.Parsing/GameMSBTDumper.cs index 9ec121d..ef52428 100644 --- a/NHSE.Parsing/GameMSBTDumper.cs +++ b/NHSE.Parsing/GameMSBTDumper.cs @@ -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 GetItemList(string msgPath, string language) diff --git a/NHSE.Sprites/Field/MapViewer.cs b/NHSE.Sprites/Field/MapViewer.cs index 45e32fa..f7139f8 100644 --- a/NHSE.Sprites/Field/MapViewer.cs +++ b/NHSE.Sprites/Field/MapViewer.cs @@ -4,8 +4,15 @@ namespace NHSE.Sprites; +/// +/// Produces bitmaps for viewing map acres and full maps. +/// 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) diff --git a/NHSE.WinForms/Main.cs b/NHSE.WinForms/Main.cs index bdfa58e..ab29a10 100644 --- a/NHSE.WinForms/Main.cs +++ b/NHSE.WinForms/Main.cs @@ -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)) diff --git a/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs b/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs index b89a824..dc69611 100644 --- a/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs +++ b/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs @@ -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(); } diff --git a/NHSE.WinForms/Subforms/Map/MiscDumpHelper.cs b/NHSE.WinForms/Subforms/Map/MiscDumpHelper.cs index 15ebdba..37ce4a3 100644 --- a/NHSE.WinForms/Subforms/Map/MiscDumpHelper.cs +++ b/NHSE.WinForms/Subforms/Map/MiscDumpHelper.cs @@ -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; }