HexManiacAdvance/src/HexManiac.Core/ViewModels/Map/BlockEditor.cs
haven1433 5c5f51fcc9 Sprite export improvement
Allow exporting indexed PNGs
Other changes:
* multi-select blocks
* edit borders
* fix castform export
2022-10-11 20:11:37 -05:00

798 lines
31 KiB
C#

using HavenSoft.HexManiac.Core.Models;
using HavenSoft.HexManiac.Core.Models.Runs;
using HavenSoft.HexManiac.Core.Models.Runs.Sprites;
using HavenSoft.HexManiac.Core.ViewModels.DataFormats;
using HavenSoft.HexManiac.Core.ViewModels.Images;
using HavenSoft.HexManiac.Core.ViewModels.Tools;
using HexManiac.Core.Models.Runs.Sprites;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Windows;
/*
#define MB_NORMAL 0x00
#define MB_SECRET_BASE_WALL 0x01
#define MB_TALL_GRASS 0x02
#define MB_LONG_GRASS 0x03
#define MB_UNUSED_04 0x04
#define MB_UNUSED_05 0x05
#define MB_DEEP_SAND 0x06
#define MB_SHORT_GRASS 0x07
#define MB_CAVE 0x08
#define MB_LONG_GRASS_SOUTH_EDGE 0x09
#define MB_NO_RUNNING 0x0A
#define MB_INDOOR_ENCOUNTER 0x0B
#define MB_MOUNTAIN_TOP 0x0C
#define MB_BATTLE_PYRAMID_WARP 0x0D
#define MB_MOSSDEEP_GYM_WARP 0x0E
#define MB_MT_PYRE_HOLE 0x0F
#define MB_POND_WATER 0x10
#define MB_SEMI_DEEP_WATER 0x11
#define MB_DEEP_WATER 0x12
#define MB_WATERFALL 0x13
#define MB_SOOTOPOLIS_DEEP_WATER 0x14
#define MB_OCEAN_WATER 0x15
#define MB_PUDDLE 0x16
#define MB_SHALLOW_WATER 0x17
#define MB_UNUSED_SOOTOPOLIS_DEEP_WATER 0x18
#define MB_NO_SURFACING 0x19
#define MB_UNUSED_SOOTOPOLIS_DEEP_WATER_2 0x1A
#define MB_STAIRS_OUTSIDE_ABANDONED_SHIP 0x1B
#define MB_SHOAL_CAVE_ENTRANCE 0x1C
#define MB_UNUSED_1D 0x1D
#define MB_UNUSED_1E 0x1E
#define MB_UNUSED_1F 0x1F
#define MB_ICE 0x20
#define MB_SAND 0x21
#define MB_SEAWEED 0x22
#define MB_UNUSED_23 0x23
#define MB_ASHGRASS 0x24
#define MB_FOOTPRINTS 0x25
#define MB_THIN_ICE 0x26
#define MB_CRACKED_ICE 0x27
#define MB_HOT_SPRINGS 0x28
#define MB_LAVARIDGE_GYM_B1F_WARP 0x29
#define MB_SEAWEED_NO_SURFACING 0x2A
#define MB_REFLECTION_UNDER_BRIDGE 0x2B
#define MB_UNUSED_2C 0x2C
#define MB_UNUSED_2D 0x2D
#define MB_UNUSED_2E 0x2E
#define MB_UNUSED_2F 0x2F
#define MB_IMPASSABLE_EAST 0x30
#define MB_IMPASSABLE_WEST 0x31
#define MB_IMPASSABLE_NORTH 0x32
#define MB_IMPASSABLE_SOUTH 0x33
#define MB_IMPASSABLE_NORTHEAST 0x34
#define MB_IMPASSABLE_NORTHWEST 0x35
#define MB_IMPASSABLE_SOUTHEAST 0x36
#define MB_IMPASSABLE_SOUTHWEST 0x37
#define MB_JUMP_EAST 0x38
#define MB_JUMP_WEST 0x39
#define MB_JUMP_NORTH 0x3A
#define MB_JUMP_SOUTH 0x3B
#define MB_JUMP_NORTHEAST 0x3C
#define MB_JUMP_NORTHWEST 0x3D
#define MB_JUMP_SOUTHEAST 0x3E
#define MB_JUMP_SOUTHWEST 0x3F
#define MB_WALK_EAST 0x40
#define MB_WALK_WEST 0x41
#define MB_WALK_NORTH 0x42
#define MB_WALK_SOUTH 0x43
#define MB_SLIDE_EAST 0x44
#define MB_SLIDE_WEST 0x45
#define MB_SLIDE_NORTH 0x46
#define MB_SLIDE_SOUTH 0x47
#define MB_TRICK_HOUSE_PUZZLE_8_FLOOR 0x48
#define MB_UNUSED_49 0x49
#define MB_UNUSED_4A 0x4A
#define MB_UNUSED_4B 0x4B
#define MB_UNUSED_4C 0x4C
#define MB_UNUSED_4D 0x4D
#define MB_UNUSED_4E 0x4E
#define MB_UNUSED_4F 0x4F
#define MB_EASTWARD_CURRENT 0x50
#define MB_WESTWARD_CURRENT 0x51
#define MB_NORTHWARD_CURRENT 0x52
#define MB_SOUTHWARD_CURRENT 0x53
#define MB_UNUSED_54 0x54
#define MB_UNUSED_55 0x55
#define MB_UNUSED_56 0x56
#define MB_UNUSED_57 0x57
#define MB_UNUSED_58 0x58
#define MB_UNUSED_59 0x59
#define MB_UNUSED_5A 0x5A
#define MB_UNUSED_5B 0x5B
#define MB_UNUSED_5C 0x5C
#define MB_UNUSED_5D 0x5D
#define MB_UNUSED_5E 0x5E
#define MB_UNUSED_5F 0x5F
#define MB_NON_ANIMATED_DOOR 0x60
#define MB_LADDER 0x61
#define MB_EAST_ARROW_WARP 0x62
#define MB_WEST_ARROW_WARP 0x63
#define MB_NORTH_ARROW_WARP 0x64
#define MB_SOUTH_ARROW_WARP 0x65
#define MB_CRACKED_FLOOR_HOLE 0x66
#define MB_AQUA_HIDEOUT_WARP 0x67
#define MB_LAVARIDGE_GYM_1F_WARP 0x68
#define MB_ANIMATED_DOOR 0x69
#define MB_UP_ESCALATOR 0x6A
#define MB_DOWN_ESCALATOR 0x6B
#define MB_WATER_DOOR 0x6C
#define MB_WATER_SOUTH_ARROW_WARP 0x6D
#define MB_DEEP_SOUTH_WARP 0x6E
#define MB_UNUSED_6F 0x6F
#define MB_BRIDGE_OVER_OCEAN 0x70
#define MB_BRIDGE_OVER_POND_LOW 0x71
#define MB_BRIDGE_OVER_POND_MED 0x72
#define MB_BRIDGE_OVER_POND_HIGH 0x73
#define MB_PACIFIDLOG_VERTICAL_LOG_TOP 0x74
#define MB_PACIFIDLOG_VERTICAL_LOG_BOTTOM 0x75
#define MB_PACIFIDLOG_HORIZONTAL_LOG_LEFT 0x76
#define MB_PACIFIDLOG_HORIZONTAL_LOG_RIGHT 0x77
#define MB_FORTREE_BRIDGE 0x78
#define MB_UNUSED_79 0x79
#define MB_BRIDGE_OVER_POND_MED_EDGE_1 0x7A
#define MB_BRIDGE_OVER_POND_MED_EDGE_2 0x7B
#define MB_BRIDGE_OVER_POND_HIGH_EDGE_1 0x7C
#define MB_BRIDGE_OVER_POND_HIGH_EDGE_2 0x7D
#define MB_UNUSED_BRIDGE 0x7E
#define MB_BIKE_BRIDGE_OVER_BARRIER 0x7F
#define MB_COUNTER 0x80
#define MB_UNUSED_81 0x81
#define MB_UNUSED_82 0x82
#define MB_PC 0x83
#define MB_CABLE_BOX_RESULTS_1 0x84
#define MB_REGION_MAP 0x85
#define MB_TELEVISION 0x86
#define MB_POKEBLOCK_FEEDER 0x87
#define MB_UNUSED_88 0x88
#define MB_SLOT_MACHINE 0x89
#define MB_ROULETTE 0x8A
#define MB_CLOSED_SOOTOPOLIS_DOOR 0x8B
#define MB_TRICK_HOUSE_PUZZLE_DOOR 0x8C
#define MB_PETALBURG_GYM_DOOR 0x8D
#define MB_RUNNING_SHOES_INSTRUCTION 0x8E
#define MB_QUESTIONNAIRE 0x8F
#define MB_SECRET_BASE_SPOT_RED_CAVE 0x90
#define MB_SECRET_BASE_SPOT_RED_CAVE_OPEN 0x91
#define MB_SECRET_BASE_SPOT_BROWN_CAVE 0x92
#define MB_SECRET_BASE_SPOT_BROWN_CAVE_OPEN 0x93
#define MB_SECRET_BASE_SPOT_YELLOW_CAVE 0x94
#define MB_SECRET_BASE_SPOT_YELLOW_CAVE_OPEN 0x95
#define MB_SECRET_BASE_SPOT_TREE_LEFT 0x96
#define MB_SECRET_BASE_SPOT_TREE_LEFT_OPEN 0x97
#define MB_SECRET_BASE_SPOT_SHRUB 0x98
#define MB_SECRET_BASE_SPOT_SHRUB_OPEN 0x99
#define MB_SECRET_BASE_SPOT_BLUE_CAVE 0x9A
#define MB_SECRET_BASE_SPOT_BLUE_CAVE_OPEN 0x9B
#define MB_SECRET_BASE_SPOT_TREE_RIGHT 0x9C
#define MB_SECRET_BASE_SPOT_TREE_RIGHT_OPEN 0x9D
#define MB_UNUSED_9E 0x9E
#define MB_UNUSED_9F 0x9F
#define MB_BERRY_TREE_SOIL 0xA0
#define MB_UNUSED_A1 0xA1
#define MB_UNUSED_A2 0xA2
#define MB_UNUSED_A3 0xA3
#define MB_UNUSED_A4 0xA4
#define MB_UNUSED_A5 0xA5
#define MB_UNUSED_A6 0xA6
#define MB_UNUSED_A7 0xA7
#define MB_UNUSED_A8 0xA8
#define MB_UNUSED_A9 0xA9
#define MB_UNUSED_AA 0xAA
#define MB_UNUSED_AB 0xAB
#define MB_UNUSED_AC 0xAC
#define MB_UNUSED_AD 0xAD
#define MB_UNUSED_AE 0xAE
#define MB_UNUSED_AF 0xAF
#define MB_SECRET_BASE_PC 0xB0
#define MB_SECRET_BASE_REGISTER_PC 0xB1
#define MB_SECRET_BASE_SCENERY 0xB2
#define MB_SECRET_BASE_TRAINER_SPOT 0xB3
#define MB_SECRET_BASE_DECORATION 0xB4
#define MB_HOLDS_SMALL_DECORATION 0xB5
#define MB_UNUSED_B6 0xB6
#define MB_SECRET_BASE_NORTH_WALL 0xB7
#define MB_SECRET_BASE_BALLOON 0xB8
#define MB_SECRET_BASE_IMPASSABLE 0xB9
#define MB_SECRET_BASE_GLITTER_MAT 0xBA
#define MB_SECRET_BASE_JUMP_MAT 0xBB
#define MB_SECRET_BASE_SPIN_MAT 0xBC
#define MB_SECRET_BASE_SOUND_MAT 0xBD
#define MB_SECRET_BASE_BREAKABLE_DOOR 0xBE
#define MB_SECRET_BASE_SAND_ORNAMENT 0xBF
#define MB_IMPASSABLE_SOUTH_AND_NORTH 0xC0
#define MB_IMPASSABLE_WEST_AND_EAST 0xC1
#define MB_SECRET_BASE_HOLE 0xC2
#define MB_HOLDS_LARGE_DECORATION 0xC3
#define MB_SECRET_BASE_TV_SHIELD 0xC4
#define MB_PLAYER_ROOM_PC_ON 0xC5
#define MB_SECRET_BASE_DECORATION_BASE 0xC6
#define MB_SECRET_BASE_POSTER 0xC7
#define MB_UNUSED_C8 0xC8
#define MB_UNUSED_C9 0xC9
#define MB_UNUSED_CA 0xCA
#define MB_UNUSED_CB 0xCB
#define MB_UNUSED_CC 0xCC
#define MB_UNUSED_CD 0xCD
#define MB_UNUSED_CE 0xCE
#define MB_UNUSED_CF 0xCF
#define MB_MUDDY_SLOPE 0xD0
#define MB_BUMPY_SLOPE 0xD1
#define MB_CRACKED_FLOOR 0xD2
#define MB_ISOLATED_VERTICAL_RAIL 0xD3
#define MB_ISOLATED_HORIZONTAL_RAIL 0xD4
#define MB_VERTICAL_RAIL 0xD5
#define MB_HORIZONTAL_RAIL 0xD6
#define MB_UNUSED_D7 0xD7
#define MB_UNUSED_D8 0xD8
#define MB_UNUSED_D9 0xD9
#define MB_UNUSED_DA 0xDA
#define MB_UNUSED_DB 0xDB
#define MB_UNUSED_DC 0xDC
#define MB_UNUSED_DD 0xDD
#define MB_UNUSED_DE 0xDE
#define MB_UNUSED_DF 0xDF
#define MB_PICTURE_BOOK_SHELF 0xE0
#define MB_BOOKSHELF 0xE1
#define MB_POKEMON_CENTER_BOOKSHELF 0xE2
#define MB_VASE 0xE3
#define MB_TRASH_CAN 0xE4
#define MB_SHOP_SHELF 0xE5
#define MB_BLUEPRINT 0xE6
#define MB_CABLE_BOX_RESULTS_2 0xE7
#define MB_WIRELESS_BOX_RESULTS 0xE8
#define MB_TRAINER_HILL_TIMER 0xE9
#define MB_SKY_PILLAR_CLOSED_DOOR 0xEA
#define MB_UNUSED_EB 0xEB
#define MB_UNUSED_EC 0xEC
#define MB_UNUSED_ED 0xED
#define MB_UNUSED_EE 0xEE
#define MB_UNUSED_EF 0xEF
#define NUM_METATILE_BEHAVIORS 0xF0
#define MB_INVALID 0xFF
*/
namespace HavenSoft.HexManiac.Core.ViewModels.Map {
public abstract class TileAttribute {
public int Behavior { get; set; }
public int Layer { get; set; }
public abstract byte[] Serialize();
public static TileAttribute Create(byte[] data) {
if (data.Length == 2) return new AttributeRSE(data);
return new AttributeFRLG(data);
}
}
/*
[METATILE_ATTRIBUTE_BEHAVIOR] = 0x000001ff,
[METATILE_ATTRIBUTE_TERRAIN] = 0x00003e00, 00=normal 01=grass 10=water
[METATILE_ATTRIBUTE_2] = 0x0003c000,
[METATILE_ATTRIBUTE_3] = 0x00fc0000,
[METATILE_ATTRIBUTE_ENCOUNTER_TYPE] = 0x07000000, 00=none, 01=land, 10=water
[METATILE_ATTRIBUTE_5] = 0x18000000,
[METATILE_ATTRIBUTE_LAYER_TYPE] = 0x60000000, 00=normal, 01=covered, 10=split, (reserve 11 for triple-layer tile expansion)
[METATILE_ATTRIBUTE_7] = 0x80000000
*/
public class AttributeFRLG : TileAttribute {
public int Terrain { get; set; }
public int Encounter { get; set; }
public AttributeFRLG(byte[] data) {
Behavior = data[0];
Terrain = data[1] >> 1;
Encounter = data[3] & 3;
Layer = data[3] >> 5;
Debug.Assert((data.ReadMultiByteValue(0, 4) & 0x9CFFF900) == 0, "Expected attribute mask 9CFFF900 to be zero!");
}
public override byte[] Serialize() {
var result = new byte[4];
result[0] = (byte)Behavior;
result[1] = (byte)(Terrain << 1);
result[2] = 0;
result[3] = (byte)((Layer << 5) | Encounter);
return result;
}
}
/*
#define METATILE_ATTR_BEHAVIOR_MASK 0x00FF 'behavior' (see below)
#define METATILE_ATTR_LAYER_MASK 0x3000 00=normal, 01=covered, 10=split
*/
public class AttributeRSE : TileAttribute {
public AttributeRSE(byte[] data) {
Behavior = data[0];
Layer = data[1] >> 4;
Debug.Assert((data[1] & 0xF) == 0, "Expected attribute mask 0F00 to be zero!");
}
public override byte[] Serialize() {
var result = new byte[2];
result[0] = (byte)Behavior;
result[1] = (byte)(Layer << 4);
return result;
}
}
public class BorderEditor : ViewModelCore {
private readonly ChangeHistory<ModelDelta> changeHistory;
private readonly IDataModel model;
private readonly BlockMapViewModel blockmap;
public event EventHandler BorderChanged;
private bool showBorder;
public bool ShowBorderPanel { get => showBorder; set => Set(ref showBorder, value); }
private CanvasPixelViewModel borderRender;
public IPixelViewModel BorderRender {
get {
if (borderRender != null) return borderRender;
var layout = blockmap.GetLayout();
RenderBorder(layout);
return borderRender;
}
}
#region Border Width/Height
public bool HasBorderDimensions { get; private set; }
private int width = -1, height = -1;
public int Width {
get {
if (width != -1) return width;
width = HasBorderDimensions ? blockmap.GetLayout().GetValue("borderwidth") : 2;
return width;
}
set {
if (!HasBorderDimensions) return;
var oldWidth = Width;
if (oldWidth == value) return;
width = value;
var layout = blockmap.GetLayout();
layout.SetValue("borderwidth", value);
Resize(layout, oldWidth, Height, width, Height);
}
}
public int Height {
get {
if (height != -1) return height;
height = HasBorderDimensions ? blockmap.GetLayout().GetValue("borderheight") : 2;
return height;
}
set {
if (!HasBorderDimensions) return;
var oldHeight = Height;
if (oldHeight == value) return;
height = value;
var layout = blockmap.GetLayout();
layout.SetValue("borderheight", value);
Resize(layout, Width, oldHeight, width, Height);
}
}
private void Resize(ModelArrayElement layout, int oldWidth, int oldHeight, int newWidth, int newHeight) {
var existingData = new int[oldWidth, oldHeight];
var oldStart = layout.GetAddress("borderblock");
var considerRepoint = oldWidth * oldHeight < newWidth * newHeight;
var token = changeHistory.CurrentChange;
for (int y = 0; y < oldHeight; y++) {
for (int x = 0; x < oldWidth; x++) {
existingData[x, y] = model.ReadMultiByteValue(oldStart + (y * oldWidth + x) * 2, 2);
if (considerRepoint) model.WriteMultiByteValue(oldStart + (y * oldWidth + x) * 2, 2, token, 0xFFFF);
}
}
var newStart = oldStart;
if (considerRepoint) {
var run = model.GetNextRun(oldStart);
run = model.RelocateForExpansion(token, run, newWidth * newHeight * 2);
newStart = run.Start;
}
for (int y = 0; y < newHeight; y++) {
for (int x = 0; x < newWidth; x++) {
model.WriteMultiByteValue(newStart + (y * newWidth + x) * 2, 2, token, x < existingData.GetLength(0) && y < existingData.GetLength(1) ? existingData[x, y] : 0);
}
}
RenderBorder(layout);
NotifyPropertyChanged(nameof(BorderRender));
BorderChanged.Raise(this);
}
#endregion
public BorderEditor(BlockMapViewModel blockmap) {
this.blockmap = blockmap;
this.model = blockmap.ViewPort.Model;
this.changeHistory = blockmap.ViewPort.ChangeHistory;
var layout = blockmap.GetLayout();
HasBorderDimensions = layout.HasField("borderwidth") && layout.HasField("borderheight");
}
public void Draw(int blockIndex, double x, double y) {
if (blockIndex < 0 || blockIndex >= blockmap.BlockRenders.Count) return;
int xx = (int)(x / 16);
int yy = (int)(y / 16);
var layout = blockmap.GetLayout();
int width = Width;
var address = layout.GetAddress("borderblock");
address += (yy * width + xx) * 2;
var existingValue = model.ReadMultiByteValue(address, 2);
if (blockIndex == existingValue) return;
model.WriteMultiByteValue(address, 2, changeHistory.CurrentChange, blockIndex);
RenderBorder(layout);
NotifyPropertyChanged(nameof(BorderRender));
BorderChanged.Raise(this);
}
public void Draw(int[,] tiles, double x, double y) {
int xx = (int)(x / 16);
int yy = (int)(y / 16);
var layout = blockmap.GetLayout();
var (width, height) = (Width, Height);
var startAddress = layout.GetAddress("borderblock");
bool redraw = false;
for (int dx = 0; dx < tiles.GetLength(0); dx++) {
for (int dy = 0; dy < tiles.GetLength(1); dy++) {
if (yy + dy >= height) continue;
if (xx + dx >= width) continue;
var address = startAddress + ((yy + dy) * width + xx + dx) * 2;
var existingValue = model.ReadMultiByteValue(address, 2);
if (tiles[dx, dy] == existingValue) continue;
model.WriteMultiByteValue(address, 2, changeHistory.CurrentChange, tiles[dx, dy]);
redraw = true;
}
}
if (!redraw) return;
RenderBorder(layout);
NotifyPropertyChanged(nameof(BorderRender));
BorderChanged.Raise(this);
}
public int GetBlock(double x, double y) {
int xx = (int)(x / 16);
int yy = (int)(y / 16);
var layout = blockmap.GetLayout();
int width = Width;
var address = layout.GetAddress("borderblock");
address += (yy * width + xx) * 2;
var existingValue = model.ReadMultiByteValue(address, 2);
return existingValue;
}
private void RenderBorder(ModelArrayElement layout) {
int width = Width, height = Height;
var canvas = new CanvasPixelViewModel(width * 16, height * 16) { SpriteScale = 3 };
var borderStart = layout.GetAddress("borderblock");
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
var index = model.ReadMultiByteValue(borderStart + (y * width + x) * 2, 2) & 0x3FF;
if (index >= blockmap.BlockRenders.Count) continue;
canvas.Draw(blockmap.BlockRenders[index], x * 16, y * 16);
}
}
if (borderRender == null || borderRender.PixelWidth != canvas.PixelWidth || borderRender.PixelHeight != canvas.PixelHeight) {
borderRender = canvas;
} else {
borderRender.Draw(canvas, 0, 0);
}
}
}
public class BlockEditor : ViewModelCore {
private readonly ChangeHistory<ModelDelta> history;
private readonly short[][] palettes;
private readonly int[][,] tiles;
private readonly byte[][] blocks;
private readonly byte[][] blockAttributes;
private readonly IDictionary<IPixelViewModel, int> indexForTileImage;
private readonly CanvasPixelViewModel[] images;
private int hoverTile;
private int layerMode;
public int LayerMode {
get => layerMode;
set => Set(ref layerMode, value, arg => EnterTile(images[hoverTile]));
}
private int blockIndex = 0;
public int BlockIndex {
get => blockIndex;
set => Set(ref blockIndex, value.LimitToRange(0, blocks.Length - 1), UpdateBlockUI);
}
private static readonly List<((int, int), (int, int))> bottomLayerTogether, topLayerTogether, twoSets;
static BlockEditor() {
int p0 = 0, p1 = 24, p2 = 48, p3 = 72, p4 = 96, shortD = 8;
bottomLayerTogether = new() {
((p1 - shortD, p1), (p1, p1 - shortD)),
((p3 , p1), (p2, p1 - shortD)),
((p1 - shortD, p2), (p1, p3 )),
((p3 , p2), (p2, p3 )),
((p1 , p0), (p0, p1 )),
((p3 - shortD, p0), (p3, p1 )),
((p1 , p3), (p0, p3 - shortD)),
((p3 - shortD, p3), (p3, p3 - shortD)),
};
topLayerTogether = new() {
((p1 , p0), (p0, p1 )),
((p3 - shortD, p0), (p3, p1 )),
((p1 , p3), (p0, p3 - shortD)),
((p3 - shortD, p3), (p3, p3 - shortD)),
((p1 - shortD, p1), (p1, p1 - shortD)),
((p3 , p1), (p2, p1 - shortD)),
((p1 - shortD, p2), (p1, p3 )),
((p3 , p2), (p2, p3 )),
};
twoSets = new() {
((p2 - shortD, p2), (p2, p2 - shortD)),
((p4 , p2), (p3, p2 - shortD)),
((p2 - shortD, p3), (p2, p4 )),
((p4 , p3), (p3, p4 )),
((p0 - shortD, p0), (p0, p0 - shortD)),
((p2 , p0), (p1, p0 - shortD)),
((p0 - shortD, p1), (p0, p2 )),
((p2 , p1), (p1, p2 )),
};
}
public event EventHandler AutoscrollTiles;
public event EventHandler<byte[][]> BlocksChanged;
public event EventHandler<byte[][]> BlockAttributesChanged;
private IPixelViewModel tileRender;
public IPixelViewModel TileRender => tileRender;
private IPixelViewModel drawTileRender;
public IPixelViewModel DrawTileRender => drawTileRender;
public BlockEditor(ChangeHistory<ModelDelta> history, IDataModel listSource, short[][] palettes, int[][,] tiles, byte[][] blocks, byte[][] blockAttributes) {
this.history = history;
this.palettes = palettes;
this.tiles = tiles;
this.blocks = blocks;
this.blockAttributes = blockAttributes;
images = new CanvasPixelViewModel[8];
indexForTileImage = new Dictionary<IPixelViewModel, int>();
if (listSource.TryGetList("MapAttributeBehaviors", out var list)) {
foreach (var item in list) BehaviorOptions.Add(item);
}
foreach (var palette in palettes) Palettes.Add(new ReadonlyPaletteCollection(palette));
}
public IPixelViewModel LeftTopBack => images[0];
public IPixelViewModel RightTopBack => images[1];
public IPixelViewModel LeftBottomBack => images[2];
public IPixelViewModel RightBottomBack => images[3];
public IPixelViewModel LeftTopFront => images[4];
public IPixelViewModel RightTopFront => images[5];
public IPixelViewModel LeftBottomFront => images[6];
public IPixelViewModel RightBottomFront => images[7];
private static readonly string[] imageNames = "LeftTopBack,RightTopBack,LeftBottomBack,RightBottomBack,LeftTopFront,RightTopFront,LeftBottomFront,RightBottomFront".Split(",");
#region FlipV / FlipH
private int flipVLeft, flipVTop, flipHLeft, flipHTop;
public int FlipVLeft { get => flipVLeft; private set => Set(ref flipVLeft, value); }
public int FlipVTop { get => flipVTop; private set => Set(ref flipVTop, value); }
public int FlipHLeft { get => flipHLeft; private set => Set(ref flipHLeft, value); }
public int FlipHTop { get => flipHTop; private set => Set(ref flipHTop, value); }
private bool flipVVisible, flipHVisible;
public bool FlipVVisible { get => flipVVisible; set => Set(ref flipVVisible, value); }
public bool FlipHVisible { get => flipHVisible; set => Set(ref flipHVisible, value); }
public void FlipH() {
var (pal, hFlip, vFlip, tile) = LzTilemapRun.ReadTileData(blocks[blockIndex], hoverTile, 2);
hFlip = !hFlip;
LzTilemapRun.WriteTileData(blocks[blockIndex], hoverTile, pal, hFlip, vFlip, tile);
var newImage = BlocksetModel.Read(blocks[blockIndex], hoverTile, tiles, palettes);
images[hoverTile].Fill(newImage.PixelData);
BlocksChanged?.Invoke(this, blocks);
}
public void FlipV() {
var (pal, hFlip, vFlip, tile) = LzTilemapRun.ReadTileData(blocks[blockIndex], hoverTile, 2);
vFlip = !vFlip;
LzTilemapRun.WriteTileData(blocks[blockIndex], hoverTile, pal, hFlip, vFlip, tile);
var newImage = BlocksetModel.Read(blocks[blockIndex], hoverTile, tiles, palettes);
images[hoverTile].Fill(newImage.PixelData);
BlocksChanged?.Invoke(this, blocks);
}
#endregion
public void EnterTile(IPixelViewModel tile) {
FlipVVisible = true;
FlipHVisible = true;
var index = indexForTileImage[tile];
((FlipVLeft, FlipVTop), (FlipHLeft, FlipHTop)) = layerMode == 0 ? twoSets[index] : layerMode == 1 ? bottomLayerTogether[index] : topLayerTogether[index];
hoverTile = indexForTileImage[tile];
}
public void DrawOnTile(IPixelViewModel tile) {
if (!showTiles) return;
var index = indexForTileImage[tile];
LzTilemapRun.WriteTileData(blocks[blockIndex], index, drawPalette, drawFlipH, drawFlipV, drawTile);
var newImage = BlocksetModel.Read(blocks[blockIndex], index, tiles, palettes);
images[hoverTile].Fill(newImage.PixelData);
BlocksChanged?.Invoke(this, blocks);
}
public void GetSelectionFromTile(IPixelViewModel tileImage) {
ShowTiles = true;
var index = indexForTileImage[tileImage];
var (pal, hFlip, vFlip, tileIndex) = LzTilemapRun.ReadTileData(blocks[blockIndex], index, 2);
drawTile = tileIndex;
(drawFlipV, drawFlipH) = (vFlip, hFlip);
PaletteSelection = pal;
NotifyPropertiesChanged(nameof(TileSelectionX), nameof(TileSelectionY));
AutoscrollTiles.Raise(this);
AnimateTileSelection();
history.ChangeCompleted();
UpdateDrawTileRender();
}
public void ExitTiles() {
FlipVVisible = false;
FlipHVisible = false;
}
#region Attribute UI
private int behavior, layer, terrain, encounter;
public int Behavior { get => behavior; set => Set(ref behavior, value, SaveAttributes); }
public int Layer { get => layer; set => Set(ref layer, value, SaveAttributes); }
public int Terrain { get => terrain; set => Set(ref terrain, value, SaveAttributes); }
public int Encounter { get => encounter; set => Set(ref encounter, value, SaveAttributes); }
public ObservableCollection<string> BehaviorOptions { get; } = new();
public ObservableCollection<string> LayerOptions { get; } = new() { "Normal", "Covered", "Split", };
public ObservableCollection<string> TerrainOptions { get; } = new() { "Normal", "Grass", "Water" };
public ObservableCollection<string> EncounterOptions { get; } = new() { "Normal", "Grass", "Water" };
private bool hasTerrainAndEncounter;
public bool HasTerrainAndEncounter { get => hasTerrainAndEncounter; set => Set(ref hasTerrainAndEncounter, value); }
private void UpdateAttributeUI() {
var attributes = TileAttribute.Create(blockAttributes[blockIndex]);
behavior = attributes.Behavior;
layer = attributes.Layer;
if (attributes is AttributeFRLG fr) {
terrain = fr.Terrain;
encounter = fr.Encounter;
}
new List<string> { nameof(Behavior), nameof(Layer), nameof(Terrain), nameof(Encounter) }.ForEach(NotifyPropertyChanged);
}
private void SaveAttributes(int arg = default) {
var attributes = TileAttribute.Create(blockAttributes[blockIndex]);
attributes.Behavior = behavior;
attributes.Layer = layer;
if (attributes is AttributeFRLG fr) {
fr.Terrain = terrain;
fr.Encounter = encounter;
}
blockAttributes[blockIndex] = attributes.Serialize();
BlockAttributesChanged?.Invoke(this, blockAttributes);
}
#endregion
private void UpdateBlockUI(int old) {
for (int i = 0; i < 8; i++) images[i] = null;
UpdateBlockUI();
}
private void UpdateBlockUI() {
if (blockIndex == -1) return;
for (int i = 0; i < 8; i++) {
if (images[i] == null) {
var image = BlocksetModel.Read(blocks[blockIndex], i, tiles, palettes);
images[i] = new CanvasPixelViewModel(image.PixelWidth, image.PixelHeight, image.PixelData) { SpriteScale = 3 };
indexForTileImage[images[i]] = i;
NotifyPropertyChanged(imageNames[i]);
}
}
indexForTileImage.Clear();
for (int i = 0; i < 8; i++) indexForTileImage[images[i]] = i;
UpdateAttributeUI();
}
#region Tile UI
const int TilesPerRow = 16, PixelPerTile = 24;
private bool showTiles;
public bool ShowTiles {
get => showTiles;
set => Set(ref showTiles, value, arg => {
if (showTiles && tileRender == null) UpdateTileRender(drawPalette);
});
}
public void HideTiles() => ShowTiles = false;
// hack: a DataTrigger can watch this property and start an animation whenever this property changes.
// the value doesn't matter, just the change
private bool tileSelectionToggle;
public bool TileSelectionToggle { get => tileSelectionToggle; set => Set(ref tileSelectionToggle, value); }
private void AnimateTileSelection() => TileSelectionToggle = !TileSelectionToggle;
private int drawTile, drawPalette;
private bool drawFlipV, drawFlipH;
public int TileSelectionX {
get => (drawTile % TilesPerRow) * PixelPerTile;
set {
var (x, y) = (drawTile % TilesPerRow, drawTile / TilesPerRow);
x = value / PixelPerTile;
drawTile = y * TilesPerRow + x;
if (drawTile >= tiles.Length) drawTile = tiles.Length - 1;
NotifyPropertyChanged();
drawFlipH = drawFlipV = false;
UpdateDrawTileRender();
}
}
public int TileSelectionY {
get => (drawTile / TilesPerRow) * PixelPerTile;
set {
var (x, y) = (drawTile % TilesPerRow, drawTile / TilesPerRow);
y = value / PixelPerTile;
drawTile = y * TilesPerRow + x;
if (drawTile >= tiles.Length) {
drawTile = tiles.Length - 1;
NotifyPropertiesChanged(nameof(TileSelectionX));
}
NotifyPropertyChanged();
drawFlipH = drawFlipV = false;
history.ChangeCompleted();
UpdateDrawTileRender();
}
}
public int PaletteSelection {
get => drawPalette;
set => Set(ref drawPalette, value, arg => UpdateTileRender(drawPalette));
}
public ObservableCollection<ReadonlyPaletteCollection> Palettes { get; } = new();
private void UpdateTileRender(int paletteIndex) {
var tileLines = (tiles.Length + TilesPerRow - 1) / TilesPerRow;
var pixelData = new short[8 * 8 * TilesPerRow * tileLines];
for (int i = 0; i < pixelData.Length; i++) pixelData[i] = short.MinValue;
var render = new CanvasPixelViewModel(8 * 16, 8 * 16 * 4, pixelData) { SpriteScale = 3, Transparent = short.MinValue };
var palette = palettes[paletteIndex];
for (int y = 0; y < 64; y++) {
for (int x = 0; x < 16; x++) {
var tile = new ReadonlyPixelViewModel(8, 8, SpriteTool.Render(tiles[y * 16 + x], palette, 0, 0), palette[0]);
if (tile == null || tile.PixelData.Length == 0) break;
render.Draw(tile, x * 8, y * 8);
}
}
tileRender = render;
NotifyPropertyChanged(nameof(TileRender));
UpdateDrawTileRender();
}
private void UpdateDrawTileRender() {
var palette = palettes[drawPalette];
drawTileRender = new CanvasPixelViewModel(8, 8, SpriteTool.Render(tiles[drawTile], palette, 0, 0)) { Transparent = palette[0], SpriteScale = 4 };
NotifyPropertiesChanged(nameof(DrawTileRender));
}
#endregion
}
}