mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-16 01:07:45 -05:00
given an area of all the same block, replace all those blocks with blocks that match the surrounding non-same blocks based on what blocks should 'match'. Note that this is a single-pass algorithm right now, which means that it creates a bunch of bad blocks as it gets further from the edge and more conflicts are found. We may try to update this more in the future, or we may just go with something simpler like a maze generator using border blocks or 9-grid. But the results of this are tantalizing...
2434 lines
116 KiB
C#
2434 lines
116 KiB
C#
using HavenSoft.HexManiac.Core.Models;
|
|
using HavenSoft.HexManiac.Core.Models.Code;
|
|
using HavenSoft.HexManiac.Core.Models.Map;
|
|
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.Linq;
|
|
using static HavenSoft.HexManiac.Core.ViewModels.Map.MapSliderIcons;
|
|
|
|
namespace HavenSoft.HexManiac.Core.ViewModels.Map {
|
|
public record ImageLocation(double X, double Y); // ranges from (0,0) upper-left to (1,1) lower-right
|
|
|
|
public class BlockMapViewModel : ViewModelCore, IPixelViewModel {
|
|
private readonly Format format;
|
|
private readonly IFileSystem fileSystem;
|
|
private readonly MapTutorialsViewModel tutorials;
|
|
private readonly IEditableViewPort viewPort;
|
|
private readonly IDataModel model;
|
|
private readonly Func<ModelDelta> tokenFactory;
|
|
private readonly int group, map;
|
|
|
|
private int PrimaryTiles { get; }
|
|
private int PrimaryBlocks { get; }
|
|
private int TotalBlocks => 1024;
|
|
private int PrimaryPalettes { get; } // 7
|
|
|
|
private int zIndex;
|
|
public int ZIndex { get => zIndex; set => Set(ref zIndex, value); }
|
|
|
|
public event EventHandler RequestClearMapCaches;
|
|
|
|
public IEditableViewPort ViewPort => viewPort;
|
|
|
|
#region SelectedEvent
|
|
|
|
private IEventViewModel selectedEvent;
|
|
public IEventViewModel SelectedEvent {
|
|
get => selectedEvent;
|
|
set {
|
|
var oldValue = selectedEvent;
|
|
selectedEvent = value;
|
|
NotifyPropertyChanged();
|
|
HandleSelectedEventChanged(oldValue);
|
|
}
|
|
}
|
|
|
|
public ObservableCollection<EventSelector> EventSelectors { get; } = new();
|
|
|
|
private void HandleSelectedEventChanged(IEventViewModel old) {
|
|
if (old == selectedEvent) return;
|
|
if (old != null) {
|
|
old.EventVisualUpdated -= RefreshFromEventChange;
|
|
old.CycleEvent -= CycleActiveEvent;
|
|
}
|
|
if (selectedEvent != null) {
|
|
selectedEvent.EventVisualUpdated += RefreshFromEventChange;
|
|
selectedEvent.CycleEvent += CycleActiveEvent;
|
|
}
|
|
RedrawEvents();
|
|
|
|
EventSelectors.Clear();
|
|
if (selectedEvent != null) {
|
|
var parts = selectedEvent.EventIndex.Split("/");
|
|
var index = int.Parse(parts[0]) - 1;
|
|
var count = int.Parse(parts[1]);
|
|
for (int i = 0; i < count; i++) {
|
|
EventSelector selector = new() { IsSelected = i == index, Index = i };
|
|
selector.Bind(nameof(selector.IsSelected), (sender, e) => {
|
|
var events = GetEvents().Where(ev => ev.GetType() == selectedEvent.GetType()).ToList();
|
|
SelectedEvent = events[sender.Index];
|
|
});
|
|
EventSelectors.Add(selector);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void RefreshFromEventChange(object sender, EventArgs e) => RedrawEvents();
|
|
|
|
private void CycleActiveEvent(object sender, EventCycleDirection direction) {
|
|
// organize events into categories
|
|
var events = GetEvents();
|
|
var categories = new List<List<IEventViewModel>> { new(), new(), new(), new(), new() };
|
|
int selectionIndex = -1, selectedCategory = -1;
|
|
for (int i = 0; i < events.Count; i++) {
|
|
int currentCategory =
|
|
events[i] is ObjectEventViewModel ? 0 :
|
|
events[i] is WarpEventViewModel ? 1 :
|
|
events[i] is ScriptEventViewModel ? 2 :
|
|
events[i] is SignpostEventViewModel ? 3 :
|
|
events[i] is FlyEventViewModel ? 4 :
|
|
-1;
|
|
categories[currentCategory].Add(events[i]);
|
|
|
|
if (events[i].Equals(selectedEvent)) {
|
|
selectionIndex = categories[currentCategory].Count - 1;
|
|
selectedCategory = currentCategory;
|
|
};
|
|
}
|
|
|
|
// remove unused categories
|
|
for (int i = 0; i < categories.Count; i++) {
|
|
if (categories[i].Count != 0) continue;
|
|
categories.RemoveAt(i);
|
|
if (selectedCategory > i) selectedCategory--;
|
|
i--;
|
|
}
|
|
|
|
// cycle
|
|
if (direction == EventCycleDirection.PreviousCategory) {
|
|
selectedCategory += categories.Count - 1;
|
|
selectionIndex = 0;
|
|
} else if (direction == EventCycleDirection.NextCategory) {
|
|
selectedCategory += 1;
|
|
selectionIndex = 0;
|
|
} else if (direction == EventCycleDirection.PreviousEvent) {
|
|
selectionIndex += categories[selectedCategory].Count - 1;
|
|
} else if (direction == EventCycleDirection.NextEvent) {
|
|
selectionIndex += 1;
|
|
} else if (direction == EventCycleDirection.None) {
|
|
// we just wanted to regenerate the event object
|
|
} else {
|
|
throw new NotImplementedException();
|
|
}
|
|
if (selectedCategory < 0) {
|
|
SelectedEvent = null;
|
|
return;
|
|
}
|
|
selectedCategory %= categories.Count;
|
|
selectionIndex %= categories[selectedCategory].Count;
|
|
|
|
// update selection
|
|
SelectedEvent = categories[selectedCategory][selectionIndex];
|
|
tutorials.Complete(Tutorial.EventButtons_CycleEvent);
|
|
}
|
|
|
|
#endregion
|
|
|
|
private static int MapSizeLimit => 0x2800; // (x+15)*(y+14) must be less that 0x2800 (5*2048). This can lead to limits like 113x66 or 497x6
|
|
public static bool IsMapWithinSizeLimit(int width, int height) => (width / 16 + 15) * (height / 16 + 14) <= MapSizeLimit;
|
|
|
|
public int MapID => group * 1000 + map;
|
|
|
|
private MapHeaderViewModel header;
|
|
public MapHeaderViewModel Header {
|
|
get {
|
|
if (header == null) {
|
|
header = new MapHeaderViewModel(GetMapModel(), format, tokenFactory);
|
|
header.Bind(nameof(Header.PrimaryIndex), (sender, e) => ClearCaches());
|
|
header.Bind(nameof(Header.SecondaryIndex), (sender, e) => ClearCaches());
|
|
}
|
|
return header;
|
|
}
|
|
}
|
|
|
|
public bool IsValidMap => GetMapModel() != null;
|
|
|
|
#region IPixelViewModel
|
|
|
|
private short transparent;
|
|
public short Transparent { get => transparent; private set => Set(ref transparent, value); }
|
|
|
|
private int pixelWidth, pixelHeight;
|
|
public int PixelWidth { get => pixelWidth; private set => Set(ref pixelWidth, value); }
|
|
public int PixelHeight { get => pixelHeight; private set => Set(ref pixelHeight, value); }
|
|
|
|
private readonly object pixelWriteLock = new();
|
|
private short[] pixelData; // picture of the map
|
|
public short[] PixelData {
|
|
get {
|
|
lock (pixelWriteLock) {
|
|
if (pixelData == null) FillMapPixelData();
|
|
return pixelData;
|
|
}
|
|
}
|
|
}
|
|
|
|
private double spriteScale = 1;
|
|
public double SpriteScale {
|
|
get => spriteScale;
|
|
set => Set(ref spriteScale, value, old => UpdateEdgesFromScale(old, old * pixelWidth / 2, old * pixelHeight / 2));
|
|
}
|
|
|
|
private void UpdateEdgesFromScale(double old, double centerX, double centerY) {
|
|
LeftEdge += (int)(centerX * (1 - SpriteScale / old));
|
|
TopEdge += (int)(centerY * (1 - SpriteScale / old));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region IsSelected
|
|
|
|
// TODO why is the selection rect not showing up until the first move interaction?
|
|
|
|
private bool isSelected;
|
|
public bool IsSelected {
|
|
get => isSelected;
|
|
set => Set(ref isSelected, value, old => ClearPixelCache());
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Position
|
|
|
|
private int topEdge, leftEdge;
|
|
public int TopEdge { get => topEdge; set => Set(ref topEdge, value); }
|
|
public int LeftEdge { get => leftEdge; set => Set(ref leftEdge, value); }
|
|
|
|
private int BottomEdge => topEdge + (int)(PixelHeight * SpriteScale);
|
|
private int RightEdge => leftEdge + (int)(PixelWidth * SpriteScale);
|
|
|
|
private ImageLocation hoverPoint = new(0, 0);
|
|
public ImageLocation HoverPoint {
|
|
get => hoverPoint;
|
|
set {
|
|
hoverPoint = value;
|
|
NotifyPropertyChanged();
|
|
}
|
|
}
|
|
|
|
public double WidthRatio => 80.0 / PixelWidth / Math.Min(1, SpriteScale);
|
|
public double HeightRatio => 80.0 / PixelHeight / Math.Min(1, SpriteScale);
|
|
private bool showBeneath;
|
|
public bool ShowBeneath { get => showBeneath; set => Set(ref showBeneath, value, old => {
|
|
NotifyPropertiesChanged(nameof(WidthRatio), nameof(HeightRatio));
|
|
tutorials.Complete(Tutorial.SpaceBar_ShowBeneath);
|
|
} ); }
|
|
|
|
#endregion
|
|
|
|
#region Visual Blocks
|
|
|
|
private IPixelViewModel blockPixels; // all the available blocks together in one big image
|
|
public IPixelViewModel BlockPixels {
|
|
get {
|
|
if (blockPixels == null) FillBlockPixelData();
|
|
return blockPixels;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region CollisionHighlight
|
|
|
|
private int collisionHighlight = -1;
|
|
public int CollisionHighlight {
|
|
get => collisionHighlight;
|
|
set {
|
|
Set(ref collisionHighlight, value, old => ClearPixelCache());
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Cache
|
|
|
|
private short[][] palettes;
|
|
private int[][,] tiles;
|
|
private byte[][] blocks;
|
|
private byte[][] blockAttributes;
|
|
private readonly List<IPixelViewModel> blockRenders = new(); // one image per block
|
|
private IReadOnlyList<IEventViewModel> eventRenders;
|
|
public IReadOnlyList<IPixelViewModel> BlockRenders {
|
|
get {
|
|
lock (blockRenders) {
|
|
if (blockRenders.Count == 0) RefreshBlockRenderCache();
|
|
}
|
|
return blockRenders;
|
|
}
|
|
}
|
|
|
|
private void ClearPixelCache() {
|
|
pixelData = null;
|
|
NotifyPropertyChanged(nameof(PixelData));
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Borders
|
|
|
|
private bool includeBorders = true;
|
|
public bool IncludeBorders {
|
|
get => includeBorders;
|
|
set => Set(ref includeBorders, value, IncludeBordersChanged);
|
|
}
|
|
private void IncludeBordersChanged(bool oldValue) {
|
|
var width = PixelWidth;
|
|
var height = PixelHeight;
|
|
RefreshMapSize();
|
|
LeftEdge -= (PixelWidth - width) / 2;
|
|
TopEdge -= (PixelHeight - height) / 2;
|
|
}
|
|
|
|
private IPixelViewModel borderBlock;
|
|
public IPixelViewModel BorderBlock {
|
|
get {
|
|
if (borderBlock == null) RefreshBorderRender();
|
|
return borderBlock;
|
|
}
|
|
set {
|
|
borderBlock = value;
|
|
NotifyPropertyChanged();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Name
|
|
|
|
public string FullName => MapIDToText(model, MapID);
|
|
public string Name => $"({group}-{map})";
|
|
|
|
private ObservableCollection<string> availableNames;
|
|
public ObservableCollection<string> AvailableNames {
|
|
get {
|
|
if (availableNames != null) return availableNames;
|
|
availableNames = new();
|
|
foreach (var name in viewPort.Model.GetOptions(HardcodeTablesModel.MapNameTable)) {
|
|
availableNames.Add(SanitizeName(name.Trim('"')));
|
|
}
|
|
return availableNames;
|
|
}
|
|
}
|
|
|
|
public int SelectedNameIndex {
|
|
get {
|
|
var offset = model.IsFRLG() ? 0x58 : 0;
|
|
var banks = model.GetTableModel(HardcodeTablesModel.MapBankTable);
|
|
var maps = banks[group].GetSubTable("maps");
|
|
var map = maps[this.map];
|
|
if (map == null) return -1;
|
|
var subTable = map.GetSubTable("map");
|
|
if (subTable == null) return -1;
|
|
var self = subTable[0];
|
|
if (!self.HasField("regionSectionID")) return -1;
|
|
return self.GetValue("regionSectionID") - offset;
|
|
}
|
|
set {
|
|
var offset = model.IsFRLG() ? 0x58 : 0;
|
|
var banks = model.GetTableModel(HardcodeTablesModel.MapBankTable, tokenFactory);
|
|
var maps = banks[group].GetSubTable("maps");
|
|
var mapTable = maps[map];
|
|
var subTable = mapTable.GetSubTable("map");
|
|
if (subTable == null) return;
|
|
var mapElement = subTable[0];
|
|
var self = maps[map].GetSubTable("map")[0];
|
|
if (!self.HasField("regionSectionID")) return;
|
|
self.SetValue("regionSectionID", value + offset);
|
|
NotifyPropertyChanged(nameof(FullName));
|
|
}
|
|
}
|
|
|
|
public static string SanitizeName(string name) {
|
|
return name.Replace("\\CC0000", " ");
|
|
}
|
|
|
|
#endregion
|
|
|
|
public event EventHandler NeighborsChanged;
|
|
public event EventHandler AutoscrollTiles;
|
|
public event EventHandler HideSidePanels;
|
|
public event EventHandler<ChangeMapEventArgs> RequestChangeMap;
|
|
|
|
private BlockEditor blockEditor;
|
|
public BlockEditor BlockEditor {
|
|
get {
|
|
if (blockEditor == null) {
|
|
var layout = GetLayout();
|
|
if (layout == null) return null;
|
|
var blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1"));
|
|
var blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2"));
|
|
if (palettes == null) RefreshPaletteCache(layout, blockModel1, blockModel2);
|
|
if (tiles == null) RefreshTileCache(layout, blockModel1, blockModel2);
|
|
if (blocks == null) RefreshBlockCache(layout, blockModel1, blockModel2);
|
|
if (blockAttributes == null) RefreshBlockAttributeCache(layout, blockModel1, blockModel2);
|
|
blockEditor = new BlockEditor(viewPort.ChangeHistory, model, tutorials, palettes, tiles, blocks, blockAttributes);
|
|
blockEditor.BlocksChanged += HandleBlocksChanged;
|
|
blockEditor.BlockAttributesChanged += HandleBlockAttributesChanged;
|
|
blockEditor.AutoscrollTiles += HandleAutoscrollTiles;
|
|
BlockEditor.SendMessage += (sender, e) => viewPort.RaiseMessage(e);
|
|
blockEditor.Bind(nameof(blockEditor.ShowTiles), (editor, args) => BorderEditor.ShowBorderPanel &= !editor.ShowTiles);
|
|
blockEditor.Bind(nameof(blockEditor.BlockIndex), (editor, args) => lastDrawX = lastDrawY = -1);
|
|
}
|
|
return blockEditor;
|
|
}
|
|
}
|
|
|
|
private BorderEditor borderEditor;
|
|
public BorderEditor BorderEditor {
|
|
get {
|
|
if (borderEditor == null) {
|
|
borderEditor = new BorderEditor(this, tutorials);
|
|
borderEditor.BorderChanged += HandleBorderChanged;
|
|
borderEditor.Bind(nameof(borderEditor.ShowBorderPanel), (editor, args) => {
|
|
if (BlockEditor == null) return;
|
|
BlockEditor.ShowTiles &= !editor.ShowBorderPanel;
|
|
HideSidePanels.Raise(this);
|
|
});
|
|
}
|
|
return borderEditor;
|
|
}
|
|
}
|
|
|
|
private MapRepointer mapRepointer;
|
|
public MapRepointer MapRepointer => mapRepointer;
|
|
|
|
private MapScriptCollection mapScriptCollection;
|
|
public MapScriptCollection MapScriptCollection {
|
|
get {
|
|
if (mapScriptCollection.Unloaded) {
|
|
var map = GetMapModel();
|
|
mapScriptCollection.Load(map);
|
|
}
|
|
return mapScriptCollection;
|
|
}
|
|
}
|
|
|
|
private WildPokemonViewModel wildPokemon;
|
|
public WildPokemonViewModel WildPokemon {
|
|
get {
|
|
if (wildPokemon == null) wildPokemon = new WildPokemonViewModel(viewPort, tutorials, group, map);
|
|
return wildPokemon;
|
|
}
|
|
}
|
|
|
|
private SurfConnectionViewModel surfConnection;
|
|
public SurfConnectionViewModel SurfConnection {
|
|
get {
|
|
if (surfConnection == null) {
|
|
surfConnection = new SurfConnectionViewModel(viewPort, group, map);
|
|
surfConnection.RequestChangeMap += (sender, e) => RequestChangeMap.Raise(this, e);
|
|
surfConnection.ConnectNewMap += (sender, e) => ConnectNewMap(e);
|
|
surfConnection.ConnectExistingMap += (sender, e) => ConnectExistingMap(e);
|
|
surfConnection.RequestRemoveConnection += (sender, e) => {
|
|
var connections = AllMapsModel.Create(model, tokenFactory)[group][map].Connections;
|
|
var toRemove = connections.Count.Range().Where(i => connections[i].Direction.IsAny(MapDirection.Dive, MapDirection.Emerge)).ToList();
|
|
RemoveConnections(toRemove);
|
|
};
|
|
}
|
|
return surfConnection;
|
|
}
|
|
}
|
|
|
|
public BlockMapViewModel(IFileSystem fileSystem, MapTutorialsViewModel tutorials, IEditableViewPort viewPort, Format format, int group, int map) {
|
|
this.format = format;
|
|
this.fileSystem = fileSystem;
|
|
this.tutorials = tutorials;
|
|
this.viewPort = viewPort;
|
|
this.model = viewPort.Model;
|
|
this.tokenFactory = () => viewPort.ChangeHistory.CurrentChange;
|
|
(this.group, this.map) = (group, map);
|
|
Transparent = -1;
|
|
var mapModel = GetMapModel();
|
|
RefreshMapSize();
|
|
PrimaryTiles = PrimaryBlocks = model.IsFRLG() ? 640 : 512;
|
|
PrimaryPalettes = model.IsFRLG() ? 7 : 6;
|
|
|
|
(LeftEdge, TopEdge) = (-PixelWidth / 2, -PixelHeight / 2);
|
|
|
|
mapScriptCollection = new(viewPort);
|
|
mapScriptCollection.NewMapScriptsCreated += (sender, e) => GetMapModel().SetAddress("mapscripts", e.Address);
|
|
|
|
mapRepointer = new MapRepointer(format, fileSystem, viewPort, viewPort.ChangeHistory, MapID);
|
|
mapRepointer.ChangeMap += (sender, e) => RequestChangeMap.Raise(this, e);
|
|
mapRepointer.DataMoved += (sender, e) => {
|
|
ClearCaches();
|
|
if (e == null) return;
|
|
InformRepoint(e);
|
|
if (e.Type == "Layout") UpdateLayoutID();
|
|
};
|
|
}
|
|
|
|
private BerryInfo berryInfo;
|
|
public BerryInfo BerryInfo {
|
|
get {
|
|
if (berryInfo != null) return berryInfo;
|
|
return berryInfo = SetupBerryInfo();
|
|
}
|
|
set {
|
|
berryInfo = value;
|
|
NotifyPropertyChanged(nameof(BerryInfo));
|
|
}
|
|
}
|
|
|
|
private BerryInfo SetupBerryInfo() {
|
|
var collection = new ObservableCollection<string>();
|
|
var options = model.GetOptions(HardcodeTablesModel.BerryTableName);
|
|
if (options == null) options = 100.Range().Select(i => i.ToString()).ToList();
|
|
foreach (var option in options) collection.Add(option);
|
|
var spots = Flags.GetBerrySpots(model, ViewPort.Tools.CodeTool.ScriptParser);
|
|
return new(spots, collection);
|
|
}
|
|
|
|
public void InformRepoint(DataMovedEventArgs e) {
|
|
viewPort.RaiseMessage($"{e.Type} data was moved to {e.Address:X6}.");
|
|
}
|
|
|
|
public void InformCreate(DataMovedEventArgs e) {
|
|
viewPort.RaiseMessage($"{e.Type} data was created at {e.Address:X6}.");
|
|
}
|
|
|
|
public IReadOnlyList<BlockMapViewModel> GetNeighbors(MapDirection direction) {
|
|
var list = new List<BlockMapViewModel>();
|
|
var border = GetBorderThickness();
|
|
if (border == null) return list;
|
|
foreach (var connection in GetConnections()) {
|
|
if (connection.Direction != direction) continue;
|
|
var vm = GetNeighbor(connection, border);
|
|
list.Add(vm);
|
|
}
|
|
return list;
|
|
}
|
|
|
|
public bool UpdateFrom(BlockMapViewModel other) {
|
|
if (other == null) return false;
|
|
if (other == this) return true;
|
|
if (other.MapID == MapID) {
|
|
IncludeBorders = other.IncludeBorders;
|
|
SpriteScale = other.SpriteScale;
|
|
LeftEdge = other.LeftEdge;
|
|
TopEdge = other.TopEdge;
|
|
ZIndex = other.ZIndex;
|
|
IsSelected = other.IsSelected;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public void GotoData() {
|
|
var map = GetMapModel();
|
|
viewPort.Goto.Execute(map.Start);
|
|
}
|
|
|
|
public void ClearCaches() {
|
|
palettes = null;
|
|
tiles = null;
|
|
blocks = null;
|
|
lock (blockRenders) {
|
|
blockRenders.Clear();
|
|
}
|
|
blockPixels = null;
|
|
eventRenders = null;
|
|
borderBlock = null;
|
|
berryInfo = null;
|
|
WildPokemon.ClearCache();
|
|
RefreshMapSize();
|
|
if (blockEditor != null) {
|
|
blockEditor.BlocksChanged -= HandleBlocksChanged;
|
|
blockEditor.BlockAttributesChanged -= HandleBlockAttributesChanged;
|
|
BlockEditor.AutoscrollTiles -= HandleAutoscrollTiles;
|
|
var oldBlockEditor = blockEditor;
|
|
blockEditor = null;
|
|
if (BlockEditor != null && oldBlockEditor != null) {
|
|
BlockEditor.BlockIndex = oldBlockEditor.BlockIndex;
|
|
(BlockEditor.TileSelectionX, BlockEditor.TileSelectionY) = (oldBlockEditor.TileSelectionX, oldBlockEditor.TileSelectionY);
|
|
BlockEditor.PaletteSelection = oldBlockEditor.PaletteSelection;
|
|
BlockEditor.ShowTiles = oldBlockEditor.ShowTiles;
|
|
BlockEditor.LoadClipboard(oldBlockEditor);
|
|
}
|
|
if (oldBlockEditor != null) oldBlockEditor.ShowTiles = false;
|
|
NotifyPropertyChanged(nameof(BlockEditor));
|
|
}
|
|
if (borderEditor != null) {
|
|
var oldShowBorder = borderEditor.ShowBorderPanel;
|
|
borderEditor.BorderChanged -= HandleBorderChanged;
|
|
var oldBorderEditor = borderEditor;
|
|
borderEditor = null;
|
|
BorderEditor.ShowBorderPanel = oldShowBorder;
|
|
oldBorderEditor.ShowBorderPanel = false;
|
|
NotifyPropertyChanged(nameof(BorderEditor));
|
|
}
|
|
ClearPixelCache();
|
|
NotifyPropertiesChanged(nameof(BlockRenders), nameof(BlockPixels), nameof(BerryInfo));
|
|
if (SelectedEvent != null) CycleActiveEvent(default, EventCycleDirection.None);
|
|
}
|
|
|
|
public void RedrawEvents() {
|
|
eventRenders = null;
|
|
NotifyPropertyChanged(nameof(CanCreateFlyEvent));
|
|
ClearPixelCache();
|
|
}
|
|
|
|
public void Scale(double x, double y, bool enlarge) {
|
|
(lastDrawX, lastDrawY) = (-1, -1);
|
|
var old = spriteScale;
|
|
|
|
if (enlarge && spriteScale < 10) {
|
|
if (spriteScale < 1) spriteScale *= 2;
|
|
else spriteScale += 1;
|
|
} else if (!enlarge && spriteScale > .1) {
|
|
if (spriteScale > 1) spriteScale -= 1;
|
|
else spriteScale /= 2;
|
|
}
|
|
|
|
if (old != spriteScale) UpdateEdgesFromScale(old, x - leftEdge, y - topEdge);
|
|
NotifyPropertyChanged(nameof(SpriteScale));
|
|
}
|
|
|
|
// check for other warps on this same tile and see what primary/secondary blockset is expected for the new map.
|
|
// if no reasonable tileset is found, just use the current map's blocksets
|
|
public BlockMapViewModel CreateMapForWarp(WarpEventViewModel warp) {
|
|
// what bank should the new map go in?
|
|
var option = MapRepointer.GetMapBankForNewMap(
|
|
"Maps are organized into banks. The game doesn't care, so you can use the banks however you like."
|
|
+ Environment.NewLine +
|
|
"Which map bank do you want to use for the new map?");
|
|
if (option == -1) return null;
|
|
var token = tokenFactory();
|
|
MapModel thisMap = new(GetMapModel());
|
|
|
|
// give me this block
|
|
var blockIndex = thisMap.Blocks[warp.X, warp.Y].Tile;
|
|
|
|
// give me all maps that use this blockset
|
|
var borderBlockAddress = thisMap.Layout.BorderBlockAddress;
|
|
var primaryBlocksetAddress = thisMap.Layout.PrimaryBlockset.Start;
|
|
var secondaryBlocksetAddress = thisMap.Layout.SecondaryBlockset.Start;
|
|
|
|
var maps = new List<MapModel>();
|
|
foreach (var map in GetAllMaps()) {
|
|
if (map.Layout.PrimaryBlockset.Start != primaryBlocksetAddress && blockIndex < PrimaryBlocks) continue;
|
|
if (map.Layout.SecondaryBlockset.Start != secondaryBlocksetAddress && blockIndex >= PrimaryBlocks) continue;
|
|
maps.Add(map);
|
|
}
|
|
|
|
// give me all warps in those maps (except for this warp itself)
|
|
// give me all warps that are on this tile
|
|
var warps = new List<WarpEventModel>();
|
|
foreach (var map in maps) {
|
|
var layout = map.Layout;
|
|
foreach (var w in map.Events.Warps) {
|
|
if (w.Element.Start == warp.Element.Start) continue;
|
|
if (map.Blocks[w.X, w.Y].Tile != blockIndex) continue;
|
|
warps.Add(w);
|
|
}
|
|
}
|
|
|
|
// give me maps that those warp to
|
|
// give me all the primary/secondary blocksets for those maps
|
|
// give me all the borders for those maps
|
|
var prototypes = new Dictionary<LayoutPrototype, List<WarpEventModel>>();
|
|
foreach (var w in warps) {
|
|
var m = w.TargetMap;
|
|
if (m == null) continue;
|
|
var (primary, secondary, border) = (m.Layout.PrimaryBlockset, m.Layout.SecondaryBlockset, m.Layout.BorderBlockAddress);
|
|
if (primary == null || secondary == null || border == Pointer.NULL) continue;
|
|
var prototype = new LayoutPrototype(primary.Start, secondary.Start, border);
|
|
if (!prototypes.ContainsKey(prototype)) prototypes.Add(prototype, new());
|
|
prototypes[prototype].Add(w);
|
|
}
|
|
|
|
var orderedPrototypes = prototypes.Keys.ToList();
|
|
var initialBlockmap = new int[9, 9];
|
|
var warpIsBottomSquare = true;
|
|
|
|
if (orderedPrototypes.Count > 1) {
|
|
var initialBlockmaps = new List<int[,]>();
|
|
var warpIsBottomSquareForIndex = new List<bool>();
|
|
|
|
// for each prototype, create an image that represents what that map prototype would look like
|
|
var images = new List<VisualOption>();
|
|
foreach (var prototype in orderedPrototypes) {
|
|
var targets = prototypes[prototype];
|
|
var (render, blockmap, isBottomSquare) = RenderPrototype(targets);
|
|
initialBlockmaps.Add(blockmap);
|
|
warpIsBottomSquareForIndex.Add(isBottomSquare);
|
|
var targetMapName = MapIDToText(model, targets[0].Bank, targets[0].Map);
|
|
var targetLocation = targetMapName.Split('(')[0];
|
|
var targetName = '(' + targetMapName.Split('(')[1];
|
|
var visOption = new VisualOption { Index = orderedPrototypes.IndexOf(prototype), Option = $"Like {targetLocation}", ShortDescription = targetName, Visual = render };
|
|
images.Add(visOption);
|
|
}
|
|
|
|
// create one additional 'blank' prototype
|
|
initialBlockmaps.Add(new int[9, 9]);
|
|
var blank = new LayoutPrototype(primaryBlocksetAddress, secondaryBlocksetAddress, borderBlockAddress);
|
|
orderedPrototypes.Add(blank);
|
|
var blankRender = new CanvasPixelViewModel(9 * 16, 9 * 16) { SpriteScale = images[0].Visual.SpriteScale };
|
|
var blankVisOption = new VisualOption { Index = orderedPrototypes.Count - 1, Option = "Blank", ShortDescription = "Use Current Blocksets", Visual = blankRender };
|
|
warpIsBottomSquareForIndex.Add(true);
|
|
images.Add(blankVisOption);
|
|
|
|
var choice = fileSystem.ShowOptions("Create New Map", "Start from which template?", null, images.ToArray());
|
|
if (choice == -1) return null;
|
|
var chosenPrototype = orderedPrototypes[choice];
|
|
// use the most frequent primary/secondary blockset and border blocks
|
|
primaryBlocksetAddress = chosenPrototype.PrimaryBlockset;
|
|
secondaryBlocksetAddress = chosenPrototype.SecondaryBlockset;
|
|
borderBlockAddress = chosenPrototype.BorderBlock;
|
|
initialBlockmap = initialBlockmaps[choice];
|
|
warpIsBottomSquare = warpIsBottomSquareForIndex[choice];
|
|
} else if (orderedPrototypes.Count == 1) {
|
|
// no dialog, just go with this one
|
|
var chosenPrototype = orderedPrototypes[0];
|
|
primaryBlocksetAddress = chosenPrototype.PrimaryBlockset;
|
|
secondaryBlocksetAddress = chosenPrototype.SecondaryBlockset;
|
|
borderBlockAddress = chosenPrototype.BorderBlock;
|
|
var (render, blockmap, isBottomSquare) = RenderPrototype(prototypes.Values.Single());
|
|
initialBlockmap = blockmap;
|
|
warpIsBottomSquare = isBottomSquare;
|
|
}
|
|
|
|
// create a new 9x9 map
|
|
var newMap = CreateNewMap(token, option, 9, 9);
|
|
var newLayout = newMap.GetLayout();
|
|
newLayout.SetAddress(Format.BorderBlock, borderBlockAddress);
|
|
newLayout.SetAddress(Format.PrimaryBlockset, primaryBlocksetAddress);
|
|
newLayout.SetAddress(Format.SecondaryBlockset, secondaryBlocksetAddress);
|
|
var start = newLayout.GetAddress(Format.BlockMap);
|
|
for (int x = 0; x < 9; x++) {
|
|
for (int y = 0; y < 9; y++) {
|
|
model.WriteMultiByteValue(start + (y * 9 + x) * 2, 2, token, initialBlockmap[x, y]);
|
|
}
|
|
}
|
|
|
|
// place reverse warp at bottom heading back
|
|
(warp.Bank, warp.Map, warp.WarpID) = (newMap.group, newMap.map, 1);
|
|
var returnWarp = newMap.CreateWarpEvent(group, map);
|
|
returnWarp.WarpID = GetEvents().Where(e => e is WarpEventViewModel).Until(e => e.Equals(warp)).Count();
|
|
(returnWarp.X, returnWarp.Y) = (4, 8);
|
|
if (!warpIsBottomSquare) (returnWarp.X, returnWarp.Y) = (4, 7);
|
|
|
|
// repoint border block
|
|
newMap.MapRepointer.RepointBorderBlock.Execute();
|
|
|
|
return newMap;
|
|
}
|
|
|
|
// from the maps that use this blockmap/blockset/border,
|
|
// figure out appropriate wall/floor tiles to make a small prototype map
|
|
private (IPixelViewModel, int[,], bool) RenderPrototype(List<WarpEventModel> warps) {
|
|
// TODO I don't just want to return an image, I want to return the tiles/collisions to use as well
|
|
|
|
// find the edge tiles
|
|
var topTile = warps.Select(warp => warp.TargetMap.Layout).SelectMany(layout => (layout.Width - 2).Range(x => layout.BlockMap[x + 1, 0].Block)).ToHistogram().MostCommonKey();
|
|
var bottomTile = warps.Select(warp => warp.TargetMap.Layout).SelectMany(layout => (layout.Width - 2).Range(x => layout.BlockMap[x + 1, layout.Height - 1].Block)).ToHistogram().MostCommonKey();
|
|
var leftTile = warps.Select(warp => warp.TargetMap.Layout).SelectMany(layout => (layout.Height - 2).Range(y => layout.BlockMap[0, y + 1].Block)).ToHistogram().MostCommonKey();
|
|
var rightTile = warps.Select(warp => warp.TargetMap.Layout).SelectMany(layout => (layout.Height - 2).Range(y => layout.BlockMap[layout.Width - 1, y + 1].Block)).ToHistogram().MostCommonKey();
|
|
var topLeftTile = warps.Select(warp => warp.TargetMap.Layout).Select(layout => layout.BlockMap[0, 0].Block).ToHistogram().MostCommonKey();
|
|
var topRightTile = warps.Select(warp => warp.TargetMap.Layout).Select(layout => layout.BlockMap[layout.Width - 1, 0].Block).ToHistogram().MostCommonKey();
|
|
var bottomLeftTile = warps.Select(warp => warp.TargetMap.Layout).Select(layout => layout.BlockMap[0, layout.Height - 1].Block).ToHistogram().MostCommonKey();
|
|
var bottomRightTile = warps.Select(warp => warp.TargetMap.Layout).Select(layout => layout.BlockMap[layout.Width - 1, layout.Height - 1].Block).ToHistogram().MostCommonKey();
|
|
|
|
// find the floor tile
|
|
var centerTiles = new List<int>();
|
|
foreach (var warp in warps) {
|
|
var layout = warp.TargetMap.Layout;
|
|
for (int x = 1; x < layout.Width - 1; x++) {
|
|
for (int y = 1; y < layout.Height - 1; y++) {
|
|
centerTiles.Add(layout.BlockMap[x, y].Block);
|
|
}
|
|
}
|
|
}
|
|
var floorTile = centerTiles.ToHistogram().MostCommonKey();
|
|
|
|
// build the map image data
|
|
var blockMap = new int[9, 9];
|
|
for (int i = 1; i < 8; i++) {
|
|
blockMap[0, i] = leftTile;
|
|
blockMap[8, i] = rightTile;
|
|
blockMap[i, 0] = topTile;
|
|
blockMap[i, 8] = bottomTile;
|
|
for (int j = 1; j < 8; j++) blockMap[i, j] = floorTile;
|
|
}
|
|
(blockMap[0, 0], blockMap[8, 0], blockMap[0, 8], blockMap[8, 8]) = (topLeftTile, topRightTile, bottomLeftTile, bottomRightTile);
|
|
|
|
// find the door tile
|
|
var map0 = warps[0].TargetMap;
|
|
var matchingWarp0 = map0.Events.Warps[warps[0].WarpID];
|
|
var warpIsAgainstWall = matchingWarp0.Y == map0.Layout.Height - 1;
|
|
if (warpIsAgainstWall) {
|
|
blockMap[3, 7] = map0.Blocks[matchingWarp0.X - 1, matchingWarp0.Y - 1].Tile;
|
|
blockMap[4, 7] = map0.Blocks[matchingWarp0.X, matchingWarp0.Y - 1].Tile;
|
|
blockMap[5, 7] = map0.Blocks[matchingWarp0.X + 1, matchingWarp0.Y - 1].Tile;
|
|
|
|
blockMap[3, 8] = map0.Blocks[matchingWarp0.X - 1, matchingWarp0.Y].Tile;
|
|
blockMap[4, 8] = map0.Blocks[matchingWarp0.X, matchingWarp0.Y].Tile;
|
|
blockMap[5, 8] = map0.Blocks[matchingWarp0.X + 1, matchingWarp0.Y].Tile;
|
|
} else {
|
|
blockMap[3, 7] = map0.Blocks[matchingWarp0.X - 1, matchingWarp0.Y].Tile;
|
|
blockMap[4, 7] = map0.Blocks[matchingWarp0.X, matchingWarp0.Y].Tile;
|
|
blockMap[5, 7] = map0.Blocks[matchingWarp0.X + 1, matchingWarp0.Y].Tile;
|
|
|
|
blockMap[3, 8] = map0.Blocks[matchingWarp0.X - 1, matchingWarp0.Y + 1].Tile;
|
|
blockMap[4, 8] = map0.Blocks[matchingWarp0.X, matchingWarp0.Y + 1].Tile;
|
|
blockMap[5, 8] = map0.Blocks[matchingWarp0.X + 1, matchingWarp0.Y + 1].Tile;
|
|
}
|
|
|
|
// draw the map
|
|
var viewModel = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, warps[0].Bank, warps[0].Map) { BerryInfo = BerryInfo };
|
|
var canvas = new CanvasPixelViewModel(9 * 16, 9 * 16);
|
|
for (int y = 0; y < 9; y++) {
|
|
for (int x = 0; x < 9; x++) {
|
|
canvas.Draw(viewModel.BlockRenders[blockMap[x, y] & 0x3FF], x * 16, y * 16);
|
|
}
|
|
}
|
|
|
|
canvas.SpriteScale = .5;
|
|
return (canvas, blockMap, warpIsAgainstWall);
|
|
}
|
|
|
|
#region Draw / Paint
|
|
|
|
/// <summary>
|
|
/// Gets the block index and collision index.
|
|
/// </summary>
|
|
public (int blockIndex, int collisionIndex) GetBlock(double x, double y) {
|
|
(lastDrawX, lastDrawY) = (-1, -1);
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
if (xx < 0 || yy < 0 || xx > width || yy > height) return (-1, -1);
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
var modelAddress = start + (yy * width + xx) * 2;
|
|
var data = model.ReadMultiByteValue(modelAddress, 2);
|
|
return (data & 0x3FF, data >> 10);
|
|
}
|
|
|
|
private int lastDrawVal, lastDrawX, lastDrawY;
|
|
|
|
/// <summary>
|
|
/// If collisionIndex is not valid, it's ignored.
|
|
/// If blockIndex is not valid, it's ignored.
|
|
/// </summary>
|
|
public void DrawBlock(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) {
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
|
|
var layout = GetLayout();
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
DrawBlock(token, blockIndex, collisionIndex, xx, yy);
|
|
}
|
|
|
|
public void DrawBlock(ModelDelta token, int blockIndex, int collisionIndex, int xx, int yy) {
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
|
|
xx = xx.LimitToRange(0, width - 1);
|
|
yy = yy.LimitToRange(0, height - 1);
|
|
if (lastDrawX == xx && lastDrawY == yy) return;
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
var modelAddress = start + (yy * width + xx) * 2;
|
|
var data = model.ReadMultiByteValue(modelAddress, 2);
|
|
var high = data >> 10;
|
|
var low = data & 0x3FF;
|
|
if (blockIndex >= 0 && blockIndex < blockRenders.Count) low = blockIndex;
|
|
if (collisionIndex >= 0 && collisionIndex < 0x3F) high = collisionIndex;
|
|
lastDrawVal = model.ReadMultiByteValue(modelAddress, 2);
|
|
(lastDrawX, lastDrawY) = (xx, yy);
|
|
model.WriteMultiByteValue(modelAddress, 2, token, (high << 10) | low);
|
|
|
|
var canvas = new CanvasPixelViewModel(pixelWidth, pixelHeight, PixelData);
|
|
bool updateBlock = blockIndex >= 0 && blockIndex < blockRenders.Count;
|
|
bool updateHighlight = collisionIndex == collisionHighlight && collisionHighlight != -1;
|
|
(xx, yy) = ((xx + border.West) * 16, (yy + border.North) * 16);
|
|
if (updateBlock) canvas.Draw(blockRenders[blockIndex], xx, yy);
|
|
if (updateHighlight && xx < pixelWidth && yy < pixelHeight) HighlightCollision(PixelData, xx, yy);
|
|
if (updateBlock || updateHighlight) NotifyPropertyChanged(nameof(PixelData));
|
|
tutorials.Complete(Tutorial.LeftClickMap_DrawBlock);
|
|
}
|
|
|
|
public void Draw9Grid(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) {
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
|
|
var layout = GetLayout();
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
Draw9Grid(token, blockIndex, collisionIndex, xx, yy);
|
|
}
|
|
|
|
public void Draw9Grid(ModelDelta token, int blockIndex, int collisionIndex, int xx, int yy) {
|
|
var blockHeight = (int)Math.Ceiling((double)blockRenders.Count / BlocksPerRow);
|
|
|
|
// find all neighbor blocks with blockIndex +/- 1 +/- BlocksPerRow
|
|
// arrange these blocks into a grid so we can use them
|
|
var grid = new int[3, 3]; // TODO fill the grid
|
|
var targets = new List<int> { blockIndex };
|
|
if (blockIndex >= BlocksPerRow) {
|
|
if (blockIndex % BlocksPerRow > 0) targets.Add(blockIndex - 1 - BlocksPerRow);
|
|
grid[0, 0] = blockIndex % BlocksPerRow > 0 ? blockIndex - 1 - BlocksPerRow : blockIndex - BlocksPerRow;
|
|
targets.Add(blockIndex - BlocksPerRow);
|
|
grid[0, 1] = blockIndex - BlocksPerRow;
|
|
if (blockIndex % BlocksPerRow != BlocksPerRow - 1) targets.Add(blockIndex + 1 - BlocksPerRow);
|
|
grid[0, 2] = blockIndex % BlocksPerRow != BlocksPerRow - 1 ? blockIndex + 1 - BlocksPerRow : blockIndex - BlocksPerRow;
|
|
} else {
|
|
grid[0, 0] = blockIndex % BlocksPerRow > 0 ? blockIndex - 1 : blockIndex;
|
|
grid[0, 1] = blockIndex;
|
|
grid[0, 2] = blockIndex % BlocksPerRow != BlocksPerRow - 1 ? blockIndex + 1 : blockIndex;
|
|
}
|
|
if (blockIndex % BlocksPerRow > 0) targets.Add(blockIndex - 1);
|
|
grid[1, 0] = blockIndex % BlocksPerRow > 0 ? blockIndex - 1 : blockIndex;
|
|
grid[1, 1] = blockIndex;
|
|
if (blockIndex % BlocksPerRow != BlocksPerRow - 1) targets.Add(blockIndex + 1);
|
|
grid[1, 2] = blockIndex % BlocksPerRow != BlocksPerRow - 1 ? blockIndex + 1 : blockIndex;
|
|
if (blockIndex / BlocksPerRow < blockHeight - 1) {
|
|
if (blockIndex % BlocksPerRow > 0) targets.Add(blockIndex - 1 + BlocksPerRow);
|
|
grid[2, 0] = blockIndex % BlocksPerRow > 0 ? blockIndex - 1 + BlocksPerRow : blockIndex + BlocksPerRow;
|
|
targets.Add(blockIndex + BlocksPerRow);
|
|
grid[2, 1] = blockIndex + BlocksPerRow;
|
|
if (blockIndex % BlocksPerRow != BlocksPerRow - 1) targets.Add(blockIndex + 1 + BlocksPerRow);
|
|
grid[2, 2] = blockIndex % BlocksPerRow > 0 ? blockIndex + 1 + BlocksPerRow : blockIndex + BlocksPerRow;
|
|
} else {
|
|
grid[2, 0] = blockIndex % BlocksPerRow > 0 ? blockIndex - 1 : blockIndex;
|
|
grid[2, 1] = blockIndex;
|
|
grid[2, 2] = blockIndex % BlocksPerRow != BlocksPerRow - 1 ? blockIndex + 1 : blockIndex;
|
|
}
|
|
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
int get(Point p) => p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height ? -1 : model.ReadMultiByteValue(start + (p.Y * width + p.X) * 2, 2) & 0x3FF;
|
|
void set(Point p, int block) => model.WriteMultiByteValue(start + (p.Y * width + p.X) * 2, 2, token, (block & 0x3FF) + (collisionIndex << 10));
|
|
|
|
// change all connected blocks based on the grid
|
|
var todo = new List<Point> { new(xx, yy), new(xx - 1, yy), new(xx + 1, yy), new(xx, yy - 1), new(xx, yy + 1) };
|
|
lock (pixelWriteLock) {
|
|
set(todo[0], blockIndex);
|
|
foreach (var cell in todo) {
|
|
var cellValue = get(cell);
|
|
if (!targets.Contains(cellValue)) continue;
|
|
|
|
var north = targets.Contains(get(new(cell.X, cell.Y - 1)));
|
|
var south = targets.Contains(get(new(cell.X, cell.Y + 1)));
|
|
var west = targets.Contains(get(new(cell.X - 1, cell.Y)));
|
|
var east = targets.Contains(get(new(cell.X + 1, cell.Y)));
|
|
var aggregate = (north ? "N" : " ") + (east ? "E" : " ") + (south ? "S" : " ") + (west ? "W" : " ");
|
|
|
|
var block = aggregate switch {
|
|
" ES " => grid[0, 0],
|
|
" ESW" => grid[0, 1],
|
|
" SW" => grid[0, 2],
|
|
"NES " => grid[1, 0],
|
|
"NESW" => grid[1, 1],
|
|
"N SW" => grid[1, 2],
|
|
"NE " => grid[2, 0],
|
|
"NE W" => grid[2, 1],
|
|
"N W" => grid[2, 2],
|
|
_ => grid[1, 1],
|
|
};
|
|
set(cell, block);
|
|
}
|
|
}
|
|
|
|
ClearPixelCache();
|
|
}
|
|
|
|
public void DrawBlocks(ModelDelta token, int[,] tiles, Point source, Point destination) {
|
|
while (Math.Abs(destination.X - source.X) % tiles.GetLength(0) != 0) destination -= new Point(1, 0);
|
|
while (Math.Abs(destination.Y - source.Y) % tiles.GetLength(1) != 0) destination -= new Point(0, 1);
|
|
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var start = layout.GetAddress("blockmap");
|
|
int changeCount = 0;
|
|
lock (pixelWriteLock) {
|
|
for (int x = 0; x < tiles.GetLength(0); x++) {
|
|
for (int y = 0; y < tiles.GetLength(1); y++) {
|
|
if (destination.X + x < 0 || destination.Y + y < 0 || destination.X + x >= width || destination.Y + y >= height) continue;
|
|
var address = start + ((destination.Y + y) * width + destination.X + x) * 2;
|
|
if (model.ReadMultiByteValue(address, 2) != tiles[x, y]) {
|
|
model.WriteMultiByteValue(address, 2, token, tiles[x, y]);
|
|
changeCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (changeCount > 0) ClearPixelCache();
|
|
}
|
|
|
|
public void RepeatBlock(Func<ModelDelta> futureToken, int block, int collision, int x, int y, int w, int h, bool refreshScreen) {
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var start = layout.GetAddress("blockmap");
|
|
int changeCount = 0;
|
|
lock (pixelWriteLock) {
|
|
for (int xx = 0; xx < w; xx++) {
|
|
for (int yy = 0; yy < h; yy++) {
|
|
if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue;
|
|
var address = start + ((yy + y) * width + xx + x) * 2;
|
|
// var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)];
|
|
var blockValue = model.ReadMultiByteValue(address, 2);
|
|
lastDrawVal = blockValue;
|
|
if (block >= 0) blockValue = (blockValue & 0xFC00) + block;
|
|
if (collision >= 0) blockValue = (blockValue & 0x3FF) + (collision << 10);
|
|
if (blockValue != lastDrawVal) {
|
|
model.WriteMultiByteValue(address, 2, futureToken(), blockValue);
|
|
changeCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (changeCount > 0 && refreshScreen) ClearPixelCache();
|
|
}
|
|
|
|
public void RepeatBlocks(Func<ModelDelta> futureToken, int[,] blockValues, int x, int y, int w, int h, bool refreshScreen) {
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var start = layout.GetAddress("blockmap");
|
|
int changeCount = 0;
|
|
lock (pixelWriteLock) {
|
|
for (int xx = 0; xx < w; xx++) {
|
|
for (int yy = 0; yy < h; yy++) {
|
|
if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue;
|
|
var address = start + ((yy + y) * width + xx + x) * 2;
|
|
var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)];
|
|
if (model.ReadMultiByteValue(address, 2) != block) {
|
|
model.WriteMultiByteValue(address, 2, futureToken(), block);
|
|
changeCount++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (changeCount > 0 && refreshScreen) ClearPixelCache();
|
|
}
|
|
|
|
public int[,] ReadRectangle(int x, int y, int w, int h) {
|
|
var results = new int[w, h];
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var start = layout.GetAddress("blockmap");
|
|
for (int xx = 0; xx < w; xx++) {
|
|
for (int yy = 0; yy < h; yy++) {
|
|
if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue;
|
|
var address = start + ((yy + y) * width + xx + x) * 2;
|
|
results[xx, yy] = model.ReadMultiByteValue(address, 2);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
public void PaintBlock(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) {
|
|
if (blockIndex == -1) return;
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
if (xx < 0 || yy < 0 || xx > width || yy > height) return;
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
var size = new Point(width, height);
|
|
if (collisionIndex < 0) collisionIndex = lastDrawVal >> 10;
|
|
var (before, after) = (lastDrawVal, (collisionIndex << 10) | blockIndex);
|
|
lock (pixelWriteLock) {
|
|
PaintBlock(token, new(xx - 1, yy), size, start, before, after);
|
|
PaintBlock(token, new(xx + 1, yy), size, start, before, after);
|
|
PaintBlock(token, new(xx, yy - 1), size, start, before, after);
|
|
PaintBlock(token, new(xx, yy + 1), size, start, before, after);
|
|
}
|
|
ClearPixelCache();
|
|
}
|
|
|
|
private void PaintBlock(ModelDelta token, Point p, Point size, int start, int before, int after) {
|
|
if (before == after) return;
|
|
if (p.X < 0 || p.Y < 0 || p.X >= size.X || p.Y >= size.Y) return;
|
|
var address = start + (p.Y * size.X + p.X) * 2;
|
|
if (model.ReadMultiByteValue(address, 2) != before) return;
|
|
model.WriteMultiByteValue(address, 2, token, after);
|
|
PaintBlock(token, p + new Point(-1, 0), size, start, before, after);
|
|
PaintBlock(token, p + new Point(1, 0), size, start, before, after);
|
|
PaintBlock(token, p + new Point(0, -1), size, start, before, after);
|
|
PaintBlock(token, p + new Point(0, 1), size, start, before, after);
|
|
}
|
|
|
|
public void PaintBlockBag(ModelDelta token, List<int> blockIndexes, int collisionIndex, double x, double y) {
|
|
if (blockIndexes.Count < 1) return;
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
if (xx < 0 || yy < 0 || xx > width || yy > height) return;
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
var complete = new HashSet<Point> { new(xx, yy) };
|
|
var check = new List<Point> { new(xx - 1, yy), new(xx + 1, yy), new(xx, yy - 1), new(xx, yy + 1) };
|
|
var targets = blockIndexes.Select(bi => (collisionIndex << 10) | bi).ToList();
|
|
var rnd = new Random();
|
|
lock (pixelWriteLock) {
|
|
while (check.Count > 0) {
|
|
var p = check[check.Count - 1];
|
|
check.RemoveAt(check.Count - 1);
|
|
if (p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height) continue;
|
|
if (complete.Contains(p)) continue;
|
|
complete.Add(p);
|
|
|
|
var address = start + (p.Y * width + p.X) * 2;
|
|
if (model.ReadMultiByteValue(address, 2) != lastDrawVal) continue;
|
|
model.WriteMultiByteValue(address, 2, token, rnd.From(targets));
|
|
|
|
check.AddRange(new Point[] { new(p.X - 1, p.Y), new(p.X + 1, p.Y), new(p.X, p.Y - 1), new(p.X, p.Y + 1) });
|
|
}
|
|
}
|
|
ClearPixelCache();
|
|
}
|
|
|
|
public void PaintWaveFunction(ModelDelta token, double x, double y, Func<int, int, int> wave) {
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
(x, y) = (x / 16, y / 16);
|
|
var layout = GetLayout();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
var (xx, yy) = ((int)x - border.West, (int)y - border.North);
|
|
if (xx < 0 || yy < 0 || xx > width || yy > height) return;
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
// first pass: set all the effected spaces to 0 so they won't count
|
|
var toDraw = new Queue<Point>();
|
|
toDraw.Enqueue(new(xx, yy));
|
|
var drawn = new List<Point>();
|
|
lock (pixelWriteLock) {
|
|
while (toDraw.Count > 0) {
|
|
var p = toDraw.Dequeue();
|
|
if (drawn.Contains(p)) continue;
|
|
var address = start + (p.Y * width + (p.X - 1)) * 2;
|
|
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X - 1, p.Y));
|
|
address = start + (p.Y * width + (p.X + 1)) * 2;
|
|
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X + 1, p.Y));
|
|
address = start + ((p.Y - 1) * width + p.X) * 2;
|
|
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y - 1));
|
|
address = start + ((p.Y + 1) * width + p.X) * 2;
|
|
if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y + 1));
|
|
address = start + (p.Y * width + p.X) * 2;
|
|
model.WriteMultiByteValue(address, 2, token, 0);
|
|
drawn.Add(p);
|
|
}
|
|
|
|
// second pass: wave-fill in the reverse order (outside in)
|
|
drawn.Reverse();
|
|
foreach (var p in drawn) {
|
|
var targetVal = wave(p.X, p.Y);
|
|
var address = start + (p.Y * width + p.X) * 2;
|
|
model.WriteMultiByteValue(address, 2, token, targetVal);
|
|
}
|
|
}
|
|
|
|
ClearPixelCache();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
|
|
public void UpdateEventLocation(IEventViewModel ev, double x, double y) {
|
|
(lastDrawX, lastDrawY) = (-1, -1);
|
|
var layout = GetLayout();
|
|
var border = GetBorderThickness(layout);
|
|
if (border == null) return;
|
|
(x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale);
|
|
var (xx, yy) = ((int)(x / 16) - border.West, (int)(y / 16) - border.North);
|
|
if (ev.X == xx && ev.Y == yy) return;
|
|
if (xx < 0 || yy < 0) return;
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
if (xx >= width || yy >= height) return;
|
|
ev.X = xx;
|
|
ev.Y = yy;
|
|
SelectedEvent = ev;
|
|
ClearPixelCache();
|
|
}
|
|
|
|
public IEventViewModel EventUnderCursor(double x, double y, bool autoSelect = true) {
|
|
var layout = GetLayout();
|
|
var border = GetBorderThickness(layout);
|
|
var tileX = (int)((x - LeftEdge) / SpriteScale / 16) - border.West;
|
|
var tileY = (int)((y - TopEdge) / SpriteScale / 16) - border.North;
|
|
IEventViewModel last = null;
|
|
foreach (var e in GetEvents()) {
|
|
if (e.X == tileX && e.Y == tileY) last = e;
|
|
}
|
|
if (autoSelect && SelectedEvent != last) {
|
|
SelectedEvent = last;
|
|
ClearPixelCache();
|
|
}
|
|
return last;
|
|
}
|
|
|
|
public IPixelViewModel AutoCrop(int warpID) {
|
|
if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model);
|
|
if (defaultOverworldSprite == null) defaultOverworldSprite = GetDefaultOW(model);
|
|
const int SizeX = 7, SizeY = 7;
|
|
var map = GetMapModel();
|
|
if (map == null) return null;
|
|
var layout = GetLayout(map);
|
|
if (layout == null) return null;
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var events = new EventGroupModel(ViewPort.Tools.CodeTool.ScriptParser, GotoAddress, map.GetSubTable("events")[0], allOverworldSprites, defaultOverworldSprite, BerryInfo, group, this.map);
|
|
if (events.Warps.Count <= warpID) return null;
|
|
var warp = events.Warps[warpID];
|
|
var startX = warp.X - SizeX / 2;
|
|
var startY = warp.Y - SizeY / 2;
|
|
while (startX < 0) startX += 1;
|
|
while (startY < 0) startY += 1;
|
|
while (startX + SizeX > width) startX--;
|
|
while (startY + SizeY > height) startY--;
|
|
|
|
return ReadonlyPixelViewModel.Crop(this, startX * 16, startY * 16, SizeX * 16, SizeY * 16);
|
|
}
|
|
|
|
public void DeselectEvent() {
|
|
if (selectedEvent == null) return;
|
|
SelectedEvent = null;
|
|
ClearPixelCache();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Connections
|
|
|
|
public IEnumerable<MapSlider> GetMapSliders() {
|
|
var connections = GetConnections();
|
|
if (connections == null) yield break;
|
|
var border = GetBorderThickness();
|
|
if (border == null) yield break;
|
|
var tileSize = (int)(16 * spriteScale);
|
|
int id = 0;
|
|
|
|
// get sliders for up/down/left/right connections
|
|
var connectionCount = (down: 0, up: 0, left: 0, right: 0);
|
|
foreach (var connection in connections) {
|
|
void Notify() => NeighborsChanged.Raise(this);
|
|
var map = GetNeighbor(connection, border);
|
|
var sourceMapInfo = (group, this.map);
|
|
|
|
if (connection.Direction == MapDirection.Up) {
|
|
connectionCount.up++;
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id, LeftRight, tutorials, right: map.LeftEdge, bottom: map.BottomEdge - tileSize);
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id + 1, LeftRight, tutorials, left: map.RightEdge, bottom: map.BottomEdge - tileSize);
|
|
}
|
|
|
|
if (connection.Direction == MapDirection.Down) {
|
|
connectionCount.down++;
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id, LeftRight, tutorials, right: map.LeftEdge, top: map.TopEdge + tileSize);
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id + 1, LeftRight, tutorials, left: map.RightEdge, top: map.TopEdge + tileSize);
|
|
}
|
|
|
|
if (connection.Direction == MapDirection.Left) {
|
|
connectionCount.left++;
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id, UpDown, tutorials, right: map.RightEdge - tileSize, bottom: map.TopEdge);
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id + 1, UpDown, tutorials, right: map.RightEdge - tileSize, top: map.BottomEdge);
|
|
}
|
|
|
|
if (connection.Direction == MapDirection.Right) {
|
|
connectionCount.right++;
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id, UpDown, tutorials, left: map.LeftEdge + tileSize, bottom: map.TopEdge);
|
|
yield return new ConnectionSlider(connection, sourceMapInfo, Notify, id + 1, UpDown, tutorials, left: map.LeftEdge + tileSize, top: map.BottomEdge);
|
|
}
|
|
|
|
id += 2;
|
|
}
|
|
|
|
// get sliders for size expansion
|
|
var centerX = (LeftEdge + RightEdge - MapSlider.SliderSize) / 2;
|
|
var centerY = (TopEdge + BottomEdge - MapSlider.SliderSize) / 2;
|
|
yield return new ExpansionSlider(ResizeMapData, id + 0, ExtendUp, GetConnectionCommands(connections, MapDirection.Up), left: centerX, bottom: TopEdge);
|
|
yield return new ExpansionSlider(ResizeMapData, id + 1, ExtendDown, GetConnectionCommands(connections, MapDirection.Down), left: centerX, top: BottomEdge);
|
|
yield return new ExpansionSlider(ResizeMapData, id + 2, ExtendLeft, GetConnectionCommands(connections, MapDirection.Left), right: LeftEdge, top: centerY);
|
|
yield return new ExpansionSlider(ResizeMapData, id + 3, ExtendRight, GetConnectionCommands(connections, MapDirection.Right), left: RightEdge, top: centerY);
|
|
}
|
|
|
|
private IEnumerable<IMenuCommand> GetConnectionCommands(IReadOnlyList<ConnectionModel> connections, MapDirection direction) {
|
|
var toRemove = new List<int>();
|
|
|
|
var info = CanConnect(direction);
|
|
if (info != null) {
|
|
if (info.Size > 3) {
|
|
// we can make a map here of width/height longestSpanLength
|
|
// and the offset is availableSpace[longestSpanStart]
|
|
yield return new MenuCommand<ConnectionInfo>("Create New Map", ConnectNewMap) { Parameter = info };
|
|
yield return new MenuCommand<ConnectionInfo>("Connect Existing Map", ConnectExistingMap) { Parameter = info };
|
|
} else if (info.Offset < 0) {
|
|
// we can make a map here of width/height 4
|
|
// and the offset is -3
|
|
yield return new MenuCommand<ConnectionInfo>("Create New Map", ConnectNewMap) { Parameter = info };
|
|
yield return new MenuCommand<ConnectionInfo>("Connect Existing Map", ConnectExistingMap) { Parameter = info };
|
|
} else {
|
|
// we can make a map here of width/height 4
|
|
// and the offset is dimensionLength-1
|
|
yield return new MenuCommand<ConnectionInfo>("Create New Map", ConnectNewMap) { Parameter = info };
|
|
yield return new MenuCommand<ConnectionInfo>("Connect Existing Map", ConnectExistingMap) { Parameter = info };
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < connections.Count; i++) {
|
|
if (connections[i].Direction != direction) continue;
|
|
toRemove.Add(i);
|
|
}
|
|
if (toRemove.Count > 0) {
|
|
// we can remove these connections
|
|
yield return new MenuCommand<IReadOnlyList<int>>("Remove Connections", RemoveConnections) { Parameter = toRemove };
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Work Methods
|
|
|
|
private IEnumerable<MapModel> GetAllMaps() {
|
|
foreach (var bank in model.GetTableModel(HardcodeTablesModel.MapBankTable)) {
|
|
if (bank == null) continue;
|
|
foreach (var mapList in bank.GetSubTable("maps")) {
|
|
if (mapList == null) continue;
|
|
var mapTable = mapList.GetSubTable("map");
|
|
if (mapTable == null) continue;
|
|
var map = mapTable[0];
|
|
yield return new(map);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ResizeMapData(MapDirection direction, int amount) {
|
|
if (amount == 0) return;
|
|
var token = tokenFactory();
|
|
var map = GetMapModel();
|
|
var layout = GetLayout(map);
|
|
if (layout == null) return;
|
|
var run = model.GetNextRun(layout.GetAddress("blockmap")) as BlockmapRun;
|
|
if (run == null) return;
|
|
var borderWidth = layout.HasField("borderwidth") ? layout.GetValue("borderwidth") : 2;
|
|
var borderHeight = layout.HasField("borderheight") ? layout.GetValue("borderheight") : 2;
|
|
var newRun = run.TryChangeSize(tokenFactory, direction, amount, borderWidth, borderHeight);
|
|
if (newRun != null) {
|
|
var tileSize = (int)(16 * spriteScale);
|
|
if (direction == MapDirection.Left) LeftEdge -= amount * tileSize;
|
|
if (direction == MapDirection.Up) TopEdge -= amount * tileSize;
|
|
foreach (var connection in GetConnections(map, group, this.map)) {
|
|
if (direction == MapDirection.Left) {
|
|
if (connection.Direction == MapDirection.Down || connection.Direction == MapDirection.Up) {
|
|
connection.Offset += amount;
|
|
var inverse = connection.GetInverse();
|
|
if (inverse != null) inverse.Offset -= amount;
|
|
}
|
|
} else if (direction == MapDirection.Up) {
|
|
if (connection.Direction == MapDirection.Left || connection.Direction == MapDirection.Right) {
|
|
connection.Offset += amount;
|
|
var inverse = connection.GetInverse();
|
|
if (inverse != null) inverse.Offset -= amount;
|
|
}
|
|
}
|
|
}
|
|
foreach (var e in GetEvents()) {
|
|
if (direction == MapDirection.Left) {
|
|
e.X += amount;
|
|
} else if (direction == MapDirection.Up) {
|
|
e.Y += amount;
|
|
}
|
|
}
|
|
RefreshMapSize();
|
|
NeighborsChanged.Raise(this);
|
|
if (newRun.Start != run.Start) InformRepoint(new("Map", newRun.Start));
|
|
tutorials.Complete(Tutorial.DragConnectionButtons_ResizeMap);
|
|
}
|
|
}
|
|
|
|
private void ConnectNewMap(ConnectionInfo info) {
|
|
using (viewPort.ChangeHistory.ContinueCurrentTransaction()) {
|
|
var token = tokenFactory();
|
|
var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start, tokenFactory);
|
|
tutorials.Complete(Tutorial.RightClick_CreateConnection);
|
|
var option = MapRepointer.GetMapBankForNewMap(
|
|
"Maps are organized into banks. The game doesn't care, so you can use the banks however you like."
|
|
+ Environment.NewLine +
|
|
"Which map bank do you want to use for the new map?");
|
|
if (option == -1) return;
|
|
|
|
var map = GetMapModel();
|
|
|
|
var connections = GetOrCreateConnections(map, token);
|
|
var connectionsAndCount = map.GetSubTable("connections")[0];
|
|
|
|
var originalConnectionStart = connections.Start;
|
|
connections = model.RelocateForExpansion(token, connections, connections.Length + connections.ElementLength);
|
|
if (connections.Start != originalConnectionStart) InformRepoint(new("Connections", connections.Start));
|
|
connectionsAndCount.SetValue("count", connections.ElementCount + 1);
|
|
var table = new ModelTable(model, connections.Start, tokenFactory, connections);
|
|
var newConnection = new ConnectionModel(table[connections.ElementCount], group, this.map);
|
|
newConnection.Offset = info.Offset;
|
|
newConnection.Direction = info.Direction;
|
|
|
|
var (width, height) = (info.Size, info.Size);
|
|
var isZConnection = info.Direction.IsAny(MapDirection.Dive, MapDirection.Emerge);
|
|
if (isZConnection) height = info.Offset;
|
|
var otherMap = CreateNewMap(token, option, width, height);
|
|
|
|
newConnection.MapGroup = otherMap.group;
|
|
newConnection.MapNum = otherMap.map;
|
|
info = new ConnectionInfo(info.Size, isZConnection ? 0 : -info.Offset, info.OppositeDirection);
|
|
newConnection = otherMap.AddConnection(info);
|
|
newConnection.Offset = isZConnection ? 0 : info.Offset;
|
|
newConnection.MapGroup = MapID / 1000;
|
|
newConnection.MapNum = MapID % 1000;
|
|
|
|
RefreshMapSize();
|
|
NeighborsChanged.Raise(this);
|
|
}
|
|
viewPort.ChangeHistory.ChangeCompleted();
|
|
}
|
|
|
|
private BlockMapViewModel CreateNewMap(ModelDelta token, int bank, int width, int height) {
|
|
var mapTable = MapRepointer.AddNewMapToBank(bank);
|
|
var newMap = MapRepointer.CreateNewMap(token);
|
|
var layout = MapRepointer.CreateNewLayout(token);
|
|
|
|
// update width / height
|
|
model.WriteValue(token, layout.Element.Start + 0, width);
|
|
model.WriteValue(token, layout.Element.Start + 4, height);
|
|
|
|
layout.Element.SetAddress(Format.BlockMap, MapRepointer.CreateNewBlockMap(token, width, height));
|
|
|
|
newMap.Element.SetAddress(Format.Layout, layout.Element.Start);
|
|
model.UpdateArrayPointer(token, null, null, -1, mapTable.Start + mapTable.Length - 4, newMap.Element.Start);
|
|
|
|
var otherMap = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, bank, mapTable.ElementCount - 1) {
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
};
|
|
otherMap.UpdateLayoutID();
|
|
return otherMap;
|
|
}
|
|
|
|
private void ConnectExistingMap(ConnectionInfo info) {
|
|
var token = tokenFactory();
|
|
|
|
// find available maps
|
|
var options = new Dictionary<int, ConnectionInfo>();
|
|
var table = model.GetTable(HardcodeTablesModel.MapBankTable);
|
|
var mapBanks = new ModelTable(model, table.Start, tokenFactory);
|
|
for (int group = 0; group < mapBanks.Count; group++) {
|
|
var bank = mapBanks[group];
|
|
var maps = bank.GetSubTable("maps");
|
|
for (int map = 0; map < maps.Count; map++) {
|
|
var mapVM = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, group, map) {
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
};
|
|
var newInfo = mapVM.CanConnect(info.OppositeDirection);
|
|
if (newInfo != null) options[mapVM.MapID] = newInfo;
|
|
}
|
|
}
|
|
|
|
// select which map to add
|
|
var keys = options.Keys.ToList();
|
|
var enumViewModel = new EnumViewModel(keys.Select(key => MapIDToText(model, key)).ToArray());
|
|
|
|
tutorials.Complete(Tutorial.RightClick_CreateConnection);
|
|
var option = fileSystem.ShowOptions(
|
|
"Pick a map",
|
|
"Which map do you want to connect to?",
|
|
new[] { new[] { enumViewModel } },
|
|
new VisualOption { Index = 1, Option = "OK", ShortDescription = "Connect Existing Map" });
|
|
if (option == -1) return;
|
|
var choice = keys[enumViewModel.Choice];
|
|
|
|
var otherMap = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, choice / 1000, choice % 1000) {
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
};
|
|
var size = GetBlockSize();
|
|
var otherSize = otherMap.GetBlockSize();
|
|
if (info.Direction.IsAny(MapDirection.Left, MapDirection.Right)) {
|
|
info = info with { Offset = info.Offset - (otherSize.height - info.Size) / 2 };
|
|
} else if (info.Direction.IsAny(MapDirection.Up, MapDirection.Down)) {
|
|
info = info with { Offset = info.Offset - (otherSize.width - info.Size) / 2 };
|
|
} else if (info.Direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) {
|
|
info = info with { Offset = 0 };
|
|
}
|
|
|
|
var newConnection = AddConnection(info);
|
|
if (newConnection == null) return;
|
|
newConnection.Offset = info.Offset;
|
|
newConnection.Direction = info.Direction;
|
|
newConnection.MapGroup = choice / 1000;
|
|
newConnection.MapNum = choice % 1000;
|
|
|
|
info = options[choice];
|
|
if (info.Direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) info = info with { Offset = 0 };
|
|
newConnection = otherMap.AddConnection(info);
|
|
newConnection.Offset = info.Offset;
|
|
newConnection.MapGroup = MapID / 1000;
|
|
newConnection.MapNum = MapID % 1000;
|
|
|
|
RefreshMapSize();
|
|
NeighborsChanged.Raise(this);
|
|
viewPort.ChangeHistory.ChangeCompleted();
|
|
}
|
|
|
|
private void RemoveConnections(IReadOnlyList<int> toRemove) {
|
|
var token = tokenFactory();
|
|
var map = GetMapModel();
|
|
var connections = GetConnections(map, group, this.map);
|
|
for (int i = 0; i < toRemove.Count; i++) {
|
|
for (int j = toRemove[i] - i + 1; j < connections.Count - i; j++) {
|
|
connections[j - 1].Direction = connections[j].Direction;
|
|
connections[j - 1].Offset = connections[j].Offset;
|
|
connections[j - 1].MapGroup = connections[j].MapGroup;
|
|
connections[j - 1].MapNum = connections[j].MapNum;
|
|
}
|
|
var connectionsTable = connections[0].Table;
|
|
if (connectionsTable.ElementCount == 1) {
|
|
Erase(connectionsTable, token);
|
|
} else {
|
|
var shorterTable = connectionsTable.Append(token, -1);
|
|
model.ObserveRunWritten(token, shorterTable);
|
|
}
|
|
}
|
|
var connectionsAndCount = map.GetSubTable("connections")[0];
|
|
connectionsAndCount.SetValue("count", connections.Count - toRemove.Count);
|
|
|
|
RefreshMapSize();
|
|
NeighborsChanged.Raise(this);
|
|
viewPort.ChangeHistory.ChangeCompleted();
|
|
}
|
|
|
|
private ConnectionModel AddConnection(ConnectionInfo info) {
|
|
var token = tokenFactory();
|
|
var map = GetMapModel();
|
|
|
|
var connections = GetOrCreateConnections(map, token);
|
|
if (connections == null) return null;
|
|
|
|
var count = connections.ElementCount;
|
|
connections = connections.Append(token, 1);
|
|
model.ObserveRunWritten(token, connections);
|
|
|
|
var table = new ModelTable(model, connections.Start, tokenFactory, connections);
|
|
var newConnection = new ConnectionModel(table[count], group, this.map);
|
|
token.ChangeData(model, table[count].Start, new byte[12]);
|
|
newConnection.Direction = info.Direction;
|
|
return newConnection;
|
|
}
|
|
|
|
private ITableRun GetOrCreateConnections(ModelArrayElement map, ModelDelta token) {
|
|
if (map == null) return null;
|
|
var connectionsAndCountTable = map.GetSubTable("connections");
|
|
if (connectionsAndCountTable == null) {
|
|
var newConnectionsAndCountTable = MapRepointer.CreateNewConnections(token);
|
|
model.UpdateArrayPointer(token, null, null, -1, map.Start + 12, newConnectionsAndCountTable);
|
|
connectionsAndCountTable = map.GetSubTable("connections");
|
|
}
|
|
var connectionsAndCount = connectionsAndCountTable[0];
|
|
|
|
ITableRun connections;
|
|
|
|
if (connectionsAndCount.GetValue("count") == 0) {
|
|
var newConnectionTableStart = model.FindFreeSpace(model.FreeSpaceStart, 12);
|
|
var childContent = ConnectionInfo.SingleConnectionContent;
|
|
var lengthToken = ConnectionInfo.SingleConnectionLength;
|
|
var childSegments = ArrayRun.ParseSegments(childContent, model);
|
|
var parentStrategy = TableStreamRun.ParseEndStream(model, "connections", lengthToken, childSegments, connectionsAndCountTable.Run.ElementContent);
|
|
connections = new TableStreamRun(model, newConnectionTableStart, SortedSpan.One(connectionsAndCount.Start + 4), $"[{childContent}]{lengthToken}", childSegments, parentStrategy, 0);
|
|
connectionsAndCount.SetAddress("connections", newConnectionTableStart);
|
|
InformCreate(new("Connection", newConnectionTableStart));
|
|
} else {
|
|
connections = connectionsAndCount.GetSubTable("connections").Run;
|
|
}
|
|
|
|
return connections;
|
|
}
|
|
|
|
public ObjectEventViewModel CreateObjectEvent(int graphics, int scriptAddress) {
|
|
var token = tokenFactory();
|
|
var map = GetMapModel();
|
|
if (map == null) return null;
|
|
var events = map.GetSubTable("events")[0];
|
|
var element = AddEvent(events, tokenFactory, "objectCount", "objects");
|
|
if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model);
|
|
if (defaultOverworldSprite == null) defaultOverworldSprite = GetDefaultOW(model);
|
|
var newEvent = new ObjectEventViewModel(ViewPort.Tools.CodeTool.ScriptParser, GotoAddress, element, allOverworldSprites, defaultOverworldSprite, BerryInfo) {
|
|
X = 0, Y = 0,
|
|
Elevation = 0,
|
|
ObjectID = element.Table.ElementCount,
|
|
ScriptAddress = scriptAddress,
|
|
Graphics = graphics,
|
|
RangeX = 0,
|
|
RangeY = 0,
|
|
Flag = 0,
|
|
MoveType = 0,
|
|
TrainerType = 0,
|
|
TrainerRangeOrBerryID = 0,
|
|
};
|
|
newEvent.ClearUnused();
|
|
SelectedEvent = newEvent;
|
|
return newEvent;
|
|
}
|
|
|
|
public WarpEventViewModel CreateWarpEvent(int bank, int map) {
|
|
var mapModel = GetMapModel();
|
|
if (mapModel == null) return null;
|
|
var events = mapModel.GetSubTable("events")[0];
|
|
var element = AddEvent(events, tokenFactory, "warpCount", "warps");
|
|
var newEvent = new WarpEventViewModel(element) { X = 0, Y = 0, Elevation = 0, Bank = bank, Map = map, WarpID = element.ArrayIndex + 1 };
|
|
SelectedEvent = newEvent;
|
|
return newEvent;
|
|
}
|
|
|
|
public ScriptEventViewModel CreateScriptEvent() {
|
|
var map = GetMapModel();
|
|
if (map == null) return null;
|
|
var events = map.GetSubTable("events")[0];
|
|
var element = AddEvent(events, tokenFactory, "scriptCount", "scripts");
|
|
var newEvent = new ScriptEventViewModel(GotoAddress, element) { X = 0, Y = 0, Elevation = 0, Index = 0, Trigger = 0, ScriptAddress = Pointer.NULL };
|
|
SelectedEvent = newEvent;
|
|
return newEvent;
|
|
}
|
|
|
|
public SignpostEventViewModel CreateSignpostEvent() {
|
|
var map = GetMapModel();
|
|
if (map == null) return null;
|
|
var events = map.GetSubTable("events")[0];
|
|
var element = AddEvent(events, tokenFactory, "signpostCount", "signposts");
|
|
var newEvent = new SignpostEventViewModel(element, GotoAddress) { X = 0, Y = 0, Elevation = 0, Kind = 0, Pointer = Pointer.NULL };
|
|
SelectedEvent = newEvent;
|
|
return newEvent;
|
|
}
|
|
|
|
public bool CanCreateFlyEvent {
|
|
get {
|
|
var map = GetMapModel();
|
|
if (map == null) return false;
|
|
var region = map.GetValue(Format.RegionSection);
|
|
if (model.IsFRLG()) region -= 88;
|
|
var connections = model.GetTableModel(HardcodeTablesModel.FlyConnections);
|
|
if (region < 0 || region >= connections.Count) return false;
|
|
return connections[region].GetValue("flight") == 0;
|
|
}
|
|
}
|
|
|
|
public FlyEventViewModel CreateFlyEvent() {
|
|
var map = GetMapModel();
|
|
var region = map.GetValue(Format.RegionSection);
|
|
if (model.IsFRLG()) region -= 88;
|
|
var connections = model.GetTableModel(HardcodeTablesModel.FlyConnections, tokenFactory);
|
|
if (region < 0 || region >= connections.Count) return null;
|
|
var flight = connections[region].GetValue("flight");
|
|
if (flight != 0) return null;
|
|
var spawns = model.GetTableModel(HardcodeTablesModel.FlySpawns, tokenFactory);
|
|
|
|
// hunt for an available spawn location
|
|
var emptySpawn = -1;
|
|
for (int i = 0; i < spawns.Count; i++) {
|
|
if (spawns[i].GetValue("x") == 0 && spawns[i].GetValue("y") == 0 && spawns[i].GetValue("bank") == 0 && spawns[i].GetValue("map") == 0) {
|
|
emptySpawn = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// if there were no empty entries in the table, add a new one
|
|
if (emptySpawn == -1) {
|
|
var newSpawns = model.RelocateForExpansion(tokenFactory(), spawns.Run, spawns.Run.Length + spawns.Run.ElementLength);
|
|
newSpawns = newSpawns.Append(tokenFactory(), 1);
|
|
model.ObserveRunWritten(tokenFactory(), newSpawns);
|
|
if (newSpawns.Start != spawns.Run.Start) InformRepoint(new("Fly Spawns", newSpawns.Start));
|
|
spawns = new ModelTable(model, newSpawns, tokenFactory);
|
|
emptySpawn = spawns.Count - 1;
|
|
}
|
|
|
|
// update the connections and spawn table
|
|
connections[region].SetValue("flight", emptySpawn + 1);
|
|
connections[region].SetValue("bank", group);
|
|
connections[region].SetValue("map", this.map);
|
|
spawns[emptySpawn].SetValue("bank", group);
|
|
spawns[emptySpawn].SetValue("map", this.map);
|
|
|
|
NotifyPropertyChanged(nameof(CanCreateFlyEvent));
|
|
return new FlyEventViewModel(spawns[emptySpawn], group, this.map, emptySpawn + 1);
|
|
}
|
|
|
|
// TODO use this for connections as well, since the structure is the same
|
|
public ModelArrayElement AddEvent(ModelArrayElement events, Func<ModelDelta> tokenFactory, string countName, string fieldName) {
|
|
var model = events.Model;
|
|
var count = events.GetValue(countName);
|
|
var elementTable = events.GetSubTable(fieldName)?.Run;
|
|
if (count == 0 || elementTable == null) {
|
|
var segment = (ArrayRunPointerSegment)events.Table.ElementContent.Single(seg => seg.Name == fieldName);
|
|
var divider = segment.InnerFormat.LastIndexOf("/");
|
|
var newTableStart = model.FindFreeSpace(model.FreeSpaceStart, 24);
|
|
var childContent = segment.InnerFormat.Substring(0, divider);
|
|
childContent = childContent.Substring(1, childContent.Length - 2);
|
|
var lengthToken = segment.InnerFormat.Substring(divider);
|
|
var childSegments = ArrayRun.ParseSegments(childContent, model);
|
|
var parentStrategy = TableStreamRun.ParseEndStream(model, fieldName, lengthToken, childSegments, events.Table.ElementContent);
|
|
elementTable = new TableStreamRun(model, newTableStart, SortedSpan.One(events.Table.ElementContent.Until(seg => seg.Name == fieldName).Sum(seg => seg.Length) + events.Table.Start), segment.InnerFormat, childSegments, parentStrategy, 0);
|
|
events.SetAddress(fieldName, newTableStart);
|
|
}
|
|
var token = tokenFactory();
|
|
var newRun = elementTable.Append(token, 1);
|
|
model.ObserveRunWritten(token, newRun);
|
|
if (newRun.Start != elementTable.Start) InformRepoint(new(fieldName, newRun.Start));
|
|
return new ModelArrayElement(model, newRun.Start, newRun.ElementCount - 1, tokenFactory, newRun);
|
|
}
|
|
|
|
private void Erase(ITableRun table, ModelDelta token) {
|
|
foreach (var source in table.PointerSources) {
|
|
model.ClearPointer(token, source, table.Start);
|
|
model.WritePointer(token, source, Pointer.NULL);
|
|
}
|
|
model.ClearData(token, table.Start, table.Length);
|
|
}
|
|
|
|
private void UpdateLayoutID() {
|
|
// step 1: test if we need to update the layout id
|
|
var layoutTable = model.GetTable(HardcodeTablesModel.MapLayoutTable);
|
|
var map = GetMapModel();
|
|
var layoutID = map.GetValue("layoutID") - 1;
|
|
var addressFromMap = map.GetAddress("layout");
|
|
var addressFromTable = model.ReadPointer(layoutTable.Start + layoutTable.ElementLength * layoutID);
|
|
if (addressFromMap == addressFromTable) return;
|
|
|
|
var matches = layoutTable.ElementCount.Range().Where(i => model.ReadPointer(layoutTable.Start + layoutTable.ElementLength * i) == addressFromMap).ToList();
|
|
var token = tokenFactory();
|
|
if (matches.Count == 0) {
|
|
var originalLayoutTableStart = layoutTable.Start;
|
|
layoutTable = model.RelocateForExpansion(token, layoutTable, layoutTable.Length + 4);
|
|
layoutTable = layoutTable.Append(token, 1);
|
|
model.ObserveRunWritten(token, layoutTable);
|
|
model.UpdateArrayPointer(token, layoutTable.ElementContent[0], layoutTable.ElementContent, -1, layoutTable.Start + layoutTable.ElementLength * (layoutTable.ElementCount - 1), addressFromMap);
|
|
if (originalLayoutTableStart != layoutTable.Start) InformRepoint(new("Layout Table", layoutTable.Start));
|
|
matches.Add(layoutTable.ElementCount - 1);
|
|
}
|
|
map.SetValue("layoutID", matches[0] + 1);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Helper Methods
|
|
|
|
private (int width, int height) GetBlockSize(ModelArrayElement layout = null) {
|
|
var border = GetBorderThickness(layout);
|
|
if (border == null) return (0, 0);
|
|
return (pixelWidth / 16 - border.West - border.East, pixelHeight / 16 - border.North - border.South);
|
|
}
|
|
|
|
private BlockMapViewModel GetNeighbor(ConnectionModel connection, Border border) {
|
|
var vm = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, connection.MapGroup, connection.MapNum) {
|
|
IncludeBorders = IncludeBorders,
|
|
SpriteScale = SpriteScale,
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
CollisionHighlight = CollisionHighlight,
|
|
};
|
|
var (n, _, _, w) = vm.GetBorderThickness();
|
|
vm.TopEdge = TopEdge + (connection.Offset + border.North - n) * (int)(16 * SpriteScale);
|
|
vm.LeftEdge = LeftEdge + (connection.Offset + border.West - w) * (int)(16 * SpriteScale);
|
|
if (connection.Direction == MapDirection.Left) vm.LeftEdge = LeftEdge - (int)(vm.PixelWidth * SpriteScale);
|
|
if (connection.Direction == MapDirection.Right) vm.LeftEdge = LeftEdge + (int)(PixelWidth * SpriteScale);
|
|
if (connection.Direction == MapDirection.Up) vm.TopEdge = TopEdge - (int)(vm.PixelHeight * SpriteScale);
|
|
if (connection.Direction == MapDirection.Down) vm.TopEdge = TopEdge + (int)(PixelHeight * SpriteScale);
|
|
vm.ZIndex = ZIndex;
|
|
if (connection.Direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) vm.ZIndex = ZIndex - 1;
|
|
return vm;
|
|
}
|
|
|
|
private void RefreshPaletteCache(ModelArrayElement layout = null, BlocksetModel blockModel1 = null, BlocksetModel blockModel2 = null) {
|
|
if (blockModel1 == null || blockModel2 == null) {
|
|
if (layout == null) layout = GetLayout();
|
|
if (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress(Format.PrimaryBlockset));
|
|
if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress(Format.SecondaryBlockset));
|
|
}
|
|
|
|
palettes = BlockmapRun.ReadPalettes(blockModel1, blockModel2, PrimaryPalettes);
|
|
}
|
|
|
|
private void RefreshTileCache(ModelArrayElement layout = null, BlocksetModel blockModel1 = null, BlocksetModel blockModel2 = null) {
|
|
if (blockModel1 == null || blockModel2 == null) {
|
|
if (layout == null) layout = GetLayout();
|
|
if (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1"));
|
|
if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2"));
|
|
}
|
|
|
|
tiles = BlockmapRun.ReadTiles(blockModel1, blockModel2, PrimaryTiles);
|
|
}
|
|
|
|
private void RefreshBlockCache(ModelArrayElement layout = null, BlocksetModel blockModel1 = null, BlocksetModel blockModel2 = null) {
|
|
if (layout == null) layout = GetLayout();
|
|
if (blockModel1 == null || blockModel2 == null) {
|
|
if (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress(Format.PrimaryBlockset));
|
|
if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress(Format.SecondaryBlockset));
|
|
}
|
|
int width = layout.GetValue("width"), height = layout.GetValue("height");
|
|
int start = layout.GetAddress(Format.BlockMap);
|
|
var maxUsedPrimary = BlockmapRun.GetMaxUsedBlock(model, start, width, height, PrimaryBlocks);
|
|
var maxUsedSecondary = BlockmapRun.GetMaxUsedBlock(model, start, width, height, 1024) - PrimaryBlocks;
|
|
|
|
blocks = BlockmapRun.ReadBlocks(maxUsedPrimary, maxUsedSecondary, blockModel1, blockModel2);
|
|
}
|
|
|
|
private void RefreshBlockAttributeCache(ModelArrayElement layout = null, BlocksetModel blockModel1 = null, BlocksetModel blockModel2 = null) {
|
|
if (blockModel1 == null || blockModel2 == null) {
|
|
if (layout == null) layout = GetLayout();
|
|
if (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1"));
|
|
if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2"));
|
|
}
|
|
int width = layout.GetValue("width"), height = layout.GetValue("height");
|
|
int start = layout.GetAddress(Format.BlockMap);
|
|
var maxUsedPrimary = BlockmapRun.GetMaxUsedBlock(model, start, width, height, PrimaryBlocks);
|
|
var maxUsedSecondary = BlockmapRun.GetMaxUsedBlock(model, start, width, height, 1024) - PrimaryBlocks;
|
|
|
|
blockAttributes = BlockmapRun.ReadBlockAttributes(maxUsedPrimary, maxUsedSecondary, blockModel1, blockModel2);
|
|
}
|
|
|
|
private void RefreshBlockRenderCache(ModelArrayElement layout = null, BlocksetModel blockModel1 = null, BlocksetModel blockModel2 = null) {
|
|
if (blocks == null || tiles == null || palettes == null) {
|
|
if (layout == null) layout = GetLayout();
|
|
if (layout == null) return;
|
|
if (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress(Format.PrimaryBlockset));
|
|
if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress(Format.SecondaryBlockset));
|
|
}
|
|
if (blocks == null) RefreshBlockCache(layout, blockModel1, blockModel2);
|
|
if (tiles == null) RefreshTileCache(layout, blockModel1, blockModel2);
|
|
if (palettes == null) RefreshPaletteCache(layout, blockModel1, blockModel2);
|
|
|
|
lock (blockRenders) {
|
|
blockRenders.Clear();
|
|
blockRenders.AddRange(BlockmapRun.CalculateBlockRenders(blocks, tiles, palettes));
|
|
}
|
|
}
|
|
|
|
private void RefreshMapSize() {
|
|
var layout = GetLayout();
|
|
if (layout == null) return;
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
(pixelWidth, pixelHeight) = ((width + border.West + border.East) * 16, (height + border.North + border.South) * 16);
|
|
ClearPixelCache();
|
|
}
|
|
|
|
private void RefreshMapEvents() {
|
|
if (eventRenders != null) return;
|
|
var list = new List<IEventViewModel>();
|
|
var events = GetEvents();
|
|
foreach (var obj in events) {
|
|
obj.Render(model);
|
|
list.Add(obj);
|
|
}
|
|
eventRenders = list;
|
|
}
|
|
|
|
private void FillMapPixelData() {
|
|
var layout = GetLayout();
|
|
if (layout == null) return;
|
|
lock (blockRenders) {
|
|
if (blockRenders.Count == 0) RefreshBlockRenderCache(layout);
|
|
}
|
|
if (borderBlock == null) RefreshBorderRender();
|
|
var (width, height) = (layout.GetValue("width"), layout.GetValue("height"));
|
|
var border = GetBorderThickness(layout);
|
|
var start = layout.GetAddress("blockmap");
|
|
|
|
var canvas = new CanvasPixelViewModel(pixelWidth, pixelHeight);
|
|
var (borderWidth, borderHeight) = (borderBlock.PixelWidth / 16, borderBlock.PixelHeight / 16);
|
|
for (int y = 0; y < height + border.North + border.South; y++) {
|
|
for (int x = 0; x < width + border.West + border.East; x++) {
|
|
if (y < border.North || x < border.West || y >= border.North + height || x >= border.West + width) {
|
|
var (xEdge, yEdge) = (x - border.West - width, y - border.North - height);
|
|
var (rightEdge, bottomEdge) = (xEdge >= 0, yEdge >= 0);
|
|
// top/left
|
|
if (!rightEdge && !bottomEdge && x % borderWidth == 0 && y % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16);
|
|
// right edge
|
|
if (rightEdge && !bottomEdge && xEdge % borderWidth == 0 && y % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16);
|
|
// bottom edge
|
|
if (!rightEdge && bottomEdge && x % borderWidth == 0 && yEdge % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16);
|
|
// bottom right corner
|
|
if (rightEdge && bottomEdge && xEdge % borderWidth == 0 && yEdge % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16);
|
|
continue;
|
|
}
|
|
var data = model.ReadMultiByteValue(start + ((y - border.North) * width + x - border.West) * 2, 2);
|
|
var collision = data >> 10;
|
|
data &= 0x3FF;
|
|
if (blockRenders.Count > data) canvas.Draw(blockRenders[data], x * 16, y * 16);
|
|
if (collision == collisionHighlight) HighlightCollision(canvas.PixelData, x * 16, y * 16);
|
|
if (collisionHighlight == -1 && selectedEvent is ObjectEventViewModel obj && obj.ShouldHighlight(x - border.West, y - border.North)) {
|
|
HighlightCollision(canvas.PixelData, x * 16, y * 16);
|
|
}
|
|
}
|
|
}
|
|
|
|
// draw the box for the selected event
|
|
var gray = UncompressedPaletteColor.Pack(6, 6, 6);
|
|
if (selectedEvent != null && selectedEvent.X >= 0 && selectedEvent.X < width && selectedEvent.Y >= 0 && SelectedEvent.Y < height) {
|
|
canvas.DrawBox((selectedEvent.X + border.West) * 16, (selectedEvent.Y + border.North) * 16, 16, gray);
|
|
}
|
|
|
|
// now draw the events on top
|
|
if (eventRenders == null) RefreshMapEvents();
|
|
if (eventRenders != null) {
|
|
foreach (var obj in eventRenders) {
|
|
if (obj.EventRender != null) {
|
|
var (x, y) = ((obj.X + border.West) * 16 + obj.LeftOffset, (obj.Y + border.North) * 16 + obj.TopOffset);
|
|
canvas.Draw(obj.EventRender, x, y);
|
|
}
|
|
}
|
|
}
|
|
|
|
// finally, draw a one-pixel border around the entire map (but not the border blocks)
|
|
if (isSelected) {
|
|
var (borderW, borderH) = (border.West + border.East, border.North + border.South);
|
|
canvas.DarkenRect(border.West * 16, border.North * 16, pixelWidth - borderW * 16, pixelHeight - borderH * 16, 12);
|
|
}
|
|
|
|
pixelData = canvas.PixelData;
|
|
}
|
|
|
|
private void HighlightCollision(short[] pixelData, int x, int y) {
|
|
void Transform(int xx, int yy) {
|
|
var p = (y + yy) * PixelWidth + x + xx;
|
|
pixelData[p] = CanvasPixelViewModel.Darken(pixelData[p], 8);
|
|
}
|
|
for (int i = 0; i < 15; i++) {
|
|
Transform(i, 0);
|
|
Transform(15 - i, 15);
|
|
Transform(0, 15 - i);
|
|
Transform(15, i);
|
|
}
|
|
}
|
|
|
|
public const int BlocksPerRow = 8;
|
|
private void FillBlockPixelData() {
|
|
var layout = GetLayout();
|
|
lock (blockRenders) {
|
|
if (blockRenders.Count == 0) RefreshBlockRenderCache(layout);
|
|
}
|
|
|
|
var blockHeight = (int)Math.Ceiling((double)blockRenders.Count / BlocksPerRow);
|
|
var canvas = new CanvasPixelViewModel(BlocksPerRow * 16, blockHeight * 16) { SpriteScale = 2 };
|
|
|
|
for (int y = 0; y < blockHeight; y++) {
|
|
for (int x = 0; x < BlocksPerRow; x++) {
|
|
if (blockRenders.Count <= y * BlocksPerRow + x) break;
|
|
canvas.Draw(blockRenders[y * BlocksPerRow + x], x * 16, y * 16);
|
|
}
|
|
}
|
|
|
|
blockPixels = canvas;
|
|
}
|
|
|
|
private void RefreshBorderRender(ModelArrayElement layout = null) {
|
|
if (layout == null) layout = GetLayout();
|
|
lock (blockRenders) {
|
|
if (blockRenders.Count == 0) RefreshBlockRenderCache(layout);
|
|
}
|
|
var width = layout.HasField("borderwidth") ? layout.GetValue("borderwidth") : 2;
|
|
var height = layout.HasField("borderheight") ? layout.GetValue("borderheight") : 2;
|
|
|
|
var start = layout.GetAddress("borderblock");
|
|
var canvas = new CanvasPixelViewModel(width * 16, height * 16);
|
|
for (int y = 0; y < height; y++) {
|
|
for (int x = 0; x < width; x++) {
|
|
var data = model.ReadMultiByteValue(start + (y * width + x) * 2, 2);
|
|
data &= 0x3FF;
|
|
canvas.Draw(blockRenders[data], x * 16, y * 16);
|
|
}
|
|
}
|
|
|
|
BorderBlock = canvas;
|
|
}
|
|
|
|
private ModelArrayElement GetMapModel() => GetMapModel(model, group, map, tokenFactory);
|
|
public static ModelArrayElement GetMapModel(IDataModel model, int group, int map, Func<ModelDelta> tokenFactory) {
|
|
var table = model.GetTable(HardcodeTablesModel.MapBankTable);
|
|
if (table == null) return null;
|
|
var mapBanks = new ModelTable(model, table.Start, tokenFactory);
|
|
if (mapBanks.Count <= group) return null;
|
|
var bank = mapBanks[group]?.GetSubTable("maps");
|
|
if (bank == null) return null;
|
|
if (bank.Count <= map) return null;
|
|
var mapTable = bank[map]?.GetSubTable("map");
|
|
if (mapTable == null) return null;
|
|
return mapTable[0];
|
|
}
|
|
|
|
public ModelArrayElement GetLayout(ModelArrayElement map = null) {
|
|
if (map == null) map = GetMapModel();
|
|
if (map == null) return null;
|
|
var layout = map.GetSubTable("layout");
|
|
if (layout == null) return null;
|
|
return layout[0];
|
|
}
|
|
|
|
private IReadOnlyList<ConnectionModel> GetConnections() {
|
|
var map = GetMapModel(model, group, this.map, tokenFactory);
|
|
return GetConnections(map, group, this.map);
|
|
}
|
|
public static IReadOnlyList<ConnectionModel> GetConnections(ModelArrayElement map, int bankNum, int mapNum) {
|
|
if (map == null) return null;
|
|
var connectionsAndCountTable = map.GetSubTable("connections");
|
|
var list = new List<ConnectionModel>();
|
|
if (connectionsAndCountTable == null) return list;
|
|
var connectionsAndCount = connectionsAndCountTable[0];
|
|
var count = connectionsAndCount.GetValue("count");
|
|
if (count == 0) return list;
|
|
var connections = connectionsAndCount.GetSubTable("connections");
|
|
if (connections == null) return new ConnectionModel[0];
|
|
for (int i = 0; i < count; i++) list.Add(new(connections[i], bankNum, mapNum));
|
|
return list;
|
|
}
|
|
|
|
private IPixelViewModel defaultOverworldSprite;
|
|
private IReadOnlyList<IPixelViewModel> allOverworldSprites;
|
|
public IReadOnlyList<IPixelViewModel> AllOverworldSprites {
|
|
get {
|
|
if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model);
|
|
return allOverworldSprites;
|
|
}
|
|
init => allOverworldSprites = value;
|
|
}
|
|
public static IPixelViewModel GetDefaultOW(IDataModel model) {
|
|
var defaultSpriteAddress = model.GetAddressFromAnchor(new NoDataChangeDeltaModel(), -1, HardcodeTablesModel.PokeIconsTable + "/0/icon/");
|
|
var defaultSpriteRun = model.GetNextRun(defaultSpriteAddress) as ISpriteRun;
|
|
var defaultImage = defaultSpriteRun == null ? new ReadonlyPixelViewModel(16, 16) : model.CurrentCacheScope.GetImage(defaultSpriteRun);
|
|
if (defaultImage.PixelHeight > 24) {
|
|
var canvas = new CanvasPixelViewModel(defaultImage.PixelWidth, 24) { Transparent = defaultImage.PixelData[0] };
|
|
canvas.Draw(defaultImage, 0, 24 - Math.Min(32, defaultImage.PixelHeight));
|
|
defaultImage = canvas;
|
|
}
|
|
return defaultImage;
|
|
}
|
|
public static List<IPixelViewModel> RenderOWs(IDataModel model) {
|
|
var list = new List<IPixelViewModel>();
|
|
var run = model.GetTable(HardcodeTablesModel.OverworldSprites);
|
|
var ows = new ModelTable(model, run.Start, null, run);
|
|
var defaultImage = GetDefaultOW(model);
|
|
for (int i = 0; i < ows.Count; i++) {
|
|
list.Add(ObjectEventViewModel.Render(model, ows, defaultImage, i, 0));
|
|
}
|
|
return list;
|
|
}
|
|
|
|
private IReadOnlyList<IEventViewModel> GetEvents() {
|
|
if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model);
|
|
if (defaultOverworldSprite == null) defaultOverworldSprite = GetDefaultOW(model);
|
|
var map = GetMapModel();
|
|
var results = new List<IEventViewModel>();
|
|
var eventsTable = map.GetSubTable("events");
|
|
if (eventsTable == null) return results;
|
|
var eventElements = eventsTable[0];
|
|
if (eventElements == null) return results;
|
|
var events = new EventGroupModel(ViewPort.Tools.CodeTool.ScriptParser, GotoAddress, eventElements, allOverworldSprites, defaultOverworldSprite, BerryInfo, group, this.map);
|
|
events.DataMoved += HandleEventDataMoved;
|
|
results.AddRange(events.Objects);
|
|
results.AddRange(events.Warps);
|
|
results.AddRange(events.Scripts);
|
|
results.AddRange(events.Signposts);
|
|
results.AddRange(events.FlyEvents);
|
|
return results;
|
|
}
|
|
|
|
public Border GetBorderThickness(ModelArrayElement layout = null) {
|
|
if (!includeBorders) return new(0, 0, 0, 0);
|
|
var connections = GetConnections();
|
|
if (connections == null) return null;
|
|
if (layout == null) layout = GetLayout();
|
|
var width = layout.HasField("borderwidth") ? layout.GetValue("borderwidth") : 2;
|
|
var height = layout.HasField("borderheight") ? layout.GetValue("borderheight") : 2;
|
|
var (east, west) = (width, width);
|
|
var (north, south) = (height, height);
|
|
var directions = connections.Select(c => c.Direction).ToList();
|
|
if (directions.Contains(MapDirection.Down)) south = 0;
|
|
if (directions.Contains(MapDirection.Up)) north = 0;
|
|
if (directions.Contains(MapDirection.Left)) west = 0;
|
|
if (directions.Contains(MapDirection.Right)) east = 0;
|
|
return new(north, east, south, west);
|
|
}
|
|
|
|
private ConnectionInfo CanConnect(MapDirection direction) {
|
|
var connections = GetConnections();
|
|
var (width, height) = (pixelWidth / 16, pixelHeight / 16);
|
|
var dimensionLength = (direction switch {
|
|
MapDirection.Up => width,
|
|
MapDirection.Down => width,
|
|
MapDirection.Left => height,
|
|
MapDirection.Right => height,
|
|
MapDirection.Dive => 0,
|
|
MapDirection.Emerge => 0,
|
|
_ => throw new NotImplementedException(),
|
|
});
|
|
var availableSpace = dimensionLength.Range().ToList();
|
|
|
|
// can't add a connection where there already is one
|
|
for (int i = 0; i < (connections?.Count ?? 0); i++) {
|
|
if (connections[i].Direction != direction) continue;
|
|
if (direction == MapDirection.Up || direction == MapDirection.Down) {
|
|
var map = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, connections[i].MapGroup, connections[i].MapNum) {
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
};
|
|
var removeWidth = map.pixelWidth / 16;
|
|
var removeOffset = connections[i].Offset;
|
|
foreach (int j in removeWidth.Range()) availableSpace.Remove(j + removeOffset);
|
|
} else if (direction == MapDirection.Left || direction == MapDirection.Right) {
|
|
var map = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, connections[i].MapGroup, connections[i].MapNum) {
|
|
allOverworldSprites = allOverworldSprites,
|
|
BerryInfo = BerryInfo,
|
|
};
|
|
var removeHeight = map.pixelHeight / 16;
|
|
var removeOffset = connections[i].Offset;
|
|
foreach (int j in removeHeight.Range()) availableSpace.Remove(j + removeOffset);
|
|
} else if (direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) {
|
|
// can't dive or emerge to a map that already has a dive/emerge
|
|
var map = AllMapsModel.Create(model, tokenFactory)[connections[i].MapGroup][connections[i].MapNum];
|
|
if (map.Connections.Any(c => c.Direction.IsAny(MapDirection.Emerge, MapDirection.Dive))) return null;
|
|
}
|
|
}
|
|
|
|
if (direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) {
|
|
var layout = AllMapsModel.Create(model, tokenFactory)[group][map].Layout;
|
|
return new ConnectionInfo(layout.Width, layout.Height, direction);
|
|
}
|
|
|
|
// find the longest stretch of available space
|
|
var longestSpanLength = 0;
|
|
var longestSpanStart = -1;
|
|
var spanLength = 0;
|
|
var spanStart = -1;
|
|
for (int j = 0; j < availableSpace.Count; j++) {
|
|
if (spanStart == -1) {
|
|
(spanStart, spanLength) = (j, 1);
|
|
} else if (availableSpace[j - 1] + 1 == availableSpace[j]) {
|
|
spanLength++;
|
|
} else {
|
|
if (spanLength > longestSpanLength) (longestSpanStart, longestSpanLength) = (spanStart, spanLength);
|
|
(spanStart, spanLength) = (j, 1);
|
|
}
|
|
}
|
|
|
|
if (spanLength > longestSpanLength) (longestSpanStart, longestSpanLength) = (spanStart, spanLength);
|
|
|
|
// if a long space is availabe, we can connect to it
|
|
// otherwise, we could technically connect to an edge
|
|
if (longestSpanLength > 3) {
|
|
// we can make a map here of width/height longestSpanLength
|
|
// and the offset is availableSpace[longestSpanStart]
|
|
return new ConnectionInfo(longestSpanLength, availableSpace[longestSpanStart], direction);
|
|
} else if (availableSpace.Contains(0)) {
|
|
// we can make a map here of width/height 4
|
|
// and the offset is -3
|
|
return new ConnectionInfo(4, -3, direction);
|
|
} else if (availableSpace.Contains(dimensionLength - 1)) {
|
|
// we can make a map here of width/height 4
|
|
// and the offset is dimensionLength-1
|
|
return new ConnectionInfo(4, dimensionLength - 1, direction);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private void WritePointerAndSource(ModelDelta token, int source, int destination) {
|
|
model.WritePointer(token, source, destination);
|
|
model.ObserveRunWritten(token, NoInfoRun.FromPointer(model, source));
|
|
}
|
|
|
|
private void GotoAddress(int address) {
|
|
if (model.GetNextRun(address).Start > address) {
|
|
viewPort.Tools.SelectedTool = viewPort.Tools.CodeTool;
|
|
viewPort.Tools.CodeTool.Mode = CodeMode.Script;
|
|
}
|
|
viewPort.Goto.Execute(address);
|
|
}
|
|
|
|
private void HandleBlocksChanged(object sender, byte[][] blocks) {
|
|
var layout = GetLayout();
|
|
var blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1"));
|
|
var blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2"));
|
|
BlockmapRun.WriteBlocks(tokenFactory(), blockModel1, blockModel2, blocks);
|
|
viewPort.ChangeHistory.ChangeCompleted();
|
|
RequestClearMapCaches.Raise(this);
|
|
}
|
|
|
|
private void HandleBorderChanged(object sender, EventArgs e) {
|
|
blocks = null;
|
|
lock (blockRenders) {
|
|
blockRenders.Clear();
|
|
}
|
|
blockPixels = null;
|
|
borderBlock = null;
|
|
ClearPixelCache();
|
|
NotifyPropertiesChanged(nameof(BlockPixels), nameof(BlockRenders), nameof(BorderBlock));
|
|
}
|
|
|
|
private void HandleBlockAttributesChanged(object sender, byte[][] attributes) {
|
|
var layout = GetLayout();
|
|
var blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1"));
|
|
var blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2"));
|
|
BlockmapRun.WriteBlockAttributes(tokenFactory(), blockModel1, blockModel2, attributes);
|
|
}
|
|
|
|
private void HandleAutoscrollTiles(object sender, EventArgs e) => AutoscrollTiles.Raise(this);
|
|
|
|
private void HandleEventDataMoved(object sender, DataMovedEventArgs e) => InformRepoint(e);
|
|
|
|
public static string MapIDToText(IDataModel model, int id) {
|
|
var group = id / 1000;
|
|
var map = id % 1000;
|
|
return MapIDToText(model, group, map);
|
|
}
|
|
|
|
public static string MapIDToText(IDataModel model, int group, int map){
|
|
var offset = model.IsFRLG() ? 0x58 : 0;
|
|
|
|
var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start);
|
|
var bank = mapBanks[group].GetSubTable("maps");
|
|
if (bank == null) return $"{group}-{map}";
|
|
if (bank.Count <= map) return $"{group}-{map}";
|
|
var mapTable = bank[map]?.GetSubTable("map");
|
|
if (mapTable == null) return $"{group}-{map}";
|
|
if (!mapTable[0].HasField("regionSectionID")) return $"{group}-{map}";
|
|
var key = mapTable[0].GetValue("regionSectionID") - offset;
|
|
|
|
var names = model.GetTableModel(HardcodeTablesModel.MapNameTable);
|
|
var name = names == null ? string.Empty : names[key].GetStringValue("name");
|
|
name = SanitizeName(name);
|
|
|
|
return $"{group}-{map} ({name})";
|
|
}
|
|
|
|
#endregion
|
|
|
|
/*
|
|
ruby: data.maps.banks, layout<[
|
|
width:: height:: borderblock<[border:|h]4> blockmap<`blm`>
|
|
blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1>
|
|
blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1>
|
|
events<[e1 e2 e3 e4 ee1<> ee2<> ee3<> ee4<>]1>
|
|
mapscripts<[type. pointer<>]!00>
|
|
connections<>
|
|
music: layoutID: regionSectionID. cave. weather. mapType. padding. escapeRope. flags. battleType.
|
|
|
|
firered: data.maps.banks, layout<[
|
|
width:: height:: borderblock<>
|
|
blockmap<`blm`>
|
|
blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> animation<> attributes<>]1>
|
|
blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> animation<> attributes<>]1>
|
|
borderwidth. borderheight. unused:]1>
|
|
events<[objectCount. warpCount. scriptCount. signpostCount.
|
|
objects<[id. graphics. kind: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/objectCount>
|
|
warps<[x:500 y:500 elevation. warpID. map. bank.]/warps>
|
|
scripts<[x:500 y:500 elevation: trigger: index:: script<`xse`>]/scriptCount>
|
|
signposts<[x:500 y:500 elevation. kind. unused: arg::|h]/signposts>]1>
|
|
mapscripts<[type. pointer<>]!00>
|
|
connections<[count:: connections<[direction:: offset:: mapGroup. mapNum. unused:]/count>]>
|
|
music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags.|t|allowEscaping.|allowRunning.|showMapName::: floorNum. battleType.
|
|
|
|
emerald: data.maps.banks, layout<[width:: height:: borderblock<[border:|h]4> blockmap<`blm`>
|
|
blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1>
|
|
blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1>
|
|
events<[objects. warps. scripts. signposts.
|
|
objectP<[id. graphics. unused: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/objects>
|
|
warpP<[x:500 y:500 elevation. warpID. map. bank.]/warps>
|
|
scriptP<[x:500 y:500 elevation: trigger: index: unused: script<`xse`>]/scripts>
|
|
signpostP<[x:500 y:500 elevation. kind. unused: arg::|h]/signposts>]1>
|
|
mapscripts<[type. pointer<>]!00>
|
|
connections<>
|
|
music: layoutID: regionSectionID. cave. weather. mapType. padding: flags.|t|allowCycling.|allowEscaping.|allowRunning.|showMapName::. battleType.
|
|
*/
|
|
}
|
|
|
|
public class EventSelector : ViewModelCore {
|
|
private bool isSelected;
|
|
public bool IsSelected { get => isSelected; set => Set(ref isSelected, value); }
|
|
|
|
private int index;
|
|
public int Index { get => index; set => Set(ref index, value); }
|
|
|
|
public void Select() => IsSelected = true;
|
|
}
|
|
|
|
public record Border(int North, int East, int South, int West);
|
|
|
|
public record ConnectionInfo(int Size, int Offset, MapDirection Direction) {
|
|
public const string SingleConnectionContent = "direction:: offset:: mapGroup. mapNum. unused:";
|
|
public const string SingleConnectionLength = "/count";
|
|
public static readonly string SingleConnectionFormat = $"[{SingleConnectionContent}]{SingleConnectionLength}";
|
|
public static readonly string ConnectionTableContent = $"count:: connections<{SingleConnectionFormat}>";
|
|
public MapDirection OppositeDirection => Direction.Reverse();
|
|
}
|
|
|
|
public record BerryInfo(IDictionary<int, BerrySpot> BerryMap, ObservableCollection<string> BerryOptions);
|
|
|
|
public class ConnectionModel {
|
|
private readonly ModelArrayElement connection;
|
|
private readonly int sourceGroup, sourceMap;
|
|
public IDataModel Model => connection.Model;
|
|
public Func<ModelDelta> Tokens => () => connection.Token;
|
|
public ConnectionModel(ModelArrayElement connection, int sourceGroup, int sourceMap) => (this.connection, this.sourceGroup, this.sourceMap) = (connection, sourceGroup, sourceMap);
|
|
|
|
public MapDirection Direction {
|
|
get => (MapDirection)connection.GetValue("direction");
|
|
set => connection.SetValue("direction", (int)value);
|
|
}
|
|
|
|
public ITableRun Table => connection.Table;
|
|
|
|
public int Offset {
|
|
get => connection.GetValue("offset");
|
|
set => connection.SetValue("offset", value);
|
|
}
|
|
|
|
public int MapGroup {
|
|
get => connection.GetValue("mapGroup");
|
|
set => connection.SetValue("mapGroup", value);
|
|
}
|
|
|
|
public int MapNum {
|
|
get => connection.GetValue("mapNum");
|
|
set => connection.SetValue("mapNum", value);
|
|
}
|
|
|
|
public ConnectionModel GetInverse() {
|
|
var direction = Direction.Reverse();
|
|
var map = BlockMapViewModel.GetMapModel(Model, MapGroup, MapNum, Tokens);
|
|
var neighbors = BlockMapViewModel.GetConnections(map, MapGroup, MapNum);
|
|
return neighbors.FirstOrDefault(c => c.MapGroup == sourceGroup && c.MapNum == sourceMap && c.Direction == direction);
|
|
}
|
|
|
|
public void Clear(IDataModel model, ModelDelta token) {
|
|
token.ChangeData(model, connection.Start, connection.Length.Range(i => (byte)0xFF).ToList());
|
|
}
|
|
}
|
|
|
|
public class EventGroupModel {
|
|
private readonly ModelArrayElement events;
|
|
|
|
public event EventHandler<DataMovedEventArgs> DataMoved;
|
|
|
|
public EventGroupModel(ScriptParser parser, Action<int> gotoAddress, ModelArrayElement events, IReadOnlyList<IPixelViewModel> ows, IPixelViewModel defaultOW, BerryInfo berries, int bank, int map) {
|
|
this.events = events;
|
|
|
|
var objectCount = events.GetValue("objectCount");
|
|
var objects = events.GetSubTable("objects");
|
|
var objectList = new List<ObjectEventViewModel>();
|
|
if (objects != null) {
|
|
for (int i = 0; i < objectCount; i++) {
|
|
var newEvent = new ObjectEventViewModel(parser, gotoAddress, objects[i], ows, defaultOW, berries);
|
|
newEvent.DataMoved += (sender, e) => DataMoved.Raise(this, e);
|
|
objectList.Add(newEvent);
|
|
}
|
|
}
|
|
Objects = objectList;
|
|
|
|
var warpCount = events.GetValue("warpCount");
|
|
var warps = events.GetSubTable("warps");
|
|
var warpList = new List<WarpEventViewModel>();
|
|
if (warps != null) {
|
|
for (int i = 0; i < warpCount; i++) warpList.Add(new WarpEventViewModel(warps[i]));
|
|
}
|
|
Warps = warpList;
|
|
|
|
var scriptCount = events.GetValue("scriptCount");
|
|
var scripts = events.GetSubTable("scripts");
|
|
var scriptList = new List<ScriptEventViewModel>();
|
|
if (scripts != null) {
|
|
for (int i = 0; i < scriptCount; i++) scriptList.Add(new ScriptEventViewModel(gotoAddress, scripts[i]));
|
|
}
|
|
Scripts = scriptList;
|
|
|
|
var signpostCount = events.GetValue("signpostCount");
|
|
var signposts = events.GetSubTable("signposts");
|
|
var signpostList = new List<SignpostEventViewModel>();
|
|
if (signposts != null) {
|
|
for (int i = 0; i < signpostCount; i++) {
|
|
var newEvent = new SignpostEventViewModel(signposts[i], gotoAddress);
|
|
newEvent.DataMoved += (sender, e) => DataMoved.Raise(this, e);
|
|
signpostList.Add(newEvent);
|
|
}
|
|
}
|
|
Signposts = signpostList;
|
|
|
|
var flyList = new List<FlyEventViewModel>();
|
|
foreach (var flyEvent in FlyEventViewModel.Create(events.Model, bank, map, () => events.Token)) {
|
|
if (flyEvent.Valid) flyList.Add(flyEvent);
|
|
}
|
|
FlyEvents = flyList;
|
|
}
|
|
|
|
public IReadOnlyList<ObjectEventViewModel> Objects { get; }
|
|
public IReadOnlyList<WarpEventViewModel> Warps { get; }
|
|
public IReadOnlyList<ScriptEventViewModel> Scripts { get; }
|
|
public IReadOnlyList<SignpostEventViewModel> Signposts { get; }
|
|
public IReadOnlyList<FlyEventViewModel> FlyEvents { get; }
|
|
|
|
/*
|
|
* events<[objectCount. warpCount. scriptCount. signpostCount.
|
|
objects<[id. graphics. unused: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/objectCount>
|
|
warps<[x:500 y:500 elevation. warpID. map. bank.]/warps>
|
|
scripts<[x:500 y:500 elevation: trigger: index:: script<`xse`>]/scriptCount>
|
|
signposts<[x:500 y:500 elevation. kind. unused: arg::|h]/signposts>]1>
|
|
*/
|
|
}
|
|
|
|
public enum MapDirection {
|
|
None = 0,
|
|
Down = 1,
|
|
Up = 2,
|
|
Left = 3,
|
|
Right = 4,
|
|
Dive = 5,
|
|
Emerge = 6,
|
|
}
|
|
|
|
public static class MapDirectionExtensions {
|
|
public static MapDirection Reverse(this MapDirection direction) => direction switch {
|
|
MapDirection.Up => MapDirection.Down,
|
|
MapDirection.Down => MapDirection.Up,
|
|
MapDirection.Left => MapDirection.Right,
|
|
MapDirection.Right => MapDirection.Left,
|
|
MapDirection.Dive => MapDirection.Emerge,
|
|
MapDirection.Emerge => MapDirection.Dive,
|
|
_ => throw new NotImplementedException(),
|
|
};
|
|
}
|
|
|
|
public enum ZoomDirection {
|
|
None = 0,
|
|
Shrink = 1,
|
|
Enlarge = 2,
|
|
}
|
|
}
|