mirror of
https://github.com/haven1433/HexManiacAdvance.git
synced 2026-05-16 09:17:05 -05:00
2360 lines
112 KiB
C#
2360 lines
112 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 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);
|
|
var originalBlockValue = blockValue;
|
|
if (block >= 0) blockValue = (blockValue & 0xFC00) + block;
|
|
if (collision >= 0) blockValue = (blockValue & 0x3FF) + (collision << 10);
|
|
if (blockValue != originalBlockValue) {
|
|
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 change = new Point(lastDrawVal, (collisionIndex << 10) | blockIndex);
|
|
lock (pixelWriteLock) {
|
|
PaintBlock(token, new(xx - 1, yy), size, start, change);
|
|
PaintBlock(token, new(xx + 1, yy), size, start, change);
|
|
PaintBlock(token, new(xx, yy - 1), size, start, change);
|
|
PaintBlock(token, new(xx, yy + 1), size, start, change);
|
|
}
|
|
ClearPixelCache();
|
|
}
|
|
|
|
private void PaintBlock(ModelDelta token, Point p, Point size, int start, Point change) {
|
|
if (change.X == change.Y) 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) != change.X) return;
|
|
model.WriteMultiByteValue(address, 2, token, change.Y);
|
|
PaintBlock(token, p + new Point(-1, 0), size, start, change);
|
|
PaintBlock(token, p + new Point(1, 0), size, start, change);
|
|
PaintBlock(token, p + new Point(0, -1), size, start, change);
|
|
PaintBlock(token, p + new Point(0, 1), size, start, change);
|
|
}
|
|
|
|
#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) {
|
|
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);
|
|
this.blocks = null;
|
|
lock (blockRenders) {
|
|
blockRenders.Clear();
|
|
}
|
|
blockPixels = null;
|
|
ClearPixelCache();
|
|
NotifyPropertiesChanged(nameof(BlockPixels), nameof(BlockRenders));
|
|
viewPort.ChangeHistory.ChangeCompleted();
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|