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