NHSE/NHSE.WinForms/Subforms/Map/FieldItemEditor.cs
Kurt c25cd262d4 Add automatic delete/set of extension tiles from root
Setting a root tile: will fill in the extension tiles
Deleting a root tile: will delete all extension tiles (it assumes extension tiles are valid)

Add checkbox to prevent overwriting tiles that aren't none (eg if you set a 4x4 and it'd overlap with a 2x2, the program would play a sound indicating that the requested operation was disallowed).

Closes #136 -- multi-select with the current drawing setup isn't really feasible. Bulk set/delete addresses the underlying request.
2020-05-02 00:20:44 -07:00

487 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Windows.Forms;
using NHSE.Core;
using NHSE.Sprites;
namespace NHSE.WinForms
{
public partial class FieldItemEditor : Form
{
private readonly MainSave SAV;
private readonly FieldItemManager Items;
private const int MapScale = 1;
private const int AcreScale = 16;
// Cached acre view objects to remove allocation/GC
private readonly int[] Scale1;
private readonly int[] ScaleX;
private readonly Bitmap ScaleAcre;
private readonly int[] Map;
private readonly Bitmap MapReticle;
private readonly TerrainManager Terrain;
private readonly IReadOnlyList<Building> Buildings;
private readonly uint PlazaX;
private readonly uint PlazaY;
private int X;
private int Y;
public FieldItemEditor(MainSave sav)
{
InitializeComponent();
this.TranslateInterface(GameInfo.CurrentLanguage);
SAV = sav;
Items = new FieldItemManager(SAV.GetFieldItems());
var l1 = Items.Layer1;
Scale1 = new int[l1.GridWidth * l1.GridHeight];
ScaleX = new int[Scale1.Length * AcreScale * AcreScale];
ScaleAcre = new Bitmap(l1.GridWidth * AcreScale, l1.GridHeight * AcreScale);
Map = new int[l1.MapWidth * l1.MapHeight * MapScale * MapScale];
MapReticle = new Bitmap(l1.MapWidth * MapScale, l1.MapHeight * MapScale);
Terrain = new TerrainManager(sav.GetTerrain());
Buildings = sav.Buildings;
PlazaX = sav.PlazaX;
PlazaY = sav.PlazaY;
PB_Map.BackgroundImage = TerrainSprite.GetMapWithBuildings(Terrain, Buildings, (ushort)PlazaX, (ushort)PlazaY, null, 2);
foreach (var acre in MapGrid.Acres)
CB_Acre.Items.Add(acre.Name);
PG_Tile.SelectedObject = new FieldItem();
CB_Acre.SelectedIndex = 0;
LoadGrid(X, Y);
}
private int AcreIndex => CB_Acre.SelectedIndex;
private void ChangeAcre(object sender, EventArgs e) => ChangeViewToAcre(AcreIndex);
private void ChangeViewToAcre(int acre)
{
Items.Layer1.GetViewAnchorCoordinates(acre, out X, out Y);
LoadGrid(X, Y);
}
private FieldItemLayer Layer => NUD_Layer.Value == 1 ? Items.Layer1 : Items.Layer2;
private void ReloadMap()
{
var transparency = TR_Transparency.Value / 100d;
var t = ((int)(0xFF * transparency) << 24) | 0x00FF_FFFF;
PB_Map.Image = FieldItemSpriteDrawer.GetBitmapLayer(Layer, X, Y, Map, MapReticle, t);
}
private void LoadGrid(int topX, int topY)
{
ReloadGrid(Layer, topX, topY);
ReloadBackground(topX, topY);
UpdateArrowVisibility();
ReloadMap();
}
private void ReloadBackground(int topX, int topY)
{
PB_Acre.BackgroundImage = TerrainSprite.GetAcre(topX/2, topY/2, Terrain, AcreScale * 2, Buildings, (ushort)PlazaX, (ushort)PlazaY);
}
private void ReloadGrid(FieldItemLayer layer, int topX, int topY)
{
var transparency = TR_Transparency.Value / 100d;
var t = ((int) (0xFF * transparency) << 24) | 0x00FF_FFFF;
PB_Acre.Image = FieldItemSpriteDrawer.GetBitmapLayerAcre(layer, topX, topY, AcreScale, Scale1, ScaleX, ScaleAcre, t);
}
private void UpdateArrowVisibility()
{
B_Up.Enabled = Y != 0;
B_Down.Enabled = Y < Layer.MapHeight - Layer.GridHeight;
B_Left.Enabled = X != 0;
B_Right.Enabled = X < Layer.MapWidth - Layer.GridWidth;
}
private void PB_Acre_MouseClick(object sender, MouseEventArgs e)
{
var tile = GetTile(Layer, e, out var x, out var y);
switch (ModifierKeys)
{
default: ViewTile(tile); return;
case Keys.Shift: SetTile(tile, x, y); return;
case Keys.Alt: DeleteTile(tile, x, y); return;
}
}
private int HoverX;
private int HoverY;
private FieldItem GetTile(FieldItemLayer layer, MouseEventArgs e, out int x, out int y)
{
SetHoveredItem(e);
return layer.GetTile(x = X + HoverX, y = Y + HoverY);
}
private void SetHoveredItem(MouseEventArgs e)
{
HoverX = e.X / AcreScale;
HoverY = e.Y / AcreScale;
}
private void PB_Acre_MouseMove(object sender, MouseEventArgs e)
{
var oldTile = Layer.GetTile(X + HoverX, Y + HoverY);
var tile = GetTile(Layer, e, out _, out _);
if (tile == oldTile)
return;
var str = GameInfo.Strings;
var name = str.GetItemName(tile);
TT_Hover.SetToolTip(PB_Acre, name);
}
private void ViewTile(FieldItem tile)
{
var pgt = (FieldItem)PG_Tile.SelectedObject;
pgt.CopyFrom(tile);
PG_Tile.SelectedObject = pgt;
}
private void SetTile(FieldItem tile, int x, int y)
{
var pgt = (FieldItem)PG_Tile.SelectedObject;
if (CHK_NoOverwrite.Checked && Layer.IsOccupied(pgt, x, y))
{
System.Media.SystemSounds.Asterisk.Play();
return;
}
// Clean up original placed data
if (tile.IsRoot)
Layer.DeleteExtensionTiles(tile, x, y);
// Set new placed data
if (pgt.IsRoot)
Layer.SetExtensionTiles(pgt, x, y);
tile.CopyFrom(pgt);
ReloadGrid(Layer, X, Y);
ReloadMap();
}
private void DeleteTile(FieldItem tile, int x, int y)
{
if (tile.IsRoot)
Layer.DeleteExtensionTiles(tile, x, y);
tile.Delete();
ReloadGrid(Layer, X, Y);
ReloadMap();
}
private void B_Cancel_Click(object sender, EventArgs e) => Close();
private void B_Save_Click(object sender, EventArgs e)
{
var all = ArrayUtil.ConcatAll(Items.Layer1.Tiles, Items.Layer2.Tiles);
SAV.SetFieldItems(all);
Close();
}
private void Menu_View_Click(object sender, EventArgs e)
{
var tile = Layer.GetTile(X + HoverX, Y + HoverY);
ViewTile(tile);
}
private void Menu_Set_Click(object sender, EventArgs e)
{
int x = X + HoverX;
int y = Y + HoverY;
var tile = Layer.GetTile(x, y);
SetTile(tile, x, y);
}
private void Menu_Reset_Click(object sender, EventArgs e)
{
int x = X + HoverX;
int y = Y + HoverY;
var tile = Layer.GetTile(x, y);
DeleteTile(tile, x, y);
}
private void B_Up_Click(object sender, EventArgs e)
{
if (ModifierKeys != Keys.Shift)
{
if (Y != 0)
LoadGrid(X, Y -= 2);
return;
}
CB_Acre.SelectedIndex -= MapGrid.AcreWidth;
}
private void B_Left_Click(object sender, EventArgs e)
{
if (ModifierKeys != Keys.Shift)
{
if (X != 0)
LoadGrid(X -= 2, Y);
return;
}
--CB_Acre.SelectedIndex;
}
private void B_Right_Click(object sender, EventArgs e)
{
if (ModifierKeys != Keys.Shift)
{
if (X != Layer.MapWidth - 2)
LoadGrid(X += 2, Y);
return;
}
++CB_Acre.SelectedIndex;
}
private void B_Down_Click(object sender, EventArgs e)
{
if (ModifierKeys != Keys.Shift)
{
if (Y != Layer.MapHeight - 1)
LoadGrid(X, ++Y);
return;
}
CB_Acre.SelectedIndex += MapGrid.AcreWidth;
}
private void B_DumpAcre_Click(object sender, EventArgs e)
{
var layer = Layer;
using var sfd = new SaveFileDialog
{
Filter = "New Horizons Field Item Layer (*.nhl)|*.nhl|All files (*.*)|*.*",
FileName = $"{CB_Acre.Text}-{NUD_Layer.Value}.nhl",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
var path = sfd.FileName;
var acre = AcreIndex;
var data = layer.DumpAcre(acre);
File.WriteAllBytes(path, data);
}
private void B_DumpAllAcres_Click(object sender, EventArgs e)
{
var layer = Layer;
using var sfd = new SaveFileDialog
{
Filter = "New Horizons Field Item Layer (*.nhl)|*.nhl|All files (*.*)|*.*",
FileName = "acres.nhl",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
var path = sfd.FileName;
var data = layer.DumpAllAcres();
File.WriteAllBytes(path, data);
}
private void B_ImportAcre_Click(object sender, EventArgs e)
{
var layer = Layer;
using var ofd = new OpenFileDialog
{
Filter = "New Horizons Field Item Layer (*.nhl)|*.nhl|All files (*.*)|*.*",
FileName = $"{CB_Acre.Text}-{NUD_Layer.Value}.nhl",
};
if (ofd.ShowDialog() != DialogResult.OK)
return;
var path = ofd.FileName;
var fi = new FileInfo(path);
int expect = layer.AcreTileCount * FieldItem.SIZE;
if (fi.Length != expect)
{
WinFormsUtil.Error(string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expect));
return;
}
var data = File.ReadAllBytes(path);
layer.ImportAcre(AcreIndex, data);
ChangeViewToAcre(AcreIndex);
System.Media.SystemSounds.Asterisk.Play();
}
private void B_ImportAllAcres_Click(object sender, EventArgs e)
{
var layer = Layer;
using var ofd = new OpenFileDialog
{
Filter = "New Horizons Field Item Layer (*.nhl)|*.nhl|All files (*.*)|*.*",
FileName = "acres.nhl",
};
if (ofd.ShowDialog() != DialogResult.OK)
return;
var path = ofd.FileName;
var fi = new FileInfo(path);
int expect = layer.MapTileCount * FieldItem.SIZE;
if (fi.Length != expect)
{
WinFormsUtil.Error(string.Format(MessageStrings.MsgDataSizeMismatchImport, fi.Length, expect));
return;
}
var data = File.ReadAllBytes(path);
layer.ImportAllAcres(data);
ChangeViewToAcre(AcreIndex);
System.Media.SystemSounds.Asterisk.Play();
}
private void Menu_SavePNG_Click(object sender, EventArgs e)
{
var pb = WinFormsUtil.GetUnderlyingControl<PictureBox>(sender);
if (pb?.Image == null)
{
WinFormsUtil.Alert(MessageStrings.MsgNoPictureLoaded);
return;
}
const string name = "map";
var bmp = FieldItemSpriteDrawer.GetBitmapLayer(Items.Layer1);
using var sfd = new SaveFileDialog
{
Filter = "png file (*.png)|*.png|All files (*.*)|*.*",
FileName = $"{name}.png",
};
if (sfd.ShowDialog() != DialogResult.OK)
return;
bmp.Save(sfd.FileName, ImageFormat.Png);
}
private void PB_Map_MouseDown(object sender, MouseEventArgs e) => ClickMapAt(e, true);
private void ClickMapAt(MouseEventArgs e, bool skipLagCheck)
{
var layer = Items.Layer1;
int mX = e.X;
int mY = e.Y;
bool centerReticle = CHK_SnapToAcre.Checked;
GetViewAnchorCoordinates(mX, mY, out var x, out var y, centerReticle);
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(X - x);
var dy = Math.Abs(Y - y);
if (dx <= delta && dy <= delta && !sameAcre)
return;
}
}
if (!CHK_SnapToAcre.Checked)
{
LoadGrid(X = x, Y = y);
return;
}
if (!sameAcre)
CB_Acre.SelectedIndex = acre;
}
private static void GetCursorCoordinates(int mX, int mY, out int x, out int y)
{
x = mX / MapScale;
y = mY / MapScale;
}
private void GetViewAnchorCoordinates(int mX, int mY, out int x, out int y, bool centerReticle)
{
var layer = Items.Layer1;
GetCursorCoordinates(mX, mY, out x, out y);
layer.GetViewAnchorCoordinates(ref x, ref y, centerReticle);
}
private void PB_Map_MouseMove(object sender, MouseEventArgs e)
{
if (e.Button == MouseButtons.Left)
{
ClickMapAt(e, false);
}
else
{
int mX = e.X;
int mY = e.Y;
GetCursorCoordinates(mX, mY, out var x, out var y);
L_Coordinates.Text = $"({x:000},{y:000}) = (0x{x:X2},0x{y:X2})";
}
}
private void NUD_Layer_ValueChanged(object sender, EventArgs e) => LoadGrid(X, Y);
private void Remove(Control sender, Func<int, int, int, int, int> removal)
{
bool wholeMap = ModifierKeys == Keys.Shift;
string q = string.Format(MessageStrings.MsgFieldItemRemoveAsk, sender.Text);
var question = WinFormsUtil.Prompt(MessageBoxButtons.YesNo, q);
if (question != DialogResult.Yes)
return;
var count = wholeMap
? removal(0, 0, Layer.MapWidth, Layer.MapHeight)
: removal(X, Y, Layer.GridWidth, Layer.GridHeight);
if (count == 0)
{
WinFormsUtil.Alert(MessageStrings.MsgFieldItemRemoveNone);
return;
}
LoadGrid(X, Y);
WinFormsUtil.Alert(string.Format(MessageStrings.MsgFieldItemRemoveCount, count));
}
private void B_RemoveAllWeeds_Click(object sender, EventArgs e) => Remove(B_RemoveAllWeeds, Layer.RemoveAllWeeds);
private void B_FillHoles_Click(object sender, EventArgs e) => Remove(B_FillHoles, Layer.RemoveAllHoles);
private void B_RemovePlants_Click(object sender, EventArgs e) => Remove(B_RemovePlants, Layer.RemoveAllPlants);
private void B_RemoveFences_Click(object sender, EventArgs e) => Remove(B_RemoveFences, Layer.RemoveAllFences);
private void B_RemoveObjects_Click(object sender, EventArgs e) => Remove(B_RemoveObjects, Layer.RemoveAllObjects);
private void B_RemoveAll_Click(object sender, EventArgs e) => Remove(B_RemoveAll, Layer.RemoveAll);
private void B_RemovePlacedItems_Click(object sender, EventArgs e) => Remove(B_RemovePlacedItems, Layer.RemoveAllPlacedItems);
private void PG_Tile_PropertyValueChanged(object s, PropertyValueChangedEventArgs e) => PG_Tile.SelectedObject = PG_Tile.SelectedObject;
private void TR_Transparency_Scroll(object sender, EventArgs e)
{
ReloadGrid(Layer, X, Y);
ReloadMap();
}
}
}