From 4823a451045daa66d237c8463f5a1206308d15d1 Mon Sep 17 00:00:00 2001 From: Kurt Date: Sat, 24 Jan 2026 22:55:29 -0600 Subject: [PATCH] Push current Still some issues rendering the inflated viewport --- .../Editing/FieldItem/FieldItemColumn.cs | 6 +- .../Editing/FieldItem/FieldItemDropper.cs | 2 +- .../Map/Layers/LayerPositionConfig.cs | 6 + .../Structures/Map/Managers/MapEditor.cs | 9 +- .../Structures/Map/Managers/MapMutator.cs | 43 +- .../Structures/Map/Managers/MapViewState.cs | 6 + .../Structures/Map/Terrain/TerrainTile.cs | 6 +- NHSE.Sprites/Field/ItemLayerSprite.cs | 203 +++++----- NHSE.Sprites/Field/MapRenderer.cs | 160 +++++--- NHSE.Sprites/Field/TerrainSprite.cs | 12 +- .../Subforms/Map/FieldItemEditor.Designer.cs | 1 + NHSE.WinForms/Subforms/Map/FieldItemEditor.cs | 381 ++++++++++++------ .../Subforms/Map/PlayerHouseEditor.cs | 5 +- 13 files changed, 529 insertions(+), 311 deletions(-) diff --git a/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs b/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs index dae2e0a..45a1839 100644 --- a/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs +++ b/NHSE.Core/Editing/FieldItem/FieldItemColumn.cs @@ -6,8 +6,8 @@ /// /// Extension tiles underneath the actual item root are included; extension tiles to the right are not. /// -/// X Coordinate within the Field Item Layer -/// Y Coordinate within the Field Item Layer +/// X Coordinate within the Field Item Layer +/// Y Coordinate within the Field Item Layer /// Offset relative to the start of the Field Item Layer /// Data for this column -public sealed record FieldItemColumn(int X, int Y, int Offset, byte[] Data); \ No newline at end of file +public sealed record FieldItemColumn(int RelativeX, int RelativeY, int Offset, byte[] Data); \ No newline at end of file diff --git a/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs b/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs index abd8073..ca85ff7 100644 --- a/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs +++ b/NHSE.Core/Editing/FieldItem/FieldItemDropper.cs @@ -110,7 +110,7 @@ private static byte[] GetColumnExtension(ReadOnlySpan items) return Item.SetArray(col); } - private int GetTileOffset(int x, int y) => Item.SIZE * (y + (x * MapHeight)); + private int GetTileOffset(int relX, int relY) => Item.SIZE * (relY + (relX * MapHeight)); private static Item GetDroppedItem(Item item) { diff --git a/NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs b/NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs index 5a007d5..7131768 100644 --- a/NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs +++ b/NHSE.Core/Structures/Map/Layers/LayerPositionConfig.cs @@ -100,4 +100,10 @@ public int GetIndexTileRelative(int relX, int relY) // In other words, Item[1] is X=0,Y=1 return (relX * (CountHeight << TileBitShift)) + relY; } + + public bool IsCoordinateValidAbsolute(int absX, int absY) + { + var (relX, relY) = GetCoordinatesRelative(absX, absY); + return IsCoordinateValidRelative(relX, relY); + } } \ No newline at end of file diff --git a/NHSE.Core/Structures/Map/Managers/MapEditor.cs b/NHSE.Core/Structures/Map/Managers/MapEditor.cs index 7c7ce15..b832437 100644 --- a/NHSE.Core/Structures/Map/Managers/MapEditor.cs +++ b/NHSE.Core/Structures/Map/Managers/MapEditor.cs @@ -22,12 +22,11 @@ public sealed class MapEditor /// /// X coordinate (mouse on upscaled image). /// Y coordinate (mouse on upscaled image). - /// Absolute tile X coordinate. - /// Absolute tile Y coordinate. - public void GetCursorCoordinates(in int mX, in int mY, out int x, out int y) + public (int X, int Y) GetCursorCoordinates(in int mX, in int mY) { - x = mX / MapScale; - y = mY / MapScale; + var x = mX / MapScale; + var y = mY / MapScale; + return (x, y); } /// diff --git a/NHSE.Core/Structures/Map/Managers/MapMutator.cs b/NHSE.Core/Structures/Map/Managers/MapMutator.cs index b6ddedd..47b381d 100644 --- a/NHSE.Core/Structures/Map/Managers/MapMutator.cs +++ b/NHSE.Core/Structures/Map/Managers/MapMutator.cs @@ -29,9 +29,24 @@ public int ReplaceFieldItems(Item oldItem, Item newItem, in bool wholeMap) /// The number of items modified. public int ModifyFieldItems(Func action, in bool wholeMap, LayerFieldItem layerField) { - var (xMin, yMin) = wholeMap ? (0, 0) : (View.X, View.Y); - var info = layerField.TileInfo; - var (width, height) = wholeMap ? info.DimTotal : info.DimAcre; + int xMin, yMin, width, height; + if (wholeMap) + { + (xMin, yMin) = (0, 0); + var info = layerField.TileInfo; + (width, height) = info.DimTotal; + } + else + { + (xMin, yMin) = (View.X, View.Y); + // Convert absolute to relative coordinates + if (!Manager.ConfigItems.IsCoordinateValidAbsolute(xMin, yMin)) + return 0; + (xMin, yMin) = Manager.ConfigItems.GetCoordinatesRelative(xMin, yMin); + + var info = layerField.TileInfo; + (width, height) = info.DimAcre; + } return action(xMin, yMin, width, height); } @@ -45,9 +60,25 @@ public int ModifyFieldItems(Func action, in bool wholeM /// The number of items replaced. private int ReplaceFieldItems(Item oldItem, Item newItem, bool wholeMap, LayerFieldItem layerField) { - var (xMin, yMin) = wholeMap ? (0, 0) : (View.X, View.Y); - var info = layerField.TileInfo; - var (width, height) = wholeMap ? info.DimTotal : info.DimAcre; + int xMin, yMin, width, height; + if (wholeMap) + { + (xMin, yMin) = (0, 0); + var info = layerField.TileInfo; + (width, height) = info.DimTotal; + } + else + { + (xMin, yMin) = (View.X, View.Y); + // Convert absolute to relative coordinates + if (!Manager.ConfigItems.IsCoordinateValidAbsolute(xMin, yMin)) + return 0; + (xMin, yMin) = Manager.ConfigItems.GetCoordinatesRelative(xMin, yMin); + + var info = layerField.TileInfo; + (width, height) = info.DimAcre; + } + return layerField.ReplaceAll(oldItem, newItem, xMin, yMin, width, height); } diff --git a/NHSE.Core/Structures/Map/Managers/MapViewState.cs b/NHSE.Core/Structures/Map/Managers/MapViewState.cs index 0b561f3..cd01243 100644 --- a/NHSE.Core/Structures/Map/Managers/MapViewState.cs +++ b/NHSE.Core/Structures/Map/Managers/MapViewState.cs @@ -116,6 +116,12 @@ public bool SetViewTo(in int absX, in int absY) return x != X || y != Y; } + /// + /// Drags the view by the specified delta amounts. + /// + /// if the view changed; otherwise, . + public bool DragView(int dX, int dY) => SetViewTo(X + dX, Y + dY); + /// /// Sets the view to the top-left of the specified acre. /// diff --git a/NHSE.Core/Structures/Map/Terrain/TerrainTile.cs b/NHSE.Core/Structures/Map/Terrain/TerrainTile.cs index a9631e6..fe2d7e2 100644 --- a/NHSE.Core/Structures/Map/Terrain/TerrainTile.cs +++ b/NHSE.Core/Structures/Map/Terrain/TerrainTile.cs @@ -76,9 +76,9 @@ public void CopyRoadFrom(TerrainTile tile) LandMakingAngleRoad = tile.LandMakingAngleRoad; } - public bool Rotate() => UnitModelRoad != 0 ? RotateRoad() : RotateTerrain(); + public bool TryRotate() => UnitModelRoad != 0 ? TryRotateRoad() : TryRotateTerrain(); - private bool RotateTerrain() + private bool TryRotateTerrain() { if (UnitModel == TerrainUnitModel.Base) return false; @@ -88,7 +88,7 @@ private bool RotateTerrain() return true; } - private bool RotateRoad() + private bool TryRotateRoad() { var rot = LandMakingAngleRoad; rot = (ushort) ((rot + 1) & 3); diff --git a/NHSE.Sprites/Field/ItemLayerSprite.cs b/NHSE.Sprites/Field/ItemLayerSprite.cs index 465907e..6f68884 100644 --- a/NHSE.Sprites/Field/ItemLayerSprite.cs +++ b/NHSE.Sprites/Field/ItemLayerSprite.cs @@ -11,43 +11,38 @@ public static class ItemLayerSprite { private static readonly Pen Reticle = new(Color.Red); - /// - /// Generates a bitmap representation of the provided item layer at a 1px-per-tile scale. - /// - /// Item layer to render. - public static Bitmap GetBitmapItemLayer(LayerItem layer) - { - var items = layer.Tiles; - var imgHeight = layer.TileInfo.TotalHeight; - var imgWidth = items.Length / imgHeight; - - var bmpData = new int[imgWidth * imgHeight]; - LoadBitmapLayer(items, bmpData, imgWidth, imgHeight); - - return ImageUtil.GetBitmap(bmpData, imgWidth, imgHeight); - } - /// /// 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. /// /// - /// Each item's color is determined using FieldItemColor.GetItemColor and stored in ARGB format. + /// Each item's color is determined using and stored in ARGB format. /// Colors are written to bmpData in column-major order, where each column is filled from top to bottom. /// /// List of items from which color values are extracted. The span must contain at least width × height elements. /// Pixel data for the bitmap. The span must have a length of at least width × height. + /// Configuration for layer positioning. /// The number of columns in the bitmap. Must be greater than zero. /// The number of rows in the bitmap. Must be greater than zero. - private static void LoadBitmapLayer(ReadOnlySpan items, Span bmpData, int imgWidth, int imgHeight) + private static void LoadBitmapLayer(ReadOnlySpan items, Span bmpData, in LayerPositionConfig cfg, int imgWidth, int imgHeight) { + var (shiftX, shiftY) = cfg.GetCoordinatesAbsolute(0, 0); + + // Iterate through the relative positions within the layer. + // Then, map to absolute positions in the bitmap with the configured shift. for (int x = 0; x < imgWidth; x++) { var ix = x * imgHeight; for (int y = 0; y < imgHeight; y++) { + // Get the tile at this position. var index = ix + y; var tile = items[index]; - bmpData[(y * imgWidth) + x] = FieldItemColor.GetItemColor(tile).ToArgb(); + + // Get the actual shifted position in the bitmap. + var offset = ((y + shiftY) * imgWidth) + (x + shiftX); + + // Write the color to the bitmap data. + bmpData[offset] = FieldItemColor.GetItemColor(tile).ToArgb(); } } } @@ -55,62 +50,69 @@ private static void LoadBitmapLayer(ReadOnlySpan items, Span bmpData, /// /// Loads an item layer into a bitmap, scaling it up to the desired size, drawing special symbols, and overlaying a grid. /// - /// Inflated acre bitmap to write to. + /// Inflated acre bitmap to write to. /// Item layer to draw from. - /// Top-left X coordinate to start drawing from, relative to the origin of the layer (not map coordinates). - /// Top-left Y coordinate to start drawing from. + /// Configuration for layer positioning. + /// Top-left X coordinate to start drawing from, relative to the origin of the map. + /// Top-left Y coordinate to start drawing from. /// Pixel data for 1px per tile image. /// >Pixel data for final inflated image. /// Scaling factor from 1px => final image dimensions. /// Optional transparency override color. /// Color to use for gridlines. - public static void LoadItemLayerViewGrid(Bitmap img, LayerItem layer, int relX, int relY, + public static void LoadViewport(Bitmap imgScaled, LayerItem layer, in LayerPositionConfig cfg, int absX, int absY, Span imgSingle, Span imgUpscaled, int imgScale, int transparency = -1, int gridlineColor = 0) { // Update the 1px view-grid image pixel data. - LoadViewport(imgSingle, layer, relX, relY); + LoadViewport(layer, cfg, imgSingle, absX, absY); // Get the final inflated size of the image. - int imgWidth = layer.TileInfo.ViewWidth; - int h = layer.TileInfo.ViewHeight; - imgWidth *= imgScale; - h *= imgScale; + var imgWidth = imgScaled.Width; + var imgHeight = imgScaled.Height; // Inflate to the final size storage. - ImageUtil.ScalePixelImage(imgSingle, imgUpscaled, imgWidth, h, imgScale); + ImageUtil.ScalePixelImage(imgSingle, imgUpscaled, imgWidth, imgHeight, 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); + DrawDirectionals(layer, cfg, imgUpscaled, absX, absY, imgWidth, imgScale); // Apply gridlines to visually separate each cell. - DrawGrid(imgUpscaled, imgWidth, h, gridlineColor, imgScale); + DrawGrid(imgUpscaled, imgWidth, imgHeight, gridlineColor, imgScale); // Update the bitmap, final data. - img.SetBitmapData(imgUpscaled); + imgScaled.SetBitmapData(imgUpscaled); } /// /// Loads pixel data from the specified layer into the provided span, using the given starting coordinates and the layer's view dimensions. /// - /// Pixel data of the final image. /// The layer from which to load pixel data. - /// The x-coordinate of the upper-left corner in the layer from which to start loading pixels. - /// The y-coordinate of the upper-left corner in the layer from which to start loading pixels. - private static void LoadViewport(Span data, LayerItem layer, int relX, int relY) + /// Configuration for layer positioning. + /// Pixel data of the final image. + /// The x-coordinate of the upper-left corner in the map from which to start loading pixels. + /// The y-coordinate of the upper-left corner in the map from which to start loading pixels. + private static void LoadViewport(LayerItem layer, in LayerPositionConfig cfg, Span data, int absX, int absY) { var width = layer.TileInfo.ViewWidth; var height = layer.TileInfo.ViewHeight; + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + for (int y = 0; y < height; y++) { var baseIndex = (y * width); for (int x = 0; x < width; x++) { - var tile = layer.GetTile(relX + x, relY + y); + var tileX = relX + x; + var tileY = relY + y; + if (!cfg.IsCoordinateValidRelative(tileX, tileY)) + continue; + var tile = layer.GetTile(tileX, tileY); var color = FieldItemColor.GetItemColor(tile).ToArgb(); + var index = baseIndex + x; data[index] = color; } @@ -120,34 +122,41 @@ private static void LoadViewport(Span data, LayerItem layer, int relX, int /// /// Draws extension tile info for the tiles in the specified layer onto the provided pixel data. /// - /// Pixel data of the entire image. /// The layer containing the tiles to process. - /// Top-left X coordinate to start drawing from, relative to the origin of the layer (not map coordinates). - /// Top-left Y coordinate to start drawing from. + /// Configuration for layer positioning. + /// Pixel data of the entire image. + /// Top-left X coordinate to start drawing from, relative to the origin of the map. + /// Top-left Y coordinate to start drawing from. /// Width of the entire image. /// Scaling factor from 1px => final image dimensions. - private static void DrawDirectionals(Span data, LayerItem layer, int relX, int relY, int imgWidth, int imgScale) + private static void DrawDirectionals(LayerItem layer, in LayerPositionConfig cfg, + Span data, + int absX, int absY, int imgWidth, int imgScale) { var width = layer.TileInfo.ViewWidth; var height = layer.TileInfo.ViewHeight; - for (int x = 0; x < width; x++) + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + + for (int viewX = 0; viewX < width; viewX++) { - for (int y = 0; y < height; y++) + for (int viewY = 0; viewY < height; viewY++) { - var pX = relX + x; - var pY = relY + y; + var pX = relX + viewX; + var pY = relY + viewY; + if (!cfg.IsCoordinateValidRelative(pX, pY)) + continue; var tile = layer.GetTile(pX, pY); if (tile.IsNone) continue; // Apply cosmetic details based on the tile's details. if (tile.IsBuried) - DrawX(data, relX * imgScale, relY * imgScale, imgScale, imgWidth); + DrawX(data, viewX * imgScale, viewY * imgScale, imgScale, imgWidth); else if (tile.IsDropped) - DrawPlus(data, relX * imgScale, relY * imgScale, imgScale, imgWidth); + DrawPlus(data, viewX * imgScale, viewY * imgScale, imgScale, imgWidth); else if (tile.IsExtension) - DrawDirectional(data, tile, relX * imgScale, relY * imgScale, imgScale, imgWidth); + DrawDirectional(data, tile, viewX * imgScale, viewY * imgScale, imgScale, imgWidth); // Based on the display item, apply details. var id = tile.DisplayItemId; @@ -159,7 +168,7 @@ private static void DrawDirectionals(Span data, LayerItem layer, int relX, var geneValue = (genes >> (geneIndex * 2)) & 3; if (geneValue == 0) continue; - DrawGene(data, relX * imgScale, relY * imgScale, imgScale, imgWidth, geneValue, geneIndex); + DrawGene(data, absX * imgScale, absY * imgScale, imgScale, imgWidth, geneValue, geneIndex); } } } @@ -172,8 +181,13 @@ private static int GetGeneIndex(ref Item tile, LayerItem layer, int relX, int re { if (tile.IsRoot) return 0; - var geneIndex = (tile.ExtensionY << 1) | tile.ExtensionX; - tile = layer.GetTile(relX - tile.ExtensionX, relY - tile.ExtensionY); + + // Sanity check: can only extend by 1 in either direction, and must extend in same direction as relative position. + // Ignore bad extension values; we know the gene index from position alone. + var eX = relX & 1; + var eY = relY & 1; + var geneIndex = (eY << 1) | eX; + tile = layer.GetTile(relX - eX, relY - eY); return geneIndex; } @@ -181,35 +195,35 @@ private static int GetGeneIndex(ref Item tile, LayerItem layer, int relX, int re /// Draws a flower gene on the item cell. /// /// Pixel data of the entire image. - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scale of the entire cell. /// Width of the entire image. /// Value of the gene (0-3). /// 0-3 value indicating which gene (quadrant) to draw. - private static void DrawGene(Span data, int x0, int y0, int imgScale, int imgWidth, uint geneValue, int geneIndex) + private static void DrawGene(Span data, int viewX, int viewY, int imgScale, int imgWidth, uint geneValue, int geneIndex) { - var c = ShiftToGeneCoordinate(ref x0, ref y0, imgScale, geneIndex); - FillSquare(data, x0, y0, imgScale / 2, imgWidth, c, geneValue == 3 ? 1 : 2); + var c = ShiftToGeneCoordinate(ref viewX, ref viewY, imgScale, geneIndex); + FillSquare(data, viewX, viewY, imgScale / 2, imgWidth, c, geneValue == 3 ? 1 : 2); } /// - /// Fills a square region within a one-dimensional span with the specified color value, using a given increment to control the fill step. + /// Fills a square region within a one-dimensional span with the specified color value, using a given increment to control the fill step. /// /// 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. /// /// The span representing the target buffer to fill. Each element corresponds to a pixel or cell in a row-major order grid. - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scale of the entire cell. /// Width of the entire image. /// The color value to assign to each filled element in the square region. /// The step size to use when filling elements. Must be positive. A larger increment skips more elements within the square. - private static void FillSquare(Span data, int x0, int y0, int imgScale, int imgWidth, int color, int increment) + private static void FillSquare(Span data, int viewX, int viewY, int imgScale, int imgWidth, int color, int increment) { - var baseIndex = (y0 * imgWidth) + x0; + var baseIndex = (viewY * imgWidth) + viewX; for (int i = 0; i < imgScale * imgScale; i += increment) { var x = i % imgScale; @@ -226,24 +240,24 @@ private static void FillSquare(Span data, int x0, int y0, int imgScale, int /// 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. /// - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scale of the entire cell. /// 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. /// An integer representing the ARGB color value associated with the specified gene region. - private static int ShiftToGeneCoordinate(ref int x0, ref int y0, int imgScale, int geneIndex) + private static int ShiftToGeneCoordinate(ref int absX, ref int absY, int imgScale, int geneIndex) { switch (geneIndex) { case 0: // bottom right - x0 += imgScale / 2; - y0 += imgScale / 2; + absX += imgScale / 2; + absY += imgScale / 2; return Color.Red.ToArgb(); case 1: // bottom left - y0 += imgScale / 2; + absY += imgScale / 2; return Color.Yellow.ToArgb(); case 2: // top right - x0 += imgScale / 2; + absX += imgScale / 2; return Color.AntiqueWhite.ToArgb(); default: // top left return Color.Black.ToArgb(); @@ -254,13 +268,13 @@ private static int ShiftToGeneCoordinate(ref int x0, ref int y0, int imgScale, i /// Updates the pixel data to draw a `+`, indicating the item as "dropped". /// /// Pixel data of the entire image. - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scaling factor from 1px => final image dimensions. /// Width of the entire image. - private static void DrawPlus(Span data, int x0, int y0, int imgScale, int imgWidth) + private static void DrawPlus(Span data, int viewX, int viewY, int imgScale, int imgWidth) { - var x0y0 = (imgWidth * y0) + x0; + var x0y0 = (imgWidth * viewY) + viewX; var s2 = imgScale / 2; var ws2 = imgWidth * s2; @@ -280,17 +294,17 @@ private static void DrawPlus(Span data, int x0, int y0, int imgScale, int i /// Updates the pixel data to draw an `X`, indicating the item as "buried". /// /// Pixel data of the entire image. - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scaling factor from 1px => final image dimensions. /// Width of the entire image. - private static void DrawX(Span data, int x0, int y0, int imgScale, int imgWidth) + private static void DrawX(Span data, int viewX, int viewY, int imgScale, int imgWidth) { var opposite = imgScale - 1; var wo = imgWidth * opposite; // Starting offsets for each of the slashes - var bBackward = (imgWidth * y0) + x0; // Backwards \ + var bBackward = (imgWidth * viewY) + viewX; // Backwards \ var bForward = bBackward + wo; // Forwards / for (int x = 0; x < imgScale; x++) @@ -308,11 +322,11 @@ private static void DrawX(Span data, int x0, int y0, int imgScale, int imgW /// /// Pixel data of the entire image. /// Extension tile data. - /// Top-left X coordinate to start drawing from. - /// Top-left Y coordinate to start drawing from. + /// Top-left X coordinate to start drawing from. + /// Top-left Y coordinate to start drawing from. /// Scaling factor from 1px => final image dimensions. /// Width of the entire image. - private static void DrawDirectional(Span data, Item tile, int x0, int y0, int imgScale, int imgWidth) + private static void DrawDirectional(Span data, Item tile, int viewX, int viewY, int imgScale, int imgWidth) { var eX = tile.ExtensionX; var eY = tile.ExtensionY; @@ -323,10 +337,12 @@ private static void DrawDirectional(Span data, Item tile, int x0, int y0, i var startX = eX >= eY ? 0 : start; var startY = eX <= eY ? 0 : start; - var baseIndex = (imgWidth * y0) + x0; + var baseIndex = (imgWidth * viewY) + viewX; for (int x = startX, y = startY; x < imgScale && y < imgScale; x += eX, y += eY) { var index = baseIndex + (imgWidth * y) + x; + if (index >= data.Length) // Since we can't guarantee valid extension values, just skip bad ones. + continue; data[index] ^= 0x00_808080; } } @@ -364,26 +380,25 @@ public static void DrawGrid(Span data, int imgWidth, int imgHeight, int gri } } - public static Bitmap GetBitmapItemLayer(Bitmap dest, LayerItem layer, int topX, int topY, Span data, int transparency = -1) + /// + /// Loads an item layer into a viewport bitmap, drawing a view reticle over it. + /// + /// Configuration for layer positioning. + /// Item layer to draw from. + /// Pixel data of the final image. + /// Optional transparency override color. + public static void LoadItemLayerDrawReticle(LayerPositionConfig cfg, LayerItem layer, Span data, int transparency = -1) { - LoadBitmapLayer(layer.Tiles, data, layer.TileInfo.TotalWidth, layer.TileInfo.TotalHeight); + LoadBitmapLayer(layer.Tiles, data, cfg, layer.TileInfo.TotalWidth, layer.TileInfo.TotalHeight); if (transparency >>> 24 != 0xFF) ImageUtil.ClampAllTransparencyTo(data, transparency); - dest.SetBitmapData(data); - return DrawViewReticle(dest, layer.TileInfo, topX, topY); } - private static Bitmap DrawViewReticle(Bitmap map, TileGridViewport g, int topX, int topY, int scale = 1) + public static void DrawViewReticle(Bitmap map, TileGridViewport g, int absX, int absY, int scale = 1) { using var gfx = Graphics.FromImage(map); - 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); + var reticleWidth = g.ViewWidth * scale; + var reticleHeight = g.ViewHeight * scale; + gfx.DrawRectangle(Reticle, absX * scale, absY * scale, reticleWidth, reticleHeight); } } \ No newline at end of file diff --git a/NHSE.Sprites/Field/MapRenderer.cs b/NHSE.Sprites/Field/MapRenderer.cs index 5875b8a..f814949 100644 --- a/NHSE.Sprites/Field/MapRenderer.cs +++ b/NHSE.Sprites/Field/MapRenderer.cs @@ -1,6 +1,6 @@ -using System; +using NHSE.Core; +using System; using System.Drawing; -using NHSE.Core; namespace NHSE.Sprites; @@ -9,98 +9,132 @@ namespace NHSE.Sprites; /// public sealed class MapRenderer : IDisposable { + /// + /// Data source for map rendering. + /// private readonly MapEditor Map; + + /// Scale factor for full map rendering. private int MapScale => Map.MapScale; + + /// Scale factor for acre viewport rendering. private int ViewScale => Map.ViewScale; - 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; - private readonly Bitmap ScaleAcre; - private readonly int[] PixelsItemMap; - private readonly Bitmap MapReticle; - private readonly int[] PixelsBackgroundAcre1; - private readonly int[] PixelsBackgroundAcreX; - private readonly Bitmap BackgroundAcre; + /// 1px scale viewport item layer pixel data. + private readonly int[] ViewportItems1; + /// Upscaled viewport item layer pixel data. + private readonly int[] ViewportItemsX; + /// Upscaled viewport item layer image. + private readonly Bitmap ViewportItemsImage; - private readonly int[] PixelsBackgroundMap1; - private readonly int[] PixelsBackgroundMapX; - private readonly Bitmap BackgroundMap; + /// Upscaled map item layer pixel data with reticle. + private readonly int[] MapItemsReticleX; + /// Upscaled map item layer image with reticle. + private readonly Bitmap MapItemsReticleImage; + + /// 1px scale viewport terrain layer pixel data. + private readonly int[] ViewportTerrain1; + /// Upscaled viewport terrain layer pixel data. + private readonly int[] ViewportTerrainX; + /// Upscaled viewport terrain layer image. + private readonly Bitmap ViewportTerrainImage; + + /// 1px scale map terrain layer pixel data. + private readonly int[] MapTerrain1; + /// Upscaled map terrain layer pixel data. + private readonly int[] MapTerrainX; + /// Upscaled map terrain layer image. + private readonly Bitmap MapTerrainImage; public MapRenderer(MapEditor m) { Map = m; + // Initialize cached objects based on map size + // Get tile info from layer 0 (item layer is the tiniest cell we can render) var l1 = m.Mutator.Manager.FieldItems.Layer0; var info = l1.TileInfo; - PixelsItemAcre1 = new int[info.ViewWidth * info.ViewHeight]; - 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); + MapItemsReticleX = new int[info.TotalWidth * info.TotalHeight * MapScale * MapScale]; + MapItemsReticleImage = new Bitmap(info.TotalWidth * MapScale, info.TotalHeight * MapScale); - PixelsBackgroundAcre1 = new int[PixelsItemAcre1.Length]; - PixelsBackgroundAcreX = new int[PixelsItemAcreX.Length]; - BackgroundAcre = new Bitmap(ScaleAcre.Width, ScaleAcre.Height); + MapTerrain1 = new int[MapItemsReticleX.Length / (MapScale * MapScale)]; + MapTerrainX = new int[MapItemsReticleX.Length]; + MapTerrainImage = new Bitmap(MapItemsReticleImage.Width, MapItemsReticleImage.Height); - PixelsBackgroundMap1 = new int[PixelsItemMap.Length / (MapScale * MapScale)]; - PixelsBackgroundMapX = new int[PixelsItemMap.Length]; - BackgroundMap = new Bitmap(MapReticle.Width, MapReticle.Height); + ViewportItems1 = new int[info.ViewWidth * info.ViewHeight]; + ViewportItemsX = new int[ViewportItems1.Length * ViewScale * ViewScale]; + ViewportItemsImage = new Bitmap(info.ViewWidth * ViewScale, info.ViewHeight * ViewScale); + + ViewportTerrain1 = new int[ViewportItems1.Length]; + ViewportTerrainX = new int[ViewportItemsX.Length]; + ViewportTerrainImage = new Bitmap(ViewportItemsImage.Width, ViewportItemsImage.Height); } public void Dispose() { - ScaleAcre.Dispose(); - MapReticle.Dispose(); - BackgroundAcre.Dispose(); - BackgroundMap.Dispose(); + MapItemsReticleImage.Dispose(); + MapTerrainImage.Dispose(); + + ViewportItemsImage.Dispose(); + ViewportTerrainImage.Dispose(); } - public Bitmap GetLayerAcre(int t) => GetLayerAcre(Map.Mutator.View.X, Map.Mutator.View.Y, t); - public Bitmap GetMapWithReticle(int t) => GetMapWithReticle(Map.Mutator.View.X, Map.Mutator.View.Y, t, Map.Mutator.CurrentLayer); + /// + /// Updates the map items reticle bitmap for the current layer and view position. + /// + /// Transparency of items (not including reticle). + /// Option to draw the reticle. + /// Updated bitmap with reticle. + public Bitmap UpdateMapItemsReticle(int transparency, bool drawReticle = true) + => UpdateMapItemsReticle(Map.Mutator.CurrentLayer, Map.Mutator.View.X, Map.Mutator.View.Y, transparency, drawReticle); - public Bitmap GetBackgroundTerrain(int index = -1) + /// + /// Updates the map terrain bitmap. + /// + /// Index of building to highlight, or -1 for none. + /// Updated map terrain bitmap. + public Bitmap UpdateMapTerrain(int selectedBuildingIndex = -1) + => TerrainSprite.GetMapWithBuildings(MapTerrainImage, Map, null, MapTerrain1, MapTerrainX, selectedBuildingIndex); + + /// + /// Updates the viewport items bitmap for the current layer and view position. + /// + /// Transparency of items. + public Bitmap UpdateViewportItems(int transparency) + => UpdateViewportItems(Map.Mutator.View.X, Map.Mutator.View.Y, transparency); + + /// + /// Updates the viewport terrain bitmap. + /// + /// Font to use for building labels. + /// Transparency for building shapes drawn on terrain. + /// Transparency for terrain layer. + /// Index of building to highlight, or -1 for none. + /// + public Bitmap UpdateViewportTerrain(Font f, byte transparencyBuilding, byte transparencyTerrain, int selectedBuildingIndex = -1) { - return TerrainSprite.GetMapWithBuildings(Map, null, PixelsBackgroundMap1, PixelsBackgroundMapX, BackgroundMap, index); + TerrainSprite.LoadViewport(ViewportTerrainImage, Map, f, ViewportTerrain1, ViewportTerrainX, selectedBuildingIndex, transparencyBuilding, transparencyTerrain); + return ViewportTerrainImage; } - public Bitmap GetInflatedImage(Bitmap regular) + private Bitmap UpdateMapItemsReticle(LayerFieldItem layer, int absX, int absY, int transparency, bool drawReticle = true) { - // 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; + var cfg = Map.Mutator.Manager.ConfigItems; + ItemLayerSprite.LoadItemLayerDrawReticle(cfg, layer, MapItemsReticleX, transparency); + MapItemsReticleImage.SetBitmapData(MapItemsReticleX); + if (drawReticle) + ItemLayerSprite.DrawViewReticle(MapItemsReticleImage, layer.TileInfo, absX, absY); + return MapItemsReticleImage; } - private Bitmap GetLayerAcre(int topX, int topY, int transparency) + private Bitmap UpdateViewportItems(int absX, int absY, int transparency) { + var cfg = Map.Mutator.Manager.ConfigItems; var layer = Map.Mutator.CurrentLayer; - ItemLayerSprite.LoadItemLayerViewGrid(ScaleAcre, layer, topX, topY, PixelsItemAcre1, PixelsItemAcreX, ViewScale, transparency); - return ScaleAcre; - } - - public Bitmap GetBackgroundAcre(Font f, byte transparencyBuilding, byte transparencyTerrain, int index = -1) - { - TerrainSprite.LoadViewport(BackgroundAcre, Map, f, PixelsBackgroundAcre1, PixelsBackgroundAcreX, index, transparencyBuilding, transparencyTerrain); - return BackgroundAcre; - } - - private Bitmap GetMapWithReticle(int topX, int topY, int t, LayerFieldItem layerField) - { - return ItemLayerSprite.GetBitmapItemLayer(MapReticle, layerField, topX, topY, PixelsItemMap, t); + ItemLayerSprite.LoadViewport(ViewportItemsImage, layer, cfg, absX, absY, ViewportItems1, ViewportItemsX, ViewScale, transparency); + return ViewportItemsImage; } } \ No newline at end of file diff --git a/NHSE.Sprites/Field/TerrainSprite.cs b/NHSE.Sprites/Field/TerrainSprite.cs index bc26678..780cc96 100644 --- a/NHSE.Sprites/Field/TerrainSprite.cs +++ b/NHSE.Sprites/Field/TerrainSprite.cs @@ -41,7 +41,9 @@ public static void GenerateMap(Bitmap map, MapMutator mut, Span scale1, Spa DrawReticle(map, mgr.TileInfo, x, y, imgScale); } - public static Bitmap GetMapWithBuildings(MapEditor m, Font? f, Span scale1, Span scaleX, Bitmap map, int buildingIndex = -1) + public static Bitmap GetMapWithBuildings(Bitmap map, MapEditor m, Font? f, + Span scale1, Span scaleX, + int buildingIndex = -1) { GenerateMap(map, m.Mutator, scale1, scaleX, m.MapScale); using var gfx = Graphics.FromImage(map); @@ -52,7 +54,9 @@ public static Bitmap GetMapWithBuildings(MapEditor m, Font? f, Span scale1, return map; } - public static void LoadViewport(Bitmap img, MapEditor m, Font f, Span scale1, Span scaleX, int index, byte transBuild, byte transTerrain) + public static void LoadViewport(Bitmap img, MapEditor m, Font f, + Span scale1, Span scaleX, + int selectedBuildingIndex, byte transparencyBuilding, byte transTerrain) { // Convert from absolute to relative. int mx = m.X / 2; @@ -62,8 +66,8 @@ public static void LoadViewport(Bitmap img, MapEditor m, Font f, Span scale // 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); + gfx.DrawViewPlaza(m, transparencyBuilding); + gfx.DrawViewBuildings(m, selectedBuildingIndex, transparencyBuilding); // Return to pixel writing mode img.GetBitmapData(scaleX); diff --git a/NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs b/NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs index 7e0adce..666266d 100644 --- a/NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs +++ b/NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs @@ -1395,6 +1395,7 @@ private void InitializeComponent() CB_Acre.Name = "CB_Acre"; CB_Acre.Size = new System.Drawing.Size(56, 25); CB_Acre.TabIndex = 100; + CB_Acre.SelectedIndexChanged += ChangeAcre; // // FieldItemEditor // diff --git a/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs b/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs index 456fd81..6b1c94c 100644 --- a/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs +++ b/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Drawing; using System.Drawing.Imaging; using System.IO; @@ -20,16 +21,19 @@ public sealed partial class FieldItemEditor : Form, IItemLayerEditor private MapViewState View => Editor.Mutator.View; private MapTileManager Map => Editor.Mutator.Manager; - private readonly bool IsExtendedMap30; - private bool Loading; private int SelectedBuildingIndex; + /// Cached current hover X coordinate within the acre. private int HoverX; + /// Cached current hover Y coordinate within the acre. private int HoverY; + private int DragX = -1; private int DragY = -1; - private bool Dragging; + private bool IsDragOperationActive; + + private bool IsMenuHasActivate = true; public ItemEditor ItemProvider => ItemEdit; @@ -50,7 +54,6 @@ public FieldItemEditor(MainSave sav) // Read the expected scale from the control. var scale = (PB_Acre.Width - 2) / LayerFieldItem.TilesPerAcreDim; // 1px border SAV = sav; - IsExtendedMap30 = sav.FieldItemAcreWidth != 7; Editor = MapEditor.FromSaveFile(sav); Editor.MapScale = scale; Editor.ViewScale = scale; @@ -62,9 +65,12 @@ public FieldItemEditor(MainSave sav) LoadBuildings(sav); ReloadMapBackground(); LoadEditors(); + + // Set initial states LB_Items.SelectedIndex = 0; CB_Acre.SelectedIndex = 0; CB_MapAcre.SelectedIndex = 0; + Loading = false; LoadItemGridAcre(); } @@ -130,16 +136,14 @@ private void LoadItemGridAcre() private void ReloadMapBackground() { - var img = Renderer.GetBackgroundTerrain(SelectedBuildingIndex); + var img = Renderer.UpdateMapTerrain(SelectedBuildingIndex); SetMapBackgroundImage(img); } - private void ReloadMapItemGrid() => SetMapForegroundImage(Renderer.GetMapWithReticle(GetItemTransparency())); + private void ReloadMapItemGrid() => SetMapForegroundImage(Renderer.UpdateMapItemsReticle(GetItemTransparency())); private void SetMapBackgroundImage(Bitmap img) { - if (IsExtendedMap30) - img = Renderer.GetInflatedImage(img); PB_Map.BackgroundImage = img; PB_Map.Invalidate(); // background image reassigning to same img doesn't redraw; force it } @@ -153,12 +157,12 @@ private void ReloadAcreBackground() { var tbuild = (byte)TR_BuildingTransparency.Value; var tterrain = (byte)TR_Terrain.Value; - var img = Renderer.GetBackgroundAcre(L_Coordinates.Font, tbuild, tterrain, SelectedBuildingIndex); + var img = Renderer.UpdateViewportTerrain(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 = Renderer.GetLayerAcre(GetItemTransparency()); + private void ReloadAcreItemGrid() => PB_Acre.Image = Renderer.UpdateViewportItems(GetItemTransparency()); public void ReloadItems() { @@ -182,7 +186,7 @@ private void UpdateArrowVisibility() private void PB_Acre_MouseClick(object sender, MouseEventArgs e) { - if (Dragging) + if (IsDragOperationActive) { ResetDrag(); return; @@ -198,21 +202,25 @@ private void ResetDrag() { DragX = -1; DragY = -1; - Dragging = false; + IsDragOperationActive = false; } private void OmniTile(MouseEventArgs e) { - var tile = GetTile(CurrentLayer, e, out var x, out var y); - OmniTile(tile, x, y); + if (!GetTile(e, CurrentLayer, out var tile)) + return; + OmniTile(tile.Tile, tile.RelativeX, tile.RelativeY); } private void OmniTileTerrain(MouseEventArgs e) { - SetHoveredItem(e); - var x = View.X + HoverX; - var y = View.Y + HoverY; - TerrainTile tile = Editor.Terrain.GetTile(x / 2, y / 2); + if (!GetTile(e, Editor.Mutator.Manager.LayerTerrain, out var meta)) + return; + + var tile = meta.Tile; + var relX = meta.RelativeX; + var relY = meta.RelativeY; + if (tbeForm?.IsBrushSelected != true) { OmniTileTerrain(tile); @@ -233,32 +241,32 @@ private void OmniTileTerrain(MouseEventArgs e) for (int j = -radius; j < radius; j++) { if ((i * i) + (j * j) < threshold) - selectedTiles.Add(Editor.Terrain.GetTile((x / 2) + i, (y / 2) + j)); + selectedTiles.Add(Editor.Terrain.GetTile(relX + i, relY + j)); } } SetTiles(selectedTiles); } - private void OmniTile(Item tile, int x, int y) + private void OmniTile(Item tile, int relX, int relY) { switch (ModifierKeys) { default: - ViewTile(tile, x, y); + ViewTile(tile, relX, relY); return; case Keys.Alt | Keys.Control: case Keys.Alt | Keys.Control | Keys.Shift: - ReplaceTile(tile, x, y); + ReplaceTile(tile, relX, relY); return; case Keys.Shift: - SetTile(tile, x, y); + SetTile(tile, relX, relY); return; case Keys.Alt: - DeleteTile(tile, x, y); + DeleteTile(tile, relX, relY); return; } } @@ -285,27 +293,83 @@ private void OmniTileTerrain(TerrainTile tile) } } - private Item GetTile(LayerFieldItem layerField, MouseEventArgs e, out int x, out int y) + private sealed record TileCheck(T Tile, int AbsoluteX, int AbsoluteY, int RelativeX, int RelativeY); + + private bool GetTile(MouseEventArgs e, LayerFieldItem layerField, [NotNullWhen(true)] out TileCheck? item) { - SetHoveredItem(e); - return layerField.GetTile(x = View.X + HoverX, y = View.Y + HoverY); + UpdateHoveredCoordinates(e); + var (absX, absY) = GetAbsoluteCoordinatesHover(); + return GetTile(layerField, absX, absY, out item); } - private void SetHoveredItem(MouseEventArgs e) + private (int X, int Y) GetAbsoluteCoordinatesHover() { - GetAcreCoordinates(e, out HoverX, out HoverY); + var absX = View.X + HoverX; + var absY = View.Y + HoverY; + return (absX, absY); + } + + private (int X, int Y) GetAbsoluteCoordinatesHoverTerrain() + { + // Terrain tiles are 16x16, but the view caters to 32x32 for field items. + var absX = (View.X + HoverX) / 2; + var absY = (View.Y + HoverY) / 2; + return (absX, absY); + } + + private (int X, int Y) GetViewCoordinates(MouseEventArgs e) + { + var x = e.X / Editor.ViewScale; + var y = e.Y / Editor.ViewScale; + return (x, y); + } + + private bool GetTile(LayerFieldItem layerField, int absX, int absY, [NotNullWhen(true)] out TileCheck? item) + { + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + item = null; + return false; + } + + var rel = cfg.GetCoordinatesRelative(absX, absY); + var tile = layerField.GetTile(rel.X, rel.Y); + item = new TileCheck(tile, absX, absY, rel.X, rel.Y); + return true; + } + + private bool GetTile(MouseEventArgs e, LayerTerrain layerField, [NotNullWhen(true)] out TileCheck? item) + { + UpdateHoveredCoordinates(e); + var (absX, absY) = GetAbsoluteCoordinatesHoverTerrain(); + return GetTile(layerField, absX, absY, out item); + } + + private bool GetTile(LayerTerrain layerField, int absX, int absY, [NotNullWhen(true)] out TileCheck? item) + { + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + item = null; + return false; + } + + var rel = cfg.GetCoordinatesRelative(absX, absY); + var tile = layerField.GetTile(rel.X, rel.Y); + item = new TileCheck(tile, absX, absY, rel.X, rel.Y); + return true; + } + + private void UpdateHoveredCoordinates(MouseEventArgs e) + { + (HoverX, HoverY) = GetViewCoordinates(e); // Mouse event may fire with a slightly too large x/y; clamp just in case. HoverX &= 0x1F; HoverY &= 0x1F; } - private void GetAcreCoordinates(MouseEventArgs e, out int x, out int y) - { - x = e.X / Editor.ViewScale; - y = e.Y / Editor.ViewScale; - } - private void PB_Acre_MouseDown(object sender, MouseEventArgs e) => ResetDrag(); private void PB_Acre_MouseMove(object sender, MouseEventArgs e) @@ -321,10 +385,24 @@ private void PB_Acre_MouseMove(object sender, MouseEventArgs e) OmniTileTerrain(e); } - var oldTile = l.GetTile(View.X + HoverX, View.Y + HoverY); - var tile = GetTile(l, e, out var x, out var y); - if (ReferenceEquals(tile, oldTile)) + // Update hover tooltip if it is a different tile + // Can't compare coordinates if redirection of extension tiles hijacks to return the root tile. + // Just check the (root) tile returns for each. + // Two different extension tiles can redirect to the same root tile, can skip updating in that case. + if (!GetTile(l, View.X + HoverX, View.Y + HoverY, out var old)) return; + + var oldTile = old.Tile; + if (!GetTile(e, l, out var dest)) + return; + + var tile = dest.Tile; + var x = dest.RelativeX; + var y = dest.RelativeY; + if (ReferenceEquals(tile, oldTile)) + return; // same tile, no change + + // Regenerate tooltip text var str = GameInfo.Strings; var name = str.GetItemName(tile); var flagLayer = NUD_Layer.Value == 0 ? Map.LayerItemFlag0 : Map.LayerItemFlag1; @@ -337,19 +415,19 @@ private void PB_Acre_MouseMove(object sender, MouseEventArgs e) private void MoveDrag(MouseEventArgs e) { - GetAcreCoordinates(e, out var nhX, out var nhY); + var (viewX, viewY) = GetViewCoordinates(e); if (DragX == -1) { - DragX = nhX; - DragY = nhY; + DragX = viewX; + DragY = viewY; return; } - var dX = DragX - nhX; - var dY = DragY - nhY; + var dX = DragX - viewX; + var dY = DragY - viewY; - if (ModifierKeys == Keys.Control) + if (ModifierKeys == Keys.Control) // move in larger steps { dX *= 2; dY *= 2; @@ -360,30 +438,33 @@ private void MoveDrag(MouseEventArgs e) if ((dY & 1) == 1) dY ^= 1; + // Ensure movement is significant enough var aX = Math.Abs(dX); var aY = Math.Abs(dY); if (aX < 2 && aY < 2) return; - DragX = nhX; - DragY = nhY; - if (!View.SetViewTo(View.X + dX, View.Y + dY)) + DragX = viewX; + DragY = viewY; + if (!View.DragView(dX, dY)) return; - Dragging = true; + IsDragOperationActive = true; LoadItemGridAcre(); } - private void ViewTile(Item tile, int x, int y) + private void ViewTile(Item tile, int relX, int relY) { if (CHK_RedirectExtensionLoad.Checked && tile.IsExtension) { var l = CurrentLayer; - var rx = Math.Max(0, Math.Min(l.TileInfo.TotalWidth - 1, x - tile.ExtensionX)); - var ry = Math.Max(0, Math.Min(l.TileInfo.TotalHeight - 1, y - tile.ExtensionY)); - var redir = l.GetTile(rx, ry); - if (redir.IsRoot && redir.ItemId == tile.ExtensionItemId) - tile = redir; + relX -= tile.ExtensionX; + relY -= tile.ExtensionY; + l.TileInfo.ClampInside(ref relX, ref relY); + var redirectTile = l.GetTile(relX, relY); + + if (redirectTile.IsRoot && redirectTile.ItemId == tile.ExtensionItemId) + tile = redirectTile; } ViewTile(tile); @@ -403,7 +484,7 @@ private void ViewTile(TerrainTile tile) TC_Editor.SelectedTab = Tab_Terrain; } - private void SetTile(Item tile, int x, int y) + private void SetTile(Item tile, int relX, int relY) { var l = CurrentLayer; var pgt = new Item(); @@ -412,12 +493,12 @@ private void SetTile(Item tile, int x, int y) if (pgt.IsFieldItem && CHK_FieldItemSnap.Checked) { // coordinates must be even (not odd-half) - x &= 0xFFFE; - y &= 0xFFFE; - tile = l.GetTile(x, y); + relX &= 0xFFFE; + relY &= 0xFFFE; + tile = l.GetTile(relX, relY); } - var permission = l.IsOccupied(pgt, x, y); + var permission = l.IsOccupied(pgt, relX, relY); switch (permission) { case PlacedItemPermission.OutOfBounds: @@ -428,17 +509,17 @@ private void SetTile(Item tile, int x, int y) // Clean up original placed data if (tile.IsRoot && CHK_AutoExtension.Checked) - l.DeleteExtensionTiles(tile, x, y); + l.DeleteExtensionTiles(tile, relX, relY); // Set new placed data if (pgt.IsRoot && CHK_AutoExtension.Checked) - l.SetExtensionTiles(pgt, x, y); + l.SetExtensionTiles(pgt, relX, relY); tile.CopyFrom(pgt); ReloadItems(); } - private void ReplaceTile(Item tile, int x, int y) + private void ReplaceTile(Item tile, int relX, int relY) { var l = CurrentLayer; var pgt = new Item(); @@ -447,12 +528,12 @@ private void ReplaceTile(Item tile, int x, int y) if (pgt.IsFieldItem && CHK_FieldItemSnap.Checked) { // coordinates must be even (not odd-half) - x &= 0xFFFE; - y &= 0xFFFE; - tile = l.GetTile(x, y); + relX &= 0xFFFE; + relY &= 0xFFFE; + tile = l.GetTile(relX, relY); } - var permission = l.IsOccupied(pgt, x, y); + var permission = l.IsOccupied(pgt, relX, relY); switch (permission) { case PlacedItemPermission.OutOfBounds: @@ -474,8 +555,8 @@ private void ReplaceTile(Item tile, int x, int y) private void RotateTile(TerrainTile tile) { - bool rotated = tile.Rotate(); - if (!rotated) + bool wasRotated = tile.TryRotate(); + if (!wasRotated) { System.Media.SystemSounds.Asterisk.Play(); return; @@ -486,6 +567,8 @@ private void RotateTile(TerrainTile tile) private void SetTile(TerrainTile tile) { var pgt = (TerrainTile)PG_TerrainTile.SelectedObject!; + + // Apply randomization if enabled if (tbeForm?.RandomizeVariation == true) { switch (pgt.UnitModel) @@ -499,33 +582,30 @@ private void SetTile(TerrainTile tile) } tile.CopyFrom(pgt); - ReloadBuildingsTerrain(); } private void SetTiles(IEnumerable tiles) { var pgt = (TerrainTile)PG_TerrainTile.SelectedObject!; - foreach (TerrainTile tile in tiles) - { - tile.CopyFrom(pgt); - } + foreach (var tile in tiles) + tile.CopyFrom(pgt); ReloadBuildingsTerrain(); } - private void DeleteTile(Item tile, int x, int y) + private void DeleteTile(Item tile, int relX, int relY) { if (CHK_AutoExtension.Checked) { var layer = CurrentLayer; if (!tile.IsRoot) { - x -= tile.ExtensionX; - y -= tile.ExtensionY; - tile = layer.GetTile(x, y); + relX -= tile.ExtensionX; + relY -= tile.ExtensionY; + tile = layer.GetTile(relX, relY); } - layer.DeleteExtensionTiles(tile, x, y); + layer.DeleteExtensionTiles(tile, relX, relY); } tile.Delete(); @@ -561,83 +641,125 @@ private void B_Save_Click(object sender, EventArgs e) private void Menu_View_Click(object sender, EventArgs e) { - var x = View.X + HoverX; - var y = View.Y + HoverY; + var (absX, absY) = GetAbsoluteCoordinatesHover(); + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + System.Media.SystemSounds.Asterisk.Play(); + return; + } + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); if (RB_Item.Checked) { - var tile = CurrentLayer.GetTile(x, y); - ViewTile(tile, x, y); + var tile = CurrentLayer.GetTile(relX, relY); + ViewTile(tile, relX, relY); } else if (RB_Terrain.Checked) { - TerrainTile tile = Editor.Terrain.GetTile(x / 2, y / 2); + var tile = Editor.Terrain.GetTile(relX, relY); ViewTile(tile); } } private void Menu_Set_Click(object sender, EventArgs e) { - var x = View.X + HoverX; - var y = View.Y + HoverY; - if (RB_Item.Checked) { - var tile = CurrentLayer.GetTile(x, y); - SetTile(tile, x, y); + var (absX, absY) = GetAbsoluteCoordinatesHover(); + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + System.Media.SystemSounds.Asterisk.Play(); + return; + } + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + + var tile = CurrentLayer.GetTile(relX, relY); + SetTile(tile, relX, relY); } else if (RB_Terrain.Checked) { - var tile = Editor.Terrain.GetTile(x / 2, y / 2); + var (absX, absY) = GetAbsoluteCoordinatesHoverTerrain(); + var cfg = Editor.Mutator.Manager.ConfigTerrain; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + System.Media.SystemSounds.Asterisk.Play(); + return; + } + + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + var tile = Editor.Terrain.GetTile(relX, relY); SetTile(tile); } } private void Menu_Reset_Click(object sender, EventArgs e) { - var x = View.X + HoverX; - var y = View.Y + HoverY; - if (RB_Item.Checked) { - var tile = CurrentLayer.GetTile(x, y); - DeleteTile(tile, x, y); + var (absX, absY) = GetAbsoluteCoordinatesHover(); + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + System.Media.SystemSounds.Asterisk.Play(); + return; + } + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + + var tile = CurrentLayer.GetTile(relX, relY); + DeleteTile(tile, relX, relY); } else if (RB_Terrain.Checked) { - var tile = Editor.Terrain.GetTile(x / 2, y / 2); + var (absX, absY) = GetAbsoluteCoordinatesHoverTerrain(); + var cfg = Editor.Mutator.Manager.ConfigTerrain; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + { + System.Media.SystemSounds.Asterisk.Play(); + return; + } + + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); + var tile = Editor.Terrain.GetTile(relX, relY); DeleteTile(tile); } } - private bool hasActivate = true; - private void CM_Click_Opening(object sender, System.ComponentModel.CancelEventArgs e) { - if (!RB_Item.Checked) + if (!RB_Item.Checked) // not in Item edit mode, therefore no "Activate Flag" menu { - if (hasActivate) + if (IsMenuHasActivate) CM_Click.Items.Remove(Menu_Activate); - hasActivate = false; + IsMenuHasActivate = false; return; } - var x = View.X + HoverX; - var y = View.Y + HoverY; + var (absX, absY) = GetAbsoluteCoordinatesHover(); + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + return; + + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); var flagLayer = NUD_Layer.Value == 0 ? Map.LayerItemFlag0 : Map.LayerItemFlag1; - var isActive = flagLayer.GetIsActive(x, y); + var isActive = flagLayer.GetIsActive(relX, relY); Menu_Activate.Text = isActive ? "Inactivate" : "Activate"; CM_Click.Items.Add(Menu_Activate); - hasActivate = true; + IsMenuHasActivate = true; } private void Menu_Activate_Click(object sender, EventArgs e) { - var x = View.X + HoverX; - var y = View.Y + HoverY; + var (absX, absY) = GetAbsoluteCoordinatesHover(); + var cfg = Editor.Mutator.Manager.ConfigItems; + if (!cfg.IsCoordinateValidAbsolute(absX, absY)) + return; + + var (relX, relY) = cfg.GetCoordinatesRelative(absX, absY); var flagLayer = NUD_Layer.Value == 0 ? Map.LayerItemFlag0 : Map.LayerItemFlag1; - var isActive = flagLayer.GetIsActive(x, y); - flagLayer.SetIsActive(x, y, !isActive); + var isActive = flagLayer.GetIsActive(relX, relY); + flagLayer.SetIsActive(relX, relY, !isActive); } private void B_Up_Click(object sender, EventArgs e) @@ -776,13 +898,13 @@ private void PB_Map_MouseDown(object sender, MouseEventArgs e) private void ClickMapAt(MouseEventArgs e) { - var (x, y) = Editor.GetMapCoordinates(e.X, e.Y, CHK_SnapToAcre.Checked ? MapViewCoordinateRequest.SnapAcre : MapViewCoordinateRequest.Centered); + var (absX, absY) = 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; + absX &= 0xFFFE; + absY &= 0xFFFE; - if (View.SetViewTo(x, y)) + if (View.SetViewTo(absX, absY)) LoadItemGridAcre(); } @@ -794,12 +916,12 @@ private void PB_Map_MouseMove(object sender, MouseEventArgs e) } else if (e.Button == MouseButtons.None) { - Editor.GetCursorCoordinates(e.X, e.Y, out var x, out var y); - SetCoordinateText(x, y); + var (absX, absY) = Editor.GetCursorCoordinates(e.X, e.Y); + SetCoordinateText(absX, absY); } } - private void SetCoordinateText(int x, int y) => L_Coordinates.Text = $"({x:000},{y:000}) = (0x{x:X2},0x{y:X2})"; + private void SetCoordinateText(int absX, int absY) => L_Coordinates.Text = $"({absX:000},{absY:000}) = (0x{absX:X2},0x{absY:X2})"; private void NUD_Layer_ValueChanged(object sender, EventArgs e) { @@ -809,15 +931,13 @@ private void NUD_Layer_ValueChanged(object sender, EventArgs e) private void Remove(ToolStripItem sender, Func removal) { - bool wholeMap = (ModifierKeys & Keys.Shift) != 0; - - string q = string.Format(MessageStrings.MsgFieldItemRemoveAsk, sender.Text); - var question = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, q); + var isModifyEntireMap = (ModifierKeys & Keys.Shift) != 0; + var message = string.Format(MessageStrings.MsgFieldItemRemoveAsk, sender.Text); + var question = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, message); if (question != DialogResult.Yes) return; - int count = Editor.Mutator.ModifyFieldItems(removal, wholeMap); - + var count = Editor.Mutator.ModifyFieldItems(removal, isModifyEntireMap); if (count == 0) { WinFormsUtil.Alert(MessageStrings.MsgFieldItemRemoveNone); @@ -829,15 +949,13 @@ private void Remove(ToolStripItem sender, Func removal) private void Modify(ToolStripItem sender, Func action) { - bool wholeMap = (ModifierKeys & Keys.Shift) != 0; - - string q = string.Format(MessageStrings.MsgFieldItemModifyAsk, sender.Text); - var question = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, q); + var isModifyEntireMap = (ModifierKeys & Keys.Shift) != 0; + var message = string.Format(MessageStrings.MsgFieldItemModifyAsk, sender.Text); + var question = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, message); if (question != DialogResult.Yes) return; - int count = Editor.Mutator.ModifyFieldItems(action, wholeMap); - + var count = Editor.Mutator.ModifyFieldItems(action, isModifyEntireMap); if (count == 0) { WinFormsUtil.Alert(MessageStrings.MsgFieldItemModifyNone); @@ -917,14 +1035,14 @@ private void LB_Items_SelectedIndexChanged(object sender, EventArgs e) { if (LB_Items.SelectedIndex < 0) return; - LoadIndex(LB_Items.SelectedIndex); + LoadBuildingIndex(LB_Items.SelectedIndex); // View location snap has changed the view. Reload everything LoadItemGridAcre(); ReloadMapBackground(); } - private void LoadIndex(int index) + private void LoadBuildingIndex(int index) { Loading = true; SelectedBuildingIndex = index; @@ -939,9 +1057,9 @@ private void LoadIndex(int index) NUD_UniqueID.Value = b.UniqueID; Loading = false; - // -32 for relative offset on map (buildings can be placed on the exterior ocean acres) + // Jump the view to see the building // -16 to put it in the center of the view - const int shift = 48; + const int shift = 16; var x = (b.X - shift) & 0xFFFE; var y = (b.Y - shift) & 0xFFFE; View.SetViewTo(x, y); @@ -996,6 +1114,7 @@ private void CB_MapAcreSelect_SelectedValueChanged(object sender, EventArgs e) var index = CB_MapAcre.SelectedIndex; var value = WinFormsUtil.GetIndex(CB_MapAcreSelect); + // u16[], but values are at most u8 each. var span = Editor.Terrain.BaseAcres.Span.Slice(index * 2, 2); var oldValue = span[0]; if (value == oldValue) diff --git a/NHSE.WinForms/Subforms/Map/PlayerHouseEditor.cs b/NHSE.WinForms/Subforms/Map/PlayerHouseEditor.cs index cfd6a3a..e7365e3 100644 --- a/NHSE.WinForms/Subforms/Map/PlayerHouseEditor.cs +++ b/NHSE.WinForms/Subforms/Map/PlayerHouseEditor.cs @@ -169,7 +169,10 @@ private void DrawRoom(LayerItem layer) Span scale1 = stackalloc int[w * h]; var scaleX = new int[scale * scale * scale1.Length]; var bmp = new Bitmap(scale * w, scale * h); - ItemLayerSprite.LoadItemLayerViewGrid(bmp, layer, 0, 0, scale1, scaleX, scale, gridlineColor: 0x7F000000); + + // 10x10 items (2x2 tiles per item) + var cfg = LayerPositionConfig.Create(1, 1, 20, 1); + ItemLayerSprite.LoadViewport(bmp, layer, cfg, 0, 0, scale1, scaleX, scale, gridlineColor: 0x7F000000); PB_Room.Image = bmp; }