mirror of
https://github.com/kwsch/NHSE.git
synced 2026-08-28 13:54:24 -05:00
Stash
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:
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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 { },
|
||||
};
|
||||
}
|
||||
@@ -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 },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
2183
NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs
generated
2183
NHSE.WinForms/Subforms/Map/FieldItemEditor.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
-->
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user