Code now compiles, but will need to test and bugfix extensively.
Need to pre-fill the pixel buffers with sea first unless the acre fill actually works (maybe?)
Items need to be relative'd to apply 1 row lower, and skip the bottom row.
Need to check that terrain is the same as well.

Note to self:
For Map - probably better to get a starting x,y,width and iterate off that, fetching tile from relative, converting to absolute... idk.
For View - need to check if tile is in layer.
This commit is contained in:
Kurt
2026-01-19 23:12:59 -06:00
parent 50ce05af6f
commit 0ce31ca895
21 changed files with 1738 additions and 1617 deletions

View File

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

View File

@@ -37,4 +37,11 @@ public enum BuildingType : ushort
Studio = 29,
Hotel = 42,
}
}
public static class BuildingUtil
{
public static (int Width, int Height) GetDimensions(this BuildingType type) => type switch
{
_ => (2, 2),
};
}

View File

@@ -12,10 +12,12 @@ namespace NHSE.Core;
/// <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] 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.
@@ -47,115 +49,55 @@ namespace NHSE.Core;
/// <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>
/// <returns>A new <see cref="LayerPositionConfig"/> instance.</returns>
public static LayerPositionConfig Create(byte width, byte height, [ConstantExpected(Min = Grid16, Max = Grid32)] byte tilesPerAcre)
public static LayerPositionConfig Create(byte width, byte height,
[ConstantExpected(Min = Grid16, Max = Grid32)] byte tilesPerAcre,
[ConstantExpected(Min = 1, Max = 2)] byte metaTileSize)
{
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 LayerPositionConfig(width, height, shiftW, shiftH, tilesPerAcre, bitShift);
return new LayerPositionConfig(width, height, shiftW, shiftH, tilesPerAcre, bitShift, metaTileSize);
#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)
public (int X, int Y) GetCoordinatesAbsolute(int relX, int relY)
{
relX = 0;
relY = 0;
var absX = relX - ((ShiftWidth * MetaTileSize) << TileBitShift);
var absY = relY - ((ShiftHeight * MetaTileSize) << TileBitShift);
return (absX, absY);
}
// 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;
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="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>
/// <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 IsAbsoluteCoordinateValid(int absX, int absY)
public bool IsCoordinateValidRelative(int relX, int relY)
{
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);
var index = GetIndexTileRelative(relX, relY);
return (uint)index < (CountWidth * CountHeight << (TileBitShift * 2));
}
/// <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

@@ -140,18 +140,7 @@ public void SetAllRoad(TerrainTile tile, bool interiorOnly = true)
}
}
public bool IsWithinGrid(int acreScale, int relX, int relY)
{
if ((uint)relX >= TileInfo.ViewWidth * acreScale)
return false;
if ((uint)relY >= TileInfo.ViewHeight * acreScale)
return false;
return true;
}
public int GetTileColor(int x, in int y, int relativeX, int relativeY)
public int GetTileColor(int x, int y, int relativeX, int relativeY)
{
var acre = GetTileAcre(x, y);
if (acre != 0)

View File

@@ -10,12 +10,12 @@ public sealed class MapEditor
/// <summary>
/// Amount of pixel upscaling compared to a 1px = 1 tile map.
/// </summary>
public int MapScale { get; set; } = 2;
public int MapScale { get; set; } = 1;
/// <summary>
/// Amount of pixel upscaling compared to a 1px = 1 tile map.
/// </summary>
public int AcreScale { get; set; } = 8;
public int ViewScale { get; set; } = 16;
/// <summary>
/// Converts an upscaled coordinate to a tile coordinate.
@@ -44,4 +44,87 @@ public static MapEditor FromSaveFile(MainSave sav)
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,
}

View File

@@ -2,7 +2,7 @@
namespace NHSE.Core;
public sealed class MapMutator
public sealed record MapMutator
{
public MapViewState View { get; init; } = new();
public required MapTileManager Manager { get; init; }
@@ -58,4 +58,12 @@ private int ReplaceFieldItems(Item oldItem, Item newItem, bool wholeMap, LayerFi
/// <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 { },
};
}

View File

@@ -7,9 +7,9 @@ public static class MapTileManagerUtil
/// </summary>
public static MapTileManager FromSaveFile(MainSave sav) => new()
{
ConfigTerrain = LayerPositionConfig.Create(7, 6, 16),
ConfigBuildings = LayerPositionConfig.Create(5, 4, 16),
ConfigItems = LayerPositionConfig.Create(sav.FieldItemAcreWidth, sav.FieldItemAcreHeight, 32),
ConfigTerrain = LayerPositionConfig.Create(7, 6, 16, 1),
ConfigBuildings = LayerPositionConfig.Create(5, 4, 16, 2),
ConfigItems = LayerPositionConfig.Create(sav.FieldItemAcreWidth, sav.FieldItemAcreHeight, 32, 1),
LayerTerrain = new LayerTerrain(sav.GetTerrainTiles(), sav.GetAcreBytes()),
LayerBuildings = new LayerBuilding { Buildings = sav.Buildings },

View File

@@ -129,4 +129,20 @@ public void SetViewToAcre(int acre)
var acreY = acre / (MaxX / EdgeBuffer);
SetViewTo(acreX * EdgeBuffer, acreY * EdgeBuffer);
}
public (int X, int Y) EnforceEdgeBuffer(int x, int y)
{
x = (int)Math.Clamp((uint)x, 0, MaxX - EdgeBuffer);
y = (int)Math.Clamp((uint)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;
}
}

View File

@@ -57,28 +57,4 @@ private static void ClampCoordinatesTo(ref int x, ref int y, int maxX, int maxY)
x = Math.Clamp(x, 0, maxX);
y = Math.Clamp(y, 0, 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 -= ViewWidth / 2;
y -= ViewWidth / 2;
}
// Clamp to viewport dimensions, and center to nearest acre if desired.
// Clamp to boundaries so that we always have a full grid to view.
SetTopLeftNearest(ref x, ref y);
}
private void SetTopLeftNearest(ref int x, ref int y)
{
int maxX = TotalWidth - ViewWidth;
int maxY = TotalHeight - ViewHeight;
ClampCoordinatesTo(ref x, ref y, maxX, maxY);
}
}

View File

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

View File

@@ -4,183 +4,298 @@
namespace NHSE.Sprites;
/// <summary>
/// Logic for rendering an <see cref="LayerItem"/>.
/// </summary>
public static class ItemLayerSprite
{
private static readonly Pen Reticle = new(Color.Red);
/// <summary>
/// Generates a bitmap representation of the provided item layer at a 1px-per-tile scale.
/// </summary>
/// <param name="layer">Item layer to render.</param>
public static Bitmap GetBitmapItemLayer(LayerItem layer)
{
var items = layer.Tiles;
var height = layer.TileInfo.TotalHeight;
var width = items.Length / height;
var imgHeight = layer.TileInfo.TotalHeight;
var imgWidth = items.Length / imgHeight;
var bmpData = new int[width * height];
LoadBitmapLayer(items, bmpData, width, height);
var bmpData = new int[imgWidth * imgHeight];
LoadBitmapLayer(items, bmpData, imgWidth, imgHeight);
return ImageUtil.GetBitmap(bmpData, width, height);
return ImageUtil.GetBitmap(bmpData, imgWidth, imgHeight);
}
private static void LoadBitmapLayer(ReadOnlySpan<Item> items, Span<int> bmpData, int width, int height)
/// <summary>
/// Populates a bitmap data buffer with color values derived from a collection of items, arranging the colors in column-major order based on the specified width and height.
/// </summary>
/// <remarks>
/// Each item's color is determined using FieldItemColor.GetItemColor and stored in ARGB format.
/// Colors are written to bmpData in column-major order, where each column is filled from top to bottom.
/// </remarks>
/// <param name="items">List of items from which color values are extracted. The span must contain at least width × height elements.</param>
/// <param name="bmpData">Pixel data for the bitmap. The span must have a length of at least width × height.</param>
/// <param name="imgWidth">The number of columns in the bitmap. Must be greater than zero.</param>
/// <param name="imgHeight">The number of rows in the bitmap. Must be greater than zero.</param>
private static void LoadBitmapLayer(ReadOnlySpan<Item> items, Span<int> bmpData, int imgWidth, int imgHeight)
{
for (int x = 0; x < width; x++)
for (int x = 0; x < imgWidth; x++)
{
var ix = x * height;
for (int y = 0; y < height; y++)
var ix = x * imgHeight;
for (int y = 0; y < imgHeight; y++)
{
var index = ix + y;
var tile = items[index];
bmpData[(y * width) + x] = FieldItemColor.GetItemColor(tile).ToArgb();
bmpData[(y * imgWidth) + x] = FieldItemColor.GetItemColor(tile).ToArgb();
}
}
}
private static void LoadPixelsFromLayer(LayerItem layer, int x0, int y0, int width, Span<int> bmpData)
/// <summary>
/// Loads an item layer into a bitmap, scaling it up to the desired size, drawing special symbols, and overlaying a grid.
/// </summary>
/// <param name="img">Inflated acre bitmap to write to.</param>
/// <param name="layer">Item layer to draw from.</param>
/// <param name="relX">Top-left X coordinate to start drawing from, relative to the origin of the layer (not map coordinates).</param>
/// <param name="relY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgSingle">Pixel data for 1px per tile image.</param>
/// <param name="imgUpscaled">>Pixel data for final inflated image.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="transparency">Optional transparency override color.</param>
/// <param name="gridlineColor">Color to use for gridlines.</param>
public static void LoadItemLayerViewGrid(Bitmap img, LayerItem layer, int relX, int relY,
Span<int> imgSingle, Span<int> imgUpscaled, int imgScale, int transparency = -1, int gridlineColor = 0)
{
var stride = layer.TileInfo.ViewWidth;
// Update the 1px view-grid image pixel data.
LoadViewport(imgSingle, layer, relX, relY);
for (int y = 0; y < stride; y++)
// Get the final inflated size of the image.
int imgWidth = layer.TileInfo.ViewWidth;
int h = layer.TileInfo.ViewHeight;
imgWidth *= imgScale;
h *= imgScale;
// Inflate to the final size storage.
ImageUtil.ScalePixelImage(imgSingle, imgUpscaled, imgWidth, h, imgScale);
// Optional transparency clamping to make image fainter.
if (transparency >>> 24 != 0xFF)
ImageUtil.ClampAllTransparencyTo(imgUpscaled, transparency);
// Draw symbols over special items now?
DrawDirectionals(imgUpscaled, layer, relX, relY, imgWidth, imgScale);
// Apply gridlines to visually separate each cell.
DrawGrid(imgUpscaled, imgWidth, h, gridlineColor, imgScale);
// Update the bitmap, final data.
img.SetBitmapData(imgUpscaled);
}
/// <summary>
/// Loads pixel data from the specified layer into the provided span, using the given starting coordinates and the layer's view dimensions.
/// </summary>
/// <param name="data">Pixel data of the final image.</param>
/// <param name="layer">The layer from which to load pixel data.</param>
/// <param name="relX">The x-coordinate of the upper-left corner in the layer from which to start loading pixels.</param>
/// <param name="relY">The y-coordinate of the upper-left corner in the layer from which to start loading pixels.</param>
private static void LoadViewport(Span<int> data, LayerItem layer, int relX, int relY)
{
var width = layer.TileInfo.ViewWidth;
var height = layer.TileInfo.ViewHeight;
for (int y = 0; y < height; y++)
{
var baseIndex = (y * width);
for (int x = 0; x < stride; x++)
for (int x = 0; x < width; x++)
{
var tile = layer.GetTile(x + x0, y + y0);
var tile = layer.GetTile(relX + x, relY + y);
var color = FieldItemColor.GetItemColor(tile).ToArgb();
var index = baseIndex + x;
bmpData[index] = color;
data[index] = color;
}
}
}
// non-allocation image generator
public static Bitmap GetBitmapItemLayerViewGrid(LayerItem layer, int x0, int y0, int scale, Span<int> acre1, int[] acreScale, Bitmap dest, int transparency = -1, int gridlineColor = 0)
/// <summary>
/// Draws extension tile info for the tiles in the specified layer onto the provided pixel data.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="layer">The layer containing the tiles to process.</param>
/// <param name="relX">Top-left X coordinate to start drawing from, relative to the origin of the layer (not map coordinates).</param>
/// <param name="relY">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
private static void DrawDirectionals(Span<int> data, LayerItem layer, int relX, int relY, int imgWidth, int imgScale)
{
int w = layer.TileInfo.ViewWidth;
int h = layer.TileInfo.ViewHeight;
LoadPixelsFromLayer(layer, x0, y0, w, acre1);
w *= scale;
h *= scale;
ImageUtil.ScalePixelImage(acre1, acreScale, w, h, scale);
var width = layer.TileInfo.ViewWidth;
var height = layer.TileInfo.ViewHeight;
if (transparency >>> 24 != 0xFF)
ImageUtil.ClampAllTransparencyTo(acreScale, transparency);
// draw symbols over special items now?
DrawDirectionals(acreScale, layer, w, x0, y0, scale);
// Slap on a grid
DrawGrid(acreScale, w, h, scale, gridlineColor);
// Return final data
ImageUtil.SetBitmapData(dest, acreScale);
return dest;
}
private static void DrawDirectionals(Span<int> data, LayerItem layer, int w, int x0, int y0, int scale)
{
for (int x = x0; x < x0 + layer.TileInfo.ViewWidth; x++)
for (int x = 0; x < width; x++)
{
for (int y = y0; y < y0 + layer.TileInfo.ViewHeight; y++)
for (int y = 0; y < height; y++)
{
var tile = layer.GetTile(x, y);
var pX = relX + x;
var pY = relY + y;
var tile = layer.GetTile(pX, pY);
if (tile.IsNone)
continue;
if (tile.IsBuried)
DrawX(data, (x - x0) * scale, (y - y0) * scale, scale, w);
else if (tile.IsDropped)
DrawPlus(data, (x - x0) * scale, (y - y0) * scale, scale, w);
else if (tile.IsExtension)
DrawDirectional(data, tile, (x - x0) * scale, (y - y0) * scale, scale, w);
// Apply cosmetic details based on the tile's details.
if (tile.IsBuried)
DrawX(data, relX * imgScale, relY * imgScale, imgScale, imgWidth);
else if (tile.IsDropped)
DrawPlus(data, relX * imgScale, relY * imgScale, imgScale, imgWidth);
else if (tile.IsExtension)
DrawDirectional(data, tile, relX * imgScale, relY * imgScale, imgScale, imgWidth);
// Based on the display item, apply details.
var id = tile.DisplayItemId;
var kind = ItemInfo.GetItemKind(id);
if (kind.IsFlowerGene(id))
{
int geneIndex;
if (tile.IsRoot)
{
geneIndex = 0;
}
else
{
geneIndex = (tile.ExtensionY << 1) | tile.ExtensionX;
tile = layer.GetTile(x - tile.ExtensionX, y - tile.ExtensionY);
}
var geneIndex = GetGeneIndex(ref tile, layer, pX, pY);
var genes = ((uint)tile.Genes ^ 0b00_11_00_00); // invert W bits
var geneValue = (genes >> (geneIndex * 2)) & 3;
if (geneValue == 0)
continue;
DrawGene(data, (x - x0) * scale, (y - y0) * scale, scale, w, geneValue, geneIndex);
DrawGene(data, relX * imgScale, relY * imgScale, imgScale, imgWidth, geneValue, geneIndex);
}
}
}
}
private static void DrawGene(Span<int> data, int x0, int y0, int scale, int w, uint geneValue, int geneIndex)
/// <summary>
/// Gets the index of the gene based on the specified extension value. Repoints the tile to root node if it is an extension tile.
/// </summary>
private static int GetGeneIndex(ref Item tile, LayerItem layer, int relX, int relY)
{
var c = ShiftToGeneCoordinate(ref x0, ref y0, scale, geneIndex);
FillSquare(data, x0, y0, scale / 2, w, c, geneValue == 3 ? 1 : 2);
if (tile.IsRoot)
return 0;
var geneIndex = (tile.ExtensionY << 1) | tile.ExtensionX;
tile = layer.GetTile(relX - tile.ExtensionX, relY - tile.ExtensionY);
return geneIndex;
}
private static void FillSquare(Span<int> data, int x0, int y0, int scale, int w, int color, int increment)
/// <summary>
/// Draws a flower gene on the item cell.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="geneValue">Value of the gene (0-3).</param>
/// <param name="geneIndex">0-3 value indicating which gene (quadrant) to draw.</param>
private static void DrawGene(Span<int> data, int x0, int y0, int imgScale, int imgWidth, uint geneValue, int geneIndex)
{
var baseIndex = (y0 * w) + x0;
for (int i = 0; i < scale * scale; i += increment)
var c = ShiftToGeneCoordinate(ref x0, ref y0, imgScale, geneIndex);
FillSquare(data, x0, y0, imgScale / 2, imgWidth, c, geneValue == 3 ? 1 : 2);
}
/// <summary>
/// Fills a square region within a one-dimensional span with the specified color value, using a given increment to control the fill step.
/// </summary>
/// <remarks>This method assumes that the specified region fits within the bounds of the provided span.
/// No bounds checking is performed.
/// The increment parameter can be used to fill a subset with the square's elements, which may be useful for performance or pattern effects.
/// </remarks>
/// <param name="data">The span representing the target buffer to fill. Each element corresponds to a pixel or cell in a row-major order grid.</param>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="color">The color value to assign to each filled element in the square region.</param>
/// <param name="increment">The step size to use when filling elements. Must be positive. A larger increment skips more elements within the square.</param>
private static void FillSquare(Span<int> data, int x0, int y0, int imgScale, int imgWidth, int color, int increment)
{
var baseIndex = (y0 * imgWidth) + x0;
for (int i = 0; i < imgScale * imgScale; i += increment)
{
var x = i % scale;
var y = i / scale;
var index = (y * w) + x;
var x = i % imgScale;
var y = i / imgScale;
var index = (y * imgWidth) + x;
data[baseIndex + index] = color;
}
}
private static int ShiftToGeneCoordinate(ref int x0, ref int y0, int scale, int geneIndex)
/// <summary>
/// Adjusts the specified coordinates to the center of a gene region based on the given gene index and scale, and returns the corresponding ARGB color value for that region.
/// </summary>
/// <remarks>
/// The method modifies the input coordinates in place according to the specified gene region.
/// The returned color value corresponds to the region: Red for bottom right, Yellow for bottom left, AntiqueWhite for top right, and Black for top left or any other index.
/// </remarks>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scale of the entire cell.</param>
/// <param name="geneIndex">The index of the gene region to shift to. Valid values are 0 (bottom right), 1 (bottom left), 2 (top right), and any other value for top left.</param>
/// <returns>An integer representing the ARGB color value associated with the specified gene region.</returns>
private static int ShiftToGeneCoordinate(ref int x0, ref int y0, int imgScale, int geneIndex)
{
switch (geneIndex)
{
case 0: // bottom right
x0 += scale / 2;
y0 += scale / 2;
x0 += imgScale / 2;
y0 += imgScale / 2;
return Color.Red.ToArgb();
case 1: // bottom left
y0 += scale / 2;
y0 += imgScale / 2;
return Color.Yellow.ToArgb();
case 2: // top right
x0 += scale / 2;
x0 += imgScale / 2;
return Color.AntiqueWhite.ToArgb();
default: // top left
return Color.Black.ToArgb();
}
}
private static void DrawPlus(Span<int> data, int x0, int y0, int scale, int w)
/// <summary>
/// Updates the pixel data to draw a `+`, indicating the item as "dropped".
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawPlus(Span<int> data, int x0, int y0, int imgScale, int imgWidth)
{
var x0y0 = (w * y0) + x0;
var s2 = scale / 2;
var ws2 = w * s2;
var x0y0 = (imgWidth * y0) + x0;
var s2 = imgScale / 2;
var ws2 = imgWidth * s2;
var v0 = x0y0 + s2;
var h0 = x0y0 + ws2;
for (int x = scale / 4; x <= 3 * (scale / 4); x++)
for (int x = imgScale / 4; x <= 3 * (imgScale / 4); x++)
{
var vert = v0 + (w * x);
var vert = v0 + (imgWidth * x);
data[vert] ^= 0x808080;
var hori = h0 + x;
data[hori] ^= 0x808080;
}
}
private static void DrawX(Span<int> data, int x0, int y0, int scale, int w)
/// <summary>
/// Updates the pixel data to draw an `X`, indicating the item as "buried".
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawX(Span<int> data, int x0, int y0, int imgScale, int imgWidth)
{
var opposite = scale - 1;
var wo = w * opposite;
var opposite = imgScale - 1;
var wo = imgWidth * opposite;
// Starting offsets for each of the slashes
var bBackward = (w * y0) + x0; // Backwards \
var bBackward = (imgWidth * y0) + x0; // Backwards \
var bForward = bBackward + wo; // Forwards /
for (int x = 0; x < scale; x++)
for (int x = 0; x < imgScale; x++)
{
var wx = w * x;
var wx = imgWidth * x;
var backward = bBackward + x + wx;
data[backward] ^= 0x808080;
var forward = bForward + x - wx;
@@ -188,32 +303,49 @@ private static void DrawX(Span<int> data, int x0, int y0, int scale, int w)
}
}
private static void DrawDirectional(Span<int> data, Item tile, int x0, int y0, int scale, int w)
/// <summary>
/// Updates the pixel data to draw a directional, indicating the item as an extension of a root item node.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="tile">Extension tile data.</param>
/// <param name="x0">Top-left X coordinate to start drawing from.</param>
/// <param name="y0">Top-left Y coordinate to start drawing from.</param>
/// <param name="imgScale">Scaling factor from 1px => final image dimensions.</param>
/// <param name="imgWidth">Width of the entire image.</param>
private static void DrawDirectional(Span<int> data, Item tile, int x0, int y0, int imgScale, int imgWidth)
{
var eX = tile.ExtensionX;
var eY = tile.ExtensionY;
if (eX == 0 && eY == 0)
return;
var sum = eX + eY;
var start = scale / (sum + 1);
var start = imgScale / (sum + 1);
var startX = eX >= eY ? 0 : start;
var startY = eX <= eY ? 0 : start;
var baseIndex = (w * y0) + x0;
for (int x = startX, y = startY; x < scale && y < scale; x += eX, y += eY)
var baseIndex = (imgWidth * y0) + x0;
for (int x = startX, y = startY; x < imgScale && y < imgScale; x += eX, y += eY)
{
var index = baseIndex + (w * y) + x;
data[index] ^= 0x808080;
var index = baseIndex + (imgWidth * y) + x;
data[index] ^= 0x00_808080;
}
}
public static void DrawGrid(Span<int> data, int w, int h, int scale, int gridlineColor)
/// <summary>
/// Draws gridlines on the provided pixel data at the specified scale.
/// </summary>
/// <param name="data">Pixel data of the entire image.</param>
/// <param name="imgWidth">Width of the entire image.</param>
/// <param name="imgHeight">Height of the entire image.</param>
/// <param name="gridlineColor">Color to use for gridlines.</param>
/// <param name="gridlineInterval">Pixel interval to draw gridlines.</param>
public static void DrawGrid(Span<int> data, int imgWidth, int imgHeight, int gridlineColor, int gridlineInterval)
{
// Horizontal Lines
for (int y = scale; y < h; y += scale)
for (int y = gridlineInterval; y < imgHeight; y += gridlineInterval)
{
var baseIndex = y * w;
for (int x = 0; x < w; x++)
var baseIndex = y * imgWidth;
for (int x = 0; x < imgWidth; x++)
{
var index = baseIndex + x;
data[index] = gridlineColor;
@@ -221,10 +353,10 @@ public static void DrawGrid(Span<int> data, int w, int h, int scale, int gridlin
}
// Vertical Lines
for (int y = 0; y < h; y++)
for (int y = 0; y < imgHeight; y++)
{
var baseIndex = y * w;
for (int x = scale; x < w; x += scale)
var baseIndex = y * imgWidth;
for (int x = gridlineInterval; x < imgWidth; x += gridlineInterval)
{
var index = baseIndex + x;
data[index] = gridlineColor;
@@ -232,23 +364,26 @@ public static void DrawGrid(Span<int> data, int w, int h, int scale, int gridlin
}
}
public static Bitmap GetBitmapItemLayer(LayerItem layer, int x, int y, int[] data, Bitmap dest, int transparency = -1)
public static Bitmap GetBitmapItemLayer(Bitmap dest, LayerItem layer, int topX, int topY, Span<int> data, int transparency = -1)
{
LoadBitmapLayer(layer.Tiles, data, layer.TileInfo.TotalWidth, layer.TileInfo.TotalHeight);
if (transparency >>> 24 != 0xFF)
ImageUtil.ClampAllTransparencyTo(data, transparency);
ImageUtil.SetBitmapData(dest, data);
return DrawViewReticle(dest, layer.TileInfo, x, y);
dest.SetBitmapData(data);
return DrawViewReticle(dest, layer.TileInfo, topX, topY);
}
private static Bitmap DrawViewReticle(Bitmap map, TileGridViewport g, int x, int y, int scale = 1)
private static Bitmap DrawViewReticle(Bitmap map, TileGridViewport g, int topX, int topY, int scale = 1)
{
using var gfx = Graphics.FromImage(map);
using var pen = new Pen(Color.Red);
int w = g.ViewWidth * scale;
int h = g.ViewHeight * scale;
gfx.DrawRectangle(pen, x * scale, y * scale, w, h);
gfx.DrawViewReticle(g, topX, topY, scale);
return map;
}
private static void DrawViewReticle(this Graphics gfx, TileGridViewport g, int topX, int topY, int scale)
{
int w = g.ViewWidth * scale;
int h = g.ViewHeight * scale;
gfx.DrawRectangle(Reticle, topX * scale, topY * scale, w, h);
}
}

View File

@@ -10,10 +10,9 @@ namespace NHSE.Sprites;
public sealed class MapRenderer : IDisposable
{
private readonly MapEditor Map;
private int AcreScale => Map.MapScale;
private int MapScale => Map.MapScale * 2;
private int MapScale => Map.MapScale;
private int ViewScale => Map.ViewScale;
private const byte ScaleAsMap = 2;
private const byte FieldItemWidthOld = 7;
private const byte FieldItemWidthNew = 9;
@@ -39,17 +38,17 @@ public MapRenderer(MapEditor m)
var l1 = m.Mutator.Manager.FieldItems.Layer0;
var info = l1.TileInfo;
PixelsItemAcre1 = new int[info.ViewWidth * info.ViewHeight];
PixelsItemAcreX = new int[PixelsItemAcre1.Length * AcreScale * AcreScale];
ScaleAcre = new Bitmap(info.ViewWidth * AcreScale, info.ViewHeight * AcreScale);
PixelsItemAcreX = new int[PixelsItemAcre1.Length * ViewScale * ViewScale];
ScaleAcre = new Bitmap(info.ViewWidth * ViewScale, info.ViewHeight * ViewScale);
PixelsItemMap = new int[info.TotalWidth * info.TotalHeight * MapScale * MapScale];
MapReticle = new Bitmap(info.TotalWidth * MapScale, info.TotalHeight * MapScale);
PixelsBackgroundAcre1 = new int[(int)Math.Pow(16, 4)];
PixelsBackgroundAcre1 = new int[PixelsItemAcre1.Length];
PixelsBackgroundAcreX = new int[PixelsItemAcreX.Length];
BackgroundAcre = new Bitmap(ScaleAcre.Width, ScaleAcre.Height);
PixelsBackgroundMap1 = new int[PixelsItemMap.Length / 4];
PixelsBackgroundMap1 = new int[PixelsItemMap.Length / (MapScale * MapScale)];
PixelsBackgroundMapX = new int[PixelsItemMap.Length];
BackgroundMap = new Bitmap(MapReticle.Width, MapReticle.Height);
}
@@ -67,7 +66,7 @@ public void Dispose()
public Bitmap GetBackgroundTerrain(int index = -1)
{
return TerrainSprite.GetMapWithBuildings(Map, null, PixelsBackgroundMap1, PixelsBackgroundMapX, BackgroundMap, ScaleAsMap, index);
return TerrainSprite.GetMapWithBuildings(Map, null, PixelsBackgroundMap1, PixelsBackgroundMapX, BackgroundMap, index);
}
public Bitmap GetInflatedImage(Bitmap regular)
@@ -90,12 +89,14 @@ public Bitmap GetInflatedImage(Bitmap regular)
private Bitmap GetLayerAcre(int topX, int topY, int transparency)
{
var layer = Map.Mutator.CurrentLayer;
return ItemLayerSprite.GetBitmapItemLayerViewGrid(layer, topX, topY, AcreScale, PixelsItemAcre1, PixelsItemAcreX, ScaleAcre, transparency);
ItemLayerSprite.LoadItemLayerViewGrid(ScaleAcre, layer, topX, topY, PixelsItemAcre1, PixelsItemAcreX, ViewScale, transparency);
return ScaleAcre;
}
public Bitmap GetBackgroundAcre(Font f, byte transparencyBuilding, byte transparencyTerrain, int index = -1)
{
return TerrainSprite.CreateAcreView(this, f, PixelsBackgroundAcre1, PixelsBackgroundAcreX, BackgroundAcre, index, transparencyBuilding, transparencyTerrain);
TerrainSprite.LoadViewport(BackgroundAcre, Map, f, PixelsBackgroundAcre1, PixelsBackgroundAcreX, index, transparencyBuilding, transparencyTerrain);
return BackgroundAcre;
}
private Bitmap GetMapWithReticle(int topX, int topY, int t, LayerFieldItem layerField)

View File

@@ -5,6 +5,9 @@
namespace NHSE.Sprites;
/// <summary>
/// Logic to build a viewport render of a subsection of a map (or the entire "map", assuming it is small enough).
/// </summary>
public static class TerrainSprite
{
private static readonly Brush Selected = Brushes.Red;
@@ -12,17 +15,113 @@ public static class TerrainSprite
private static readonly Brush Text = Brushes.White;
private static readonly Brush Tile = Brushes.Black;
private static readonly Brush Plaza = Brushes.RosyBrown;
private static readonly Color PlazaColor = Color.RosyBrown;
private static readonly StringFormat BuildingTextFormat = new() { Alignment = StringAlignment.Center, LineAlignment = StringAlignment.Center };
private const int PlazaWidth = 6 * 2;
private const int PlazaHeight = 5 * 2;
// 6x5 in a 16x16 acre scale. Since we are in 32x32 scale for items, double.
private const int Scale = 2;
private const int PlazaWidth = 6 * Scale;
private const int PlazaHeight = 5 * Scale;
public static void CreateMap(LayerTerrain mgr, Span<int> pixels)
public static void GenerateMap(Bitmap map, MapMutator mut, Span<int> scale1, Span<int> scaleX, int imgScale, int acreIndex = -1)
{
// Load the terrain pixels, then upscale.
var mgr = mut.Manager.LayerTerrain;
LoadTerrainPixels(mgr, scale1);
ImageUtil.ScalePixelImage(scale1, scaleX, map.Width, map.Height, imgScale);
map.SetBitmapData(scaleX);
if (acreIndex < 0)
return;
var acre = AcreCoordinate.Acres[acreIndex];
var x = acre.X * mgr.TileInfo.ViewWidth;
var y = acre.Y * mgr.TileInfo.ViewHeight;
DrawReticle(map, mgr.TileInfo, x, y, imgScale);
}
public static Bitmap GetMapWithBuildings(MapEditor m, Font? f, Span<int> scale1, Span<int> scaleX, Bitmap map, int buildingIndex = -1)
{
GenerateMap(map, m.Mutator, scale1, scaleX, m.MapScale);
using var gfx = Graphics.FromImage(map);
var plaza = m.Mutator.Manager.Plaza;
gfx.DrawPlaza(m, (ushort)plaza.X, (ushort)plaza.Z, m.MapScale);
gfx.DrawBuildings(m, m.Buildings.Buildings, m.MapScale, f, buildingIndex);
return map;
}
public static void LoadViewport(Bitmap img, MapEditor m, Font f, Span<int> scale1, Span<int> scaleX, int index, byte transBuild, byte transTerrain)
{
// Convert from absolute to relative.
int mx = m.X / 2;
int my = m.Y / 2;
SetAcreTerrainPixels(m.Terrain, mx, my, scale1, scaleX, m.ViewScale);
// Drawing building tiles currently uses the graphics API rather than writing pixels.
img.SetBitmapData(scaleX);
using var gfx = Graphics.FromImage(img);
gfx.DrawViewPlaza(m, transBuild);
gfx.DrawViewBuildings(m, index, transBuild);
// Return to pixel writing mode
img.GetBitmapData(scaleX);
// Apply Grid
const int grid1 = unchecked((int)0xFF888888u); // lighter
const int grid2 = unchecked((int)0xFF666666u); // darker
ItemLayerSprite.DrawGrid(scaleX, img.Width, img.Height, grid1, m.ViewScale); // minor
ItemLayerSprite.DrawGrid(scaleX, img.Width, img.Height, grid2, m.ViewScale * 2); // major
// Switch back to graphics mode
img.SetBitmapData(scaleX);
// Draw Text of Building Names
foreach (var b in m.Buildings.Buildings)
{
var type = b.BuildingType;
var (x, y) = m.GetViewCoordinatesBuilding(b.X, b.Y);
const int cellsAbove = 2; // Show label above the building, 2 cells up.
y -= (m.ViewScale * cellsAbove);
// Don't bother drawing if not in view.
if (!m.Mutator.View.IsWithinView(m.ViewScale, x, y))
continue;
var name = b.BuildingType.ToString();
var labelPosition = new PointF(x, y - (m.ViewScale * 2));
gfx.DrawString(name, f, Text, labelPosition, BuildingTextFormat);
}
// Draw Text of Terrain Tile Names
if (transTerrain != 0)
DrawTerrainTileNames(gfx, f, mx, my, m.Terrain, m.ViewScale, transTerrain);
// Done.
}
private static void DrawViewBuildings(this Graphics gfx, MapEditor m, int selectedBuildingIndex, byte transBuild)
{
var buildings = m.Buildings.Buildings;
for (var i = 0; i < buildings.Count; i++)
{
var b = buildings[i];
var pen = selectedBuildingIndex == i ? Selected : Others;
if (transBuild != byte.MaxValue)
{
var orig = ((SolidBrush)pen).Color;
pen = new SolidBrush(Color.FromArgb(transBuild, orig));
}
gfx.DrawBuilding(b, m, pen, m.ViewScale, Text);
}
}
private static void LoadTerrainPixels(LayerTerrain mgr, Span<int> pixels)
{
// Populate the image, with each pixel being a single tile.
int width = mgr.TileInfo.TotalWidth;
int height = mgr.TileInfo.TotalHeight;
int i = 0;
var width = mgr.TileInfo.TotalWidth;
var height = mgr.TileInfo.TotalHeight;
var i = 0;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++, i++)
@@ -30,22 +129,6 @@ public static void CreateMap(LayerTerrain mgr, Span<int> pixels)
}
}
public static Bitmap CreateMap(LayerTerrain mgr, Span<int> scale1, Span<int> scaleX, Bitmap map, int scale, int acreIndex = -1)
{
CreateMap(mgr, scale1);
ImageUtil.ScalePixelImage(scale1, scaleX, map.Width, map.Height, scale);
map.SetBitmapData(scaleX);
if (acreIndex < 0)
return map;
var acre = AcreCoordinate.Acres[acreIndex];
var x = acre.X * mgr.TileInfo.ViewWidth;
var y = acre.Y * mgr.TileInfo.ViewHeight;
return DrawReticle(map, mgr.TileInfo, x, y, scale);
}
private static Bitmap DrawReticle(Bitmap map, TileGridViewport mgr, int x, int y, int scale)
{
using var gfx = Graphics.FromImage(map);
@@ -57,70 +140,75 @@ private static Bitmap DrawReticle(Bitmap map, TileGridViewport mgr, int x, int y
return map;
}
public static Bitmap GetMapWithBuildings(MapTerrainStructure m, Font? f, Span<int> scale1, Span<int> scaleX, Bitmap map, int scale = 4, int index = -1)
private static void DrawPlaza(this Graphics gfx, MapEditor map, ushort px, ushort py, int scale)
{
CreateMap(m.Terrain, scale1, scaleX, map, scale);
using var gfx = Graphics.FromImage(map);
var (x, y) = map.GetViewCoordinatesBuilding(px, py);
gfx.DrawPlaza(m.Terrain, (ushort)m.PlazaX, (ushort)m.PlazaY, scale);
gfx.DrawBuildings(m.Terrain, m.Buildings, f, scale, index);
return map;
}
private static void DrawPlaza(this Graphics gfx, LayerTerrain g, ushort px, ushort py, int scale)
{
g.GetBuildingCoordinate(px, py, scale, out var x, out var y);
var width = scale * PlazaWidth;
var height = scale * PlazaHeight;
int width = scale * PlazaWidth;
int height = scale * PlazaHeight;
gfx.FillRectangle(Plaza, x, y, width, height);
}
private static void DrawBuildings(this Graphics gfx, LayerTerrain g, IReadOnlyList<Building> buildings, Font? f, int scale, int index = -1)
private static void DrawBuildings(this Graphics gfx, MapEditor map, IReadOnlyList<Building> buildings, int imgScale, Font? textFont = null, int selectedBuildingIndex = -1)
{
for (int i = 0; i < buildings.Count; i++)
{
var b = buildings[i];
if (b.BuildingType == 0)
continue;
g.GetBuildingCoordinate(b.X, b.Y, scale, out var x, out var y);
var pen = index == i ? Selected : Others;
DrawBuilding(gfx, f, scale, pen, x, y, b, Text);
var pen = selectedBuildingIndex == i ? Selected : Others;
gfx.DrawBuilding(b, map, pen, imgScale, Text, textFont);
}
}
private static void DrawBuilding(Graphics gfx, Font? f, int scale, Brush pen, int x, int y, Building b, Brush text)
private static void DrawBuilding(this Graphics gfx, Building b, MapEditor map, Brush bBrush, int imgScale, Brush textBrush, Font? textFont = null)
{
gfx.FillRectangle(pen, x - scale, y - scale, scale * 2, scale * 2);
var (x, y) = map.GetViewCoordinatesBuilding(b.X, b.Y);
var type = b.BuildingType;
var (width, height) = type.GetDimensions();
if (f == null)
// Draw the building.
x -= width / 2;
y -= height / 2;
gfx.FillRectangle(bBrush, x, y, width * imgScale, height * imgScale);
if (textFont == null)
return;
var name = b.BuildingType.ToString();
gfx.DrawString(name, f, text, new PointF(x, y - (scale * 2)), BuildingTextFormat);
// Draw the text label above it.
const int cellsAbove = 2;
var name = type.ToString();
gfx.DrawString(name, textFont, textBrush, new PointF(x, y - (imgScale * cellsAbove)), BuildingTextFormat);
}
private static void SetAcreTerrainPixels(int x, int y, LayerTerrain t, Span<int> data, Span<int> scaleX, int scale)
private static void SetAcreTerrainPixels(LayerTerrain t, int relX, int relY, Span<int> data, Span<int> scaleX, int imgScale)
{
GetAcre1(x, y, t, data);
ImageUtil.ScalePixelImage(data, scaleX, 16 * scale, 16 * scale, scale / 16);
GetAcre1(t, relX, relY, data);
ImageUtil.ScalePixelImage(data, scaleX, TilesPerViewport * imgScale, TilesPerViewport * imgScale, imgScale / TileScale);
}
private static void GetAcre1(int tileTopX, int tileTopY, LayerTerrain t, Span<int> data)
// Tiles are always rendered as 16x16 squares, to match the precomputed tile appearance bitmaps.
private const int TileScale = 16;
private const int TilesPerViewport = 16;
private static void GetAcre1(LayerTerrain t, int relX, int relY, Span<int> data)
{
const int viewWidth = 16;
const int viewHeight = 16;
int index = 0;
for (int tileY = 0; tileY < 16; tileY++)
for (int tileY = 0; tileY < viewHeight; tileY++)
{
var tileYIx = tileY + tileTopY;
for (int pixelY = 0; pixelY < 16; pixelY++)
var tileYIx = tileY + relY;
for (int pixelY = 0; pixelY < viewWidth; pixelY++)
{
for (int tileX = 0; tileX < 16; tileX++)
// Draw the tile row
for (int tileX = 0; tileX < TileScale; tileX++)
{
var tileXIx = tileX + tileTopX;
for (int pixelX = 0; pixelX < 16; pixelX++)
var tileXIx = tileX + relX;
for (int pixelX = 0; pixelX < TileScale; pixelX++)
{
data[index] = t.GetTileColor(tileXIx, tileYIx, pixelX, pixelY);
index++;
@@ -130,63 +218,11 @@ private static void GetAcre1(int tileTopX, int tileTopY, LayerTerrain t, Span<in
}
}
public static Bitmap CreateAcreView(MapEditor m, Font f, Span<int> scale1, Span<int> scaleX, Bitmap acre, int index, byte tbuild, byte tterrain)
private static void DrawTerrainTileNames(Graphics gfx, Font f, int topX, int topY, LayerTerrain t, int scale, byte transparency)
{
// Convert from absolute to relative.
int mx = m.X / 2;
int my = m.Y / 2;
SetAcreTerrainPixels(mx, my, m.Terrain, scale1, scaleX, m.Terrain.Scale);
const int grid1 = unchecked((int)0xFF888888u);
const int grid2 = unchecked((int)0xFF666666u);
acre.SetBitmapData(scaleX);
using var gfx = Graphics.FromImage(acre);
var plaza = m.Mutator.Manager.Plaza;
gfx.DrawAcrePlaza(m.Terrain, mx, my, plaza.X, plaza.Z, m.Terrain.Scale, tbuild);
var buildings = m.Buildings.Buildings;
var t = m.Terrain;
for (var i = 0; i < buildings.Count; i++)
{
var b = buildings[i];
t.GetBuildingRelativeCoordinates(mx, my, m.Terrain.Scale, b.X, b.Y, out var x, out var y);
var pen = index == i ? Selected : Others;
if (tbuild != byte.MaxValue)
{
var orig = ((SolidBrush)pen).Color;
pen = new SolidBrush(Color.FromArgb(tbuild, orig));
}
DrawBuilding(gfx, null, m.Terrain.Scale, pen, x, y, b, Text);
}
acre.GetBitmapData(scaleX);
ItemLayerSprite.DrawGrid(scaleX, acre.Width, acre.Height, m.AcreScale, grid1);
ItemLayerSprite.DrawGrid(scaleX, acre.Width, acre.Height, m.Terrain.Scale, grid2);
acre.SetBitmapData(scaleX);
foreach (var b in buildings)
{
t.GetBuildingRelativeCoordinates(mx, my, m.Terrain.Scale, b.X, b.Y, out var x, out var y);
if (!t.IsWithinGrid(m.Terrain.Scale, x, y))
continue;
var name = b.BuildingType.ToString();
var labelPosition = new PointF(x, y - (m.Terrain.Scale * 2));
gfx.DrawString(name, f, Text, labelPosition, BuildingTextFormat);
}
if (tterrain != 0)
DrawTerrainTileNames(mx, my, gfx, t, f, m.Terrain.Scale, tterrain);
return acre;
}
private static void DrawTerrainTileNames(int topX, int topY, Graphics gfx, LayerTerrain t, Font f, int scale, byte transparency)
{
var pen = transparency != byte.MaxValue ? new SolidBrush(Color.FromArgb(transparency, Color.Black)) : Tile;
var pen = Tile;
if (transparency != byte.MaxValue)
pen = new SolidBrush(Color.FromArgb(transparency, Color.Black));
for (int y = 0; y < 16; y++)
{
@@ -204,19 +240,18 @@ private static void DrawTerrainTileNames(int topX, int topY, Graphics gfx, Layer
}
}
private static void DrawAcrePlaza(this Graphics gfx, LayerTerrain g, int topX, int topY, uint plazaX, uint plazaY, int scale, byte transparency)
private static void DrawViewPlaza(this Graphics gfx, MapEditor m, byte transparency)
{
g.GetBuildingRelativeCoordinates(topX, topY, scale, plazaX, plazaY, out var x, out var y);
var plaza = m.Mutator.Manager.Plaza;
var (x, y) = m.GetViewCoordinatesBuilding(plaza.X, plaza.Z);
var scale = m.ViewScale;
var width = scale * PlazaWidth;
var height = scale * PlazaHeight;
var pen = Plaza;
if (transparency != byte.MaxValue)
{
var orig = ((SolidBrush)pen).Color;
pen = new SolidBrush(Color.FromArgb(transparency, orig));
}
pen = new SolidBrush(Color.FromArgb(transparency, PlazaColor));
gfx.FillRectangle(pen, x, y, width, height);
}
}

View File

@@ -111,39 +111,37 @@ public static Bitmap ResizeImage(Image image, int width, int height)
return destImage;
}
public static int[] ScalePixelImage(ReadOnlySpan<int> data, int scale, int w, int h, out int fW, out int fH)
public static int[] ScalePixelImage(ReadOnlySpan<int> data, int imgScale, int imgWidthSingle, int imgHeightSingle, out int imgWidthUpscaled, out int imgHeightUpscaled)
{
fW = scale * w;
fH = scale * h;
var scaled = new int[fW * fH];
ScalePixelImage(data, scaled, fW, fH, scale);
imgWidthUpscaled = imgScale * imgWidthSingle;
imgHeightUpscaled = imgScale * imgHeightSingle;
var scaled = new int[imgWidthUpscaled * imgHeightUpscaled];
ScalePixelImage(data, scaled, imgWidthUpscaled, imgHeightUpscaled, imgScale);
return scaled;
}
public static void ScalePixelImage(ReadOnlySpan<int> data, Span<int> scaled, int fW, int fH, int scale)
public static void ScalePixelImage(ReadOnlySpan<int> data, Span<int> scaled, int imgWidth, int imgHeight, int imgScale)
{
// For each pixel, copy to the X indexes, then block copy the row to the other rows.
int i = 0;
for (int y = 0; y < fH; y += scale)
for (int y = 0; y < imgHeight; y += imgScale)
{
// Fill the X pixels
var baseIndex = y * fW;
for (int x = 0; x < fW; x += scale)
var baseIndex = y * imgWidth;
for (int x = 0; x < imgWidth; x += imgScale)
{
var v = data[i];
var xi = baseIndex + x;
for (int x1 = 0; x1 < scale; x1++)
for (int x1 = 0; x1 < imgScale; x1++)
scaled[xi + x1] = v;
i++;
}
// Copy entire pixel row down
for (int y1 = 1; y1 < scale; y1++)
for (int y1 = 1; y1 < imgScale; y1++)
{
var src = scaled.Slice(baseIndex, fW);
var dest = scaled.Slice(baseIndex + (y1 * fW), fW);
var src = scaled.Slice(baseIndex, imgWidth);
var dest = scaled.Slice(baseIndex + (y1 * imgWidth), imgWidth);
src.CopyTo(dest);
}
}

View File

@@ -206,14 +206,17 @@ private void SaveMain()
#region Player Editing
private void LoadPlayers()
{
// A valid save file has at least one player (main player).
if (SAV.Players.Count == 0)
throw new Exception("No players found in the loaded directory.");
// Load the player names to the selection box.
CB_Players.Items.Clear();
var playerList = SAV.Players.Select(z => z.DirectoryName);
foreach (var p in playerList)
CB_Players.Items.Add(p);
// Trigger a load.
PlayerIndex = -1;
CB_Players.SelectedIndex = 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -15,6 +15,7 @@ public sealed partial class FieldItemEditor : Form, IItemLayerEditor
{
private readonly MainSave SAV;
private readonly MapEditor Editor;
private readonly MapRenderer Renderer;
private MapViewState View => Editor.Mutator.View;
private MapTileManager Map => Editor.Mutator.Manager;
@@ -52,7 +53,7 @@ public FieldItemEditor(MainSave sav)
IsExtendedMap30 = sav.FieldItemAcreWidth != 7;
Editor = MapEditor.FromSaveFile(sav);
Editor.MapScale = scale;
Editor.AcreScale = scale;
Editor.ViewScale = scale;
Renderer = new MapRenderer(Editor);
Loading = true;
@@ -129,24 +130,22 @@ private void LoadItemGridAcre()
private void ReloadMapBackground()
{
var img = View.GetBackgroundTerrain(SelectedBuildingIndex);
var img = Renderer.GetBackgroundTerrain(SelectedBuildingIndex);
SetMapBackgroundImage(img);
}
private void ReloadMapItemGrid() => SetMapForegroundImage(View.GetMapWithReticle(GetItemTransparency()));
private void ReloadMapItemGrid() => SetMapForegroundImage(Renderer.GetMapWithReticle(GetItemTransparency()));
private void SetMapBackgroundImage(Bitmap img)
{
if (IsExtendedMap30)
img = View.GetInflatedImage(img);
img = Renderer.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;
}
@@ -154,12 +153,12 @@ private void ReloadAcreBackground()
{
var tbuild = (byte)TR_BuildingTransparency.Value;
var tterrain = (byte)TR_Terrain.Value;
var img = View.GetBackgroundAcre(L_Coordinates.Font, tbuild, tterrain, SelectedBuildingIndex);
var img = Renderer.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 ReloadAcreItemGrid() => PB_Acre.Image = View.GetLayerAcre(GetItemTransparency());
private void ReloadAcreItemGrid() => PB_Acre.Image = Renderer.GetLayerAcre(GetItemTransparency());
public void ReloadItems()
{
@@ -303,8 +302,8 @@ private void SetHoveredItem(MouseEventArgs e)
private void GetAcreCoordinates(MouseEventArgs e, out int x, out int y)
{
x = e.X / Editor.AcreScale;
y = e.Y / Editor.AcreScale;
x = e.X / Editor.ViewScale;
y = e.Y / Editor.ViewScale;
}
private void PB_Acre_MouseDown(object sender, MouseEventArgs e) => ResetDrag();
@@ -772,58 +771,30 @@ private void PB_Map_MouseDown(object sender, MouseEventArgs e)
{
if (e.Button != MouseButtons.Left)
return;
ClickMapAt(e, true);
ClickMapAt(e);
}
private void ClickMapAt(MouseEventArgs e, bool skipLagCheck)
private void ClickMapAt(MouseEventArgs e)
{
var layer = Editor.Items.Layer0;
int mX = e.X;
int mY = e.Y;
bool centerReticle = CHK_SnapToAcre.Checked;
View.GetViewAnchorCoordinates(mX, mY, out var x, out var y, centerReticle);
var (x, y) = Editor.GetMapCoordinates(e.X, e.Y, CHK_SnapToAcre.Checked ? MapViewCoordinateRequest.SnapAcre : MapViewCoordinateRequest.Centered);
// Truncate to root-node coordinates. The map is only 1px per tile, and nobody is wanting to click on extension-tiles.
x &= 0xFFFE;
y &= 0xFFFE;
var acre = layer.GetAcre(x, y);
bool sameAcre = AcreIndex == acre;
if (!skipLagCheck)
{
if (CHK_SnapToAcre.Checked)
{
if (sameAcre)
return;
}
else
{
const int delta = 0; // disabled = 0
var dx = Math.Abs(View.X - x);
var dy = Math.Abs(View.Y - y);
if (dx <= delta && dy <= delta && !sameAcre)
return;
}
}
if (!CHK_SnapToAcre.Checked)
{
if (View.SetViewTo(x, y))
LoadItemGridAcre();
return;
}
if (!sameAcre)
CB_Acre.SelectedIndex = acre;
if (View.SetViewTo(x, y))
LoadItemGridAcre();
}
private void PB_Map_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ClickMapAt(e, false);
ClickMapAt(e);
}
else if (e.Button == MouseButtons.None)
{
View.GetCursorCoordinates(e.X, e.Y, out var x, out var y);
Editor.GetCursorCoordinates(e.X, e.Y, out var x, out var y);
SetCoordinateText(x, y);
}
}

View File

@@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
@@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->

View File

@@ -167,9 +167,10 @@ private void DrawRoom(LayerItem layer)
var w = layer.TileInfo.TotalWidth;
var h = layer.TileInfo.TotalHeight;
Span<int> scale1 = stackalloc int[w * h];
int[] scaleX = new int[scale * scale * scale1.Length];
var scaleX = new int[scale * scale * scale1.Length];
var bmp = new Bitmap(scale * w, scale * h);
PB_Room.Image = ItemLayerSprite.GetBitmapItemLayerViewGrid(layer, 0, 0, scale, scale1, scaleX, bmp, gridlineColor: 0x7F000000);
ItemLayerSprite.LoadItemLayerViewGrid(bmp, layer, 0, 0, scale1, scaleX, scale, gridlineColor: 0x7F000000);
PB_Room.Image = bmp;
}
private void NUD_Room_ValueChanged(object sender, EventArgs e)

View File

@@ -1,7 +1,7 @@
using System.Linq;
using System;
using System.Linq;
using System.Windows.Forms;
using NHSE.Core;
using NHSE.Injection;
namespace NHSE.WinForms;
@@ -17,10 +17,10 @@ public SimpleHexEditor(byte[] originalBytes)
Bytes = originalBytes;
}
private void Update_Click(object sender, System.EventArgs e)
private void Update_Click(object sender, EventArgs e)
{
var bytestring = RTB_RAM.Text.Replace("\t", "").Replace(" ", "").Trim();
Bytes = Decoder.StringToByteArray(bytestring);
Bytes = Convert.FromHexString(bytestring);
DialogResult = DialogResult.OK;
Close();
}

View File

@@ -8,10 +8,10 @@ namespace NHSE.WinForms;
public sealed class SysBotController(InjectionType type)
{
public readonly SysBot Bot = new();
private readonly Settings Settings = Settings.Default;
private readonly Settings _settings = Settings.Default;
public string IP => Settings.SysBotIP;
public string Port => Settings.SysBotPort.ToString();
public string IP => _settings.SysBotIP;
public string Port => _settings.SysBotPort.ToString();
public bool Connect(string ip, string port)
{
@@ -28,35 +28,29 @@ public bool Connect(string ip, string port)
return false;
}
var settings = Settings;
settings.SysBotIP = ip;
settings.SysBotPort = p;
settings.Save();
_settings.SysBotIP = ip;
_settings.SysBotPort = p;
_settings.Save();
return true;
}
public uint GetDefaultOffset()
public uint GetDefaultOffset() => type switch
{
var settings = Settings;
return type switch
{
InjectionType.Generic => settings.SysBotGenericOffset,
InjectionType.Pouch => settings.SysBotPouchOffset,
_ => throw new ArgumentOutOfRangeException()
};
}
InjectionType.Generic => _settings.SysBotGenericOffset,
InjectionType.Pouch => _settings.SysBotPouchOffset,
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
public void SetOffset(uint value)
{
var settings = Settings;
switch (type)
{
case InjectionType.Generic: settings.SysBotGenericOffset = value; break;
case InjectionType.Pouch: settings.SysBotPouchOffset = value; break;
case InjectionType.Generic: _settings.SysBotGenericOffset = value; break;
case InjectionType.Pouch: _settings.SysBotPouchOffset = value; break;
default: return;
}
settings.Save();
_settings.Save();
}
public void HexEdit(uint offset, int length)
@@ -84,12 +78,12 @@ public void HexEdit(uint offset, int length)
public void PopPrompt()
{
if (Settings.SysBotPrompted)
if (_settings.SysBotPrompted)
return;
WinFormsUtil.Alert(MessageStrings.MsgSysBotInfo, MessageStrings.MsgSysBotRequired);
Settings.SysBotPrompted = true;
Settings.Save();
_settings.SysBotPrompted = true;
_settings.Save();
}
public void WriteBytes(byte[] data, uint offset)