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 tokenFactory; private readonly int group, map; private int PrimaryTiles { get; } private int PrimaryBlocks { get; } private int TotalBlocks => 1024; private int PrimaryPalettes { get; } // 7 private int zIndex; public int ZIndex { get => zIndex; set => Set(ref zIndex, value); } public event EventHandler RequestClearMapCaches; public IEditableViewPort ViewPort => viewPort; #region SelectedEvent private IEventViewModel selectedEvent; public IEventViewModel SelectedEvent { get => selectedEvent; set { var oldValue = selectedEvent; selectedEvent = value; NotifyPropertyChanged(); HandleSelectedEventChanged(oldValue); } } public ObservableCollection 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> { 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 blockRenders = new(); // one image per block private IReadOnlyList eventRenders; public IReadOnlyList 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 availableNames; public ObservableCollection 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 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(); 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 GetNeighbors(MapDirection direction) { var list = new List(); 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(); 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(); 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>(); 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(); var warpIsBottomSquareForIndex = new List(); // for each prototype, create an image that represents what that map prototype would look like var images = new List(); 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 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(); 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 /// /// Gets the block index and collision index. /// 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; /// /// If collisionIndex is not valid, it's ignored. /// If blockIndex is not valid, it's ignored. /// 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 { 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 { 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 futureToken, int block, int collision, int x, int y, int w, int h, bool refreshScreen) { var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var start = layout.GetAddress("blockmap"); int changeCount = 0; lock (pixelWriteLock) { for (int xx = 0; xx < w; xx++) { for (int yy = 0; yy < h; yy++) { if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue; var address = start + ((yy + y) * width + xx + x) * 2; // var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)]; var blockValue = model.ReadMultiByteValue(address, 2); lastDrawVal = blockValue; if (block >= 0) blockValue = (blockValue & 0xFC00) + block; if (collision >= 0) blockValue = (blockValue & 0x3FF) + (collision << 10); if (blockValue != lastDrawVal) { model.WriteMultiByteValue(address, 2, futureToken(), blockValue); changeCount++; } } } } if (changeCount > 0 && refreshScreen) ClearPixelCache(); } public void RepeatBlocks(Func futureToken, int[,] blockValues, int x, int y, int w, int h, bool refreshScreen) { var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var start = layout.GetAddress("blockmap"); int changeCount = 0; lock (pixelWriteLock) { for (int xx = 0; xx < w; xx++) { for (int yy = 0; yy < h; yy++) { if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue; var address = start + ((yy + y) * width + xx + x) * 2; var block = blockValues[xx % blockValues.GetLength(0), yy % blockValues.GetLength(1)]; if (model.ReadMultiByteValue(address, 2) != block) { model.WriteMultiByteValue(address, 2, futureToken(), block); changeCount++; } } } } if (changeCount > 0 && refreshScreen) ClearPixelCache(); } public int[,] ReadRectangle(int x, int y, int w, int h) { var results = new int[w, h]; var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var start = layout.GetAddress("blockmap"); for (int xx = 0; xx < w; xx++) { for (int yy = 0; yy < h; yy++) { if (x + xx < 0 || y + yy < 0 || x + xx >= width || y + yy >= height) continue; var address = start + ((yy + y) * width + xx + x) * 2; results[xx, yy] = model.ReadMultiByteValue(address, 2); } } return results; } public void PaintBlock(ModelDelta token, int blockIndex, int collisionIndex, double x, double y) { if (blockIndex == -1) return; (x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale); (x, y) = (x / 16, y / 16); var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var border = GetBorderThickness(layout); var (xx, yy) = ((int)x - border.West, (int)y - border.North); if (xx < 0 || yy < 0 || xx > width || yy > height) return; var start = layout.GetAddress("blockmap"); var size = new Point(width, height); if (collisionIndex < 0) collisionIndex = lastDrawVal >> 10; var (before, after) = (lastDrawVal, (collisionIndex << 10) | blockIndex); lock (pixelWriteLock) { PaintBlock(token, new(xx - 1, yy), size, start, before, after); PaintBlock(token, new(xx + 1, yy), size, start, before, after); PaintBlock(token, new(xx, yy - 1), size, start, before, after); PaintBlock(token, new(xx, yy + 1), size, start, before, after); } ClearPixelCache(); } private void PaintBlock(ModelDelta token, Point p, Point size, int start, int before, int after) { if (before == after) return; if (p.X < 0 || p.Y < 0 || p.X >= size.X || p.Y >= size.Y) return; var address = start + (p.Y * size.X + p.X) * 2; if (model.ReadMultiByteValue(address, 2) != before) return; model.WriteMultiByteValue(address, 2, token, after); PaintBlock(token, p + new Point(-1, 0), size, start, before, after); PaintBlock(token, p + new Point(1, 0), size, start, before, after); PaintBlock(token, p + new Point(0, -1), size, start, before, after); PaintBlock(token, p + new Point(0, 1), size, start, before, after); } public void PaintBlockBag(ModelDelta token, List blockIndexes, int collisionIndex, double x, double y) { if (blockIndexes.Count < 1) return; (x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale); (x, y) = (x / 16, y / 16); var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var border = GetBorderThickness(layout); var (xx, yy) = ((int)x - border.West, (int)y - border.North); if (xx < 0 || yy < 0 || xx > width || yy > height) return; var start = layout.GetAddress("blockmap"); var complete = new HashSet { new(xx, yy) }; var check = new List { new(xx - 1, yy), new(xx + 1, yy), new(xx, yy - 1), new(xx, yy + 1) }; var targets = blockIndexes.Select(bi => (collisionIndex << 10) | bi).ToList(); var rnd = new Random(); lock (pixelWriteLock) { while (check.Count > 0) { var p = check[check.Count - 1]; check.RemoveAt(check.Count - 1); if (p.X < 0 || p.Y < 0 || p.X >= width || p.Y >= height) continue; if (complete.Contains(p)) continue; complete.Add(p); var address = start + (p.Y * width + p.X) * 2; if (model.ReadMultiByteValue(address, 2) != lastDrawVal) continue; model.WriteMultiByteValue(address, 2, token, rnd.From(targets)); check.AddRange(new Point[] { new(p.X - 1, p.Y), new(p.X + 1, p.Y), new(p.X, p.Y - 1), new(p.X, p.Y + 1) }); } } ClearPixelCache(); } public void PaintWaveFunction(ModelDelta token, double x, double y, Func wave) { (x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale); (x, y) = (x / 16, y / 16); var layout = GetLayout(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var border = GetBorderThickness(layout); var (xx, yy) = ((int)x - border.West, (int)y - border.North); if (xx < 0 || yy < 0 || xx > width || yy > height) return; var start = layout.GetAddress("blockmap"); // first pass: set all the effected spaces to 0 so they won't count var toDraw = new Queue(); toDraw.Enqueue(new(xx, yy)); var drawn = new List(); lock (pixelWriteLock) { while (toDraw.Count > 0) { var p = toDraw.Dequeue(); if (drawn.Contains(p)) continue; var address = start + (p.Y * width + (p.X - 1)) * 2; if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X - 1, p.Y)); address = start + (p.Y * width + (p.X + 1)) * 2; if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X + 1, p.Y)); address = start + ((p.Y - 1) * width + p.X) * 2; if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y - 1)); address = start + ((p.Y + 1) * width + p.X) * 2; if (p.X - 1 > 0 && model.ReadMultiByteValue(address, 2) == lastDrawVal) toDraw.Enqueue(new(p.X, p.Y + 1)); address = start + (p.Y * width + p.X) * 2; model.WriteMultiByteValue(address, 2, token, 0); drawn.Add(p); } // second pass: wave-fill in the reverse order (outside in) drawn.Reverse(); foreach (var p in drawn) { var targetVal = wave(p.X, p.Y); var address = start + (p.Y * width + p.X) * 2; model.WriteMultiByteValue(address, 2, token, targetVal); } } ClearPixelCache(); } #endregion #region Events public void UpdateEventLocation(IEventViewModel ev, double x, double y) { (lastDrawX, lastDrawY) = (-1, -1); var layout = GetLayout(); var border = GetBorderThickness(layout); if (border == null) return; (x, y) = ((x - leftEdge) / spriteScale, (y - topEdge) / spriteScale); var (xx, yy) = ((int)(x / 16) - border.West, (int)(y / 16) - border.North); if (ev.X == xx && ev.Y == yy) return; if (xx < 0 || yy < 0) return; var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); if (xx >= width || yy >= height) return; ev.X = xx; ev.Y = yy; SelectedEvent = ev; ClearPixelCache(); } public IEventViewModel EventUnderCursor(double x, double y, bool autoSelect = true) { var layout = GetLayout(); var border = GetBorderThickness(layout); var tileX = (int)((x - LeftEdge) / SpriteScale / 16) - border.West; var tileY = (int)((y - TopEdge) / SpriteScale / 16) - border.North; IEventViewModel last = null; foreach (var e in GetEvents()) { if (e.X == tileX && e.Y == tileY) last = e; } if (autoSelect && SelectedEvent != last) { SelectedEvent = last; ClearPixelCache(); } return last; } public IPixelViewModel AutoCrop(int warpID) { if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model); if (defaultOverworldSprite == null) defaultOverworldSprite = GetDefaultOW(model); const int SizeX = 7, SizeY = 7; var map = GetMapModel(); if (map == null) return null; var layout = GetLayout(map); if (layout == null) return null; var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var events = new EventGroupModel(ViewPort.Tools.CodeTool.ScriptParser, GotoAddress, map.GetSubTable("events")[0], allOverworldSprites, defaultOverworldSprite, BerryInfo, group, this.map); if (events.Warps.Count <= warpID) return null; var warp = events.Warps[warpID]; var startX = warp.X - SizeX / 2; var startY = warp.Y - SizeY / 2; while (startX < 0) startX += 1; while (startY < 0) startY += 1; while (startX + SizeX > width) startX--; while (startY + SizeY > height) startY--; return ReadonlyPixelViewModel.Crop(this, startX * 16, startY * 16, SizeX * 16, SizeY * 16); } public void DeselectEvent() { if (selectedEvent == null) return; SelectedEvent = null; ClearPixelCache(); } #endregion #region Connections public IEnumerable 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 GetConnectionCommands(IReadOnlyList connections, MapDirection direction) { var toRemove = new List(); 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("Create New Map", ConnectNewMap) { Parameter = info }; yield return new MenuCommand("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("Create New Map", ConnectNewMap) { Parameter = info }; yield return new MenuCommand("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("Create New Map", ConnectNewMap) { Parameter = info }; yield return new MenuCommand("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>("Remove Connections", RemoveConnections) { Parameter = toRemove }; } } #endregion #region Work Methods private IEnumerable 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(); 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 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 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(); var events = GetEvents(); foreach (var obj in events) { obj.Render(model); list.Add(obj); } eventRenders = list; } private void FillMapPixelData() { var layout = GetLayout(); if (layout == null) return; lock (blockRenders) { if (blockRenders.Count == 0) RefreshBlockRenderCache(layout); } if (borderBlock == null) RefreshBorderRender(); var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); var border = GetBorderThickness(layout); var start = layout.GetAddress("blockmap"); var canvas = new CanvasPixelViewModel(pixelWidth, pixelHeight); var (borderWidth, borderHeight) = (borderBlock.PixelWidth / 16, borderBlock.PixelHeight / 16); for (int y = 0; y < height + border.North + border.South; y++) { for (int x = 0; x < width + border.West + border.East; x++) { if (y < border.North || x < border.West || y >= border.North + height || x >= border.West + width) { var (xEdge, yEdge) = (x - border.West - width, y - border.North - height); var (rightEdge, bottomEdge) = (xEdge >= 0, yEdge >= 0); // top/left if (!rightEdge && !bottomEdge && x % borderWidth == 0 && y % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16); // right edge if (rightEdge && !bottomEdge && xEdge % borderWidth == 0 && y % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16); // bottom edge if (!rightEdge && bottomEdge && x % borderWidth == 0 && yEdge % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16); // bottom right corner if (rightEdge && bottomEdge && xEdge % borderWidth == 0 && yEdge % borderHeight == 0) canvas.Draw(borderBlock, x * 16, y * 16); continue; } var data = model.ReadMultiByteValue(start + ((y - border.North) * width + x - border.West) * 2, 2); var collision = data >> 10; data &= 0x3FF; if (blockRenders.Count > data) canvas.Draw(blockRenders[data], x * 16, y * 16); if (collision == collisionHighlight) HighlightCollision(canvas.PixelData, x * 16, y * 16); if (collisionHighlight == -1 && selectedEvent is ObjectEventViewModel obj && obj.ShouldHighlight(x - border.West, y - border.North)) { HighlightCollision(canvas.PixelData, x * 16, y * 16); } } } // draw the box for the selected event var gray = UncompressedPaletteColor.Pack(6, 6, 6); if (selectedEvent != null && selectedEvent.X >= 0 && selectedEvent.X < width && selectedEvent.Y >= 0 && SelectedEvent.Y < height) { canvas.DrawBox((selectedEvent.X + border.West) * 16, (selectedEvent.Y + border.North) * 16, 16, gray); } // now draw the events on top if (eventRenders == null) RefreshMapEvents(); if (eventRenders != null) { foreach (var obj in eventRenders) { if (obj.EventRender != null) { var (x, y) = ((obj.X + border.West) * 16 + obj.LeftOffset, (obj.Y + border.North) * 16 + obj.TopOffset); canvas.Draw(obj.EventRender, x, y); } } } // finally, draw a one-pixel border around the entire map (but not the border blocks) if (isSelected) { var (borderW, borderH) = (border.West + border.East, border.North + border.South); canvas.DarkenRect(border.West * 16, border.North * 16, pixelWidth - borderW * 16, pixelHeight - borderH * 16, 12); } pixelData = canvas.PixelData; } private void HighlightCollision(short[] pixelData, int x, int y) { void Transform(int xx, int yy) { var p = (y + yy) * PixelWidth + x + xx; pixelData[p] = CanvasPixelViewModel.Darken(pixelData[p], 8); } for (int i = 0; i < 15; i++) { Transform(i, 0); Transform(15 - i, 15); Transform(0, 15 - i); Transform(15, i); } } public const int BlocksPerRow = 8; private void FillBlockPixelData() { var layout = GetLayout(); lock (blockRenders) { if (blockRenders.Count == 0) RefreshBlockRenderCache(layout); } var blockHeight = (int)Math.Ceiling((double)blockRenders.Count / BlocksPerRow); var canvas = new CanvasPixelViewModel(BlocksPerRow * 16, blockHeight * 16) { SpriteScale = 2 }; for (int y = 0; y < blockHeight; y++) { for (int x = 0; x < BlocksPerRow; x++) { if (blockRenders.Count <= y * BlocksPerRow + x) break; canvas.Draw(blockRenders[y * BlocksPerRow + x], x * 16, y * 16); } } blockPixels = canvas; } private void RefreshBorderRender(ModelArrayElement layout = null) { if (layout == null) layout = GetLayout(); lock (blockRenders) { if (blockRenders.Count == 0) RefreshBlockRenderCache(layout); } var width = layout.HasField("borderwidth") ? layout.GetValue("borderwidth") : 2; var height = layout.HasField("borderheight") ? layout.GetValue("borderheight") : 2; var start = layout.GetAddress("borderblock"); var canvas = new CanvasPixelViewModel(width * 16, height * 16); for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { var data = model.ReadMultiByteValue(start + (y * width + x) * 2, 2); data &= 0x3FF; canvas.Draw(blockRenders[data], x * 16, y * 16); } } BorderBlock = canvas; } private ModelArrayElement GetMapModel() => GetMapModel(model, group, map, tokenFactory); public static ModelArrayElement GetMapModel(IDataModel model, int group, int map, Func 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 GetConnections() { var map = GetMapModel(model, group, this.map, tokenFactory); return GetConnections(map, group, this.map); } public static IReadOnlyList GetConnections(ModelArrayElement map, int bankNum, int mapNum) { if (map == null) return null; var connectionsAndCountTable = map.GetSubTable("connections"); var list = new List(); 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 allOverworldSprites; public IReadOnlyList 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 RenderOWs(IDataModel model) { var list = new List(); 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 GetEvents() { if (allOverworldSprites == null) allOverworldSprites = RenderOWs(model); if (defaultOverworldSprite == null) defaultOverworldSprite = GetDefaultOW(model); var map = GetMapModel(); var results = new List(); var eventsTable = map.GetSubTable("events"); if (eventsTable == null) return results; var eventElements = eventsTable[0]; if (eventElements == null) return results; var events = new EventGroupModel(ViewPort.Tools.CodeTool.ScriptParser, GotoAddress, eventElements, allOverworldSprites, defaultOverworldSprite, BerryInfo, group, this.map); events.DataMoved += HandleEventDataMoved; results.AddRange(events.Objects); results.AddRange(events.Warps); results.AddRange(events.Scripts); results.AddRange(events.Signposts); results.AddRange(events.FlyEvents); return results; } public Border GetBorderThickness(ModelArrayElement layout = null) { if (!includeBorders) return new(0, 0, 0, 0); var connections = GetConnections(); if (connections == null) return null; if (layout == null) layout = GetLayout(); var width = layout.HasField("borderwidth") ? layout.GetValue("borderwidth") : 2; var height = layout.HasField("borderheight") ? layout.GetValue("borderheight") : 2; var (east, west) = (width, width); var (north, south) = (height, height); var directions = connections.Select(c => c.Direction).ToList(); if (directions.Contains(MapDirection.Down)) south = 0; if (directions.Contains(MapDirection.Up)) north = 0; if (directions.Contains(MapDirection.Left)) west = 0; if (directions.Contains(MapDirection.Right)) east = 0; return new(north, east, south, west); } private ConnectionInfo CanConnect(MapDirection direction) { var connections = GetConnections(); var (width, height) = (pixelWidth / 16, pixelHeight / 16); var dimensionLength = (direction switch { MapDirection.Up => width, MapDirection.Down => width, MapDirection.Left => height, MapDirection.Right => height, MapDirection.Dive => 0, MapDirection.Emerge => 0, _ => throw new NotImplementedException(), }); var availableSpace = dimensionLength.Range().ToList(); // can't add a connection where there already is one for (int i = 0; i < (connections?.Count ?? 0); i++) { if (connections[i].Direction != direction) continue; if (direction == MapDirection.Up || direction == MapDirection.Down) { var map = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites, BerryInfo = BerryInfo, }; var removeWidth = map.pixelWidth / 16; var removeOffset = connections[i].Offset; foreach (int j in removeWidth.Range()) availableSpace.Remove(j + removeOffset); } else if (direction == MapDirection.Left || direction == MapDirection.Right) { var map = new BlockMapViewModel(fileSystem, tutorials, viewPort, format, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites, BerryInfo = BerryInfo, }; var removeHeight = map.pixelHeight / 16; var removeOffset = connections[i].Offset; foreach (int j in removeHeight.Range()) availableSpace.Remove(j + removeOffset); } else if (direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) { // can't dive or emerge to a map that already has a dive/emerge var map = AllMapsModel.Create(model, tokenFactory)[connections[i].MapGroup][connections[i].MapNum]; if (map.Connections.Any(c => c.Direction.IsAny(MapDirection.Emerge, MapDirection.Dive))) return null; } } if (direction.IsAny(MapDirection.Dive, MapDirection.Emerge)) { var layout = AllMapsModel.Create(model, tokenFactory)[group][map].Layout; return new ConnectionInfo(layout.Width, layout.Height, direction); } // find the longest stretch of available space var longestSpanLength = 0; var longestSpanStart = -1; var spanLength = 0; var spanStart = -1; for (int j = 0; j < availableSpace.Count; j++) { if (spanStart == -1) { (spanStart, spanLength) = (j, 1); } else if (availableSpace[j - 1] + 1 == availableSpace[j]) { spanLength++; } else { if (spanLength > longestSpanLength) (longestSpanStart, longestSpanLength) = (spanStart, spanLength); (spanStart, spanLength) = (j, 1); } } if (spanLength > longestSpanLength) (longestSpanStart, longestSpanLength) = (spanStart, spanLength); // if a long space is availabe, we can connect to it // otherwise, we could technically connect to an edge if (longestSpanLength > 3) { // we can make a map here of width/height longestSpanLength // and the offset is availableSpace[longestSpanStart] return new ConnectionInfo(longestSpanLength, availableSpace[longestSpanStart], direction); } else if (availableSpace.Contains(0)) { // we can make a map here of width/height 4 // and the offset is -3 return new ConnectionInfo(4, -3, direction); } else if (availableSpace.Contains(dimensionLength - 1)) { // we can make a map here of width/height 4 // and the offset is dimensionLength-1 return new ConnectionInfo(4, dimensionLength - 1, direction); } return null; } private void WritePointerAndSource(ModelDelta token, int source, int destination) { model.WritePointer(token, source, destination); model.ObserveRunWritten(token, NoInfoRun.FromPointer(model, source)); } private void GotoAddress(int address) { if (model.GetNextRun(address).Start > address) { viewPort.Tools.SelectedTool = viewPort.Tools.CodeTool; viewPort.Tools.CodeTool.Mode = CodeMode.Script; } viewPort.Goto.Execute(address); } private void HandleBlocksChanged(object sender, byte[][] blocks) { var layout = GetLayout(); var blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1")); var blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2")); BlockmapRun.WriteBlocks(tokenFactory(), blockModel1, blockModel2, blocks); viewPort.ChangeHistory.ChangeCompleted(); RequestClearMapCaches.Raise(this); } private void HandleBorderChanged(object sender, EventArgs e) { blocks = null; lock (blockRenders) { blockRenders.Clear(); } blockPixels = null; borderBlock = null; ClearPixelCache(); NotifyPropertiesChanged(nameof(BlockPixels), nameof(BlockRenders), nameof(BorderBlock)); } private void HandleBlockAttributesChanged(object sender, byte[][] attributes) { var layout = GetLayout(); var blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1")); var blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2")); BlockmapRun.WriteBlockAttributes(tokenFactory(), blockModel1, blockModel2, attributes); } private void HandleAutoscrollTiles(object sender, EventArgs e) => AutoscrollTiles.Raise(this); private void HandleEventDataMoved(object sender, DataMovedEventArgs e) => InformRepoint(e); public static string MapIDToText(IDataModel model, int id) { var group = id / 1000; var map = id % 1000; return MapIDToText(model, group, map); } public static string MapIDToText(IDataModel model, int group, int map){ var offset = model.IsFRLG() ? 0x58 : 0; var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start); var bank = mapBanks[group].GetSubTable("maps"); if (bank == null) return $"{group}-{map}"; if (bank.Count <= map) return $"{group}-{map}"; var mapTable = bank[map]?.GetSubTable("map"); if (mapTable == null) return $"{group}-{map}"; if (!mapTable[0].HasField("regionSectionID")) return $"{group}-{map}"; var key = mapTable[0].GetValue("regionSectionID") - offset; var names = model.GetTableModel(HardcodeTablesModel.MapNameTable); var name = names == null ? string.Empty : names[key].GetStringValue("name"); name = SanitizeName(name); return $"{group}-{map} ({name})"; } #endregion /* ruby: data.maps.banks, layout<[ width:: height:: borderblock<[border:|h]4> blockmap<`blm`> blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1> blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1> events<[e1 e2 e3 e4 ee1<> ee2<> ee3<> ee4<>]1> mapscripts<[type. pointer<>]!00> connections<> music: layoutID: regionSectionID. cave. weather. mapType. padding. escapeRope. flags. battleType. firered: data.maps.banks, layout<[ width:: height:: borderblock<> blockmap<`blm`> blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> animation<> attributes<>]1> blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> animation<> attributes<>]1> borderwidth. borderheight. unused:]1> events<[objectCount. warpCount. scriptCount. signpostCount. objects<[id. graphics. kind: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/objectCount> warps<[x:500 y:500 elevation. warpID. map. bank.]/warps> scripts<[x:500 y:500 elevation: trigger: index:: script<`xse`>]/scriptCount> signposts<[x:500 y:500 elevation. kind. unused: arg::|h]/signposts>]1> mapscripts<[type. pointer<>]!00> connections<[count:: connections<[direction:: offset:: mapGroup. mapNum. unused:]/count>]> music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags.|t|allowEscaping.|allowRunning.|showMapName::: floorNum. battleType. emerald: data.maps.banks, layout<[width:: height:: borderblock<[border:|h]4> blockmap<`blm`> blockdata1<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1> blockdata2<[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1> events<[objects. warps. scripts. signposts. objectP<[id. graphics. unused: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/objects> warpP<[x:500 y:500 elevation. warpID. map. bank.]/warps> scriptP<[x:500 y:500 elevation: trigger: index: unused: script<`xse`>]/scripts> signpostP<[x:500 y:500 elevation. kind. unused: arg::|h]/signposts>]1> mapscripts<[type. pointer<>]!00> connections<> music: layoutID: regionSectionID. cave. weather. mapType. padding: flags.|t|allowCycling.|allowEscaping.|allowRunning.|showMapName::. battleType. */ } public class EventSelector : ViewModelCore { private bool isSelected; public bool IsSelected { get => isSelected; set => Set(ref isSelected, value); } private int index; public int Index { get => index; set => Set(ref index, value); } public void Select() => IsSelected = true; } public record Border(int North, int East, int South, int West); public record ConnectionInfo(int Size, int Offset, MapDirection Direction) { public const string SingleConnectionContent = "direction:: offset:: mapGroup. mapNum. unused:"; public const string SingleConnectionLength = "/count"; public static readonly string SingleConnectionFormat = $"[{SingleConnectionContent}]{SingleConnectionLength}"; public static readonly string ConnectionTableContent = $"count:: connections<{SingleConnectionFormat}>"; public MapDirection OppositeDirection => Direction.Reverse(); } public record BerryInfo(IDictionary BerryMap, ObservableCollection BerryOptions); public class ConnectionModel { private readonly ModelArrayElement connection; private readonly int sourceGroup, sourceMap; public IDataModel Model => connection.Model; public Func 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 DataMoved; public EventGroupModel(ScriptParser parser, Action gotoAddress, ModelArrayElement events, IReadOnlyList 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(); 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(); 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(); 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(); 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(); foreach (var flyEvent in FlyEventViewModel.Create(events.Model, bank, map, () => events.Token)) { if (flyEvent.Valid) flyList.Add(flyEvent); } FlyEvents = flyList; } public IReadOnlyList Objects { get; } public IReadOnlyList Warps { get; } public IReadOnlyList Scripts { get; } public IReadOnlyList Signposts { get; } public IReadOnlyList 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, } }