diff --git a/src/HexManiac.Core/Core/SystemExtensions.cs b/src/HexManiac.Core/Core/SystemExtensions.cs index a5c5d80f..e3fee035 100644 --- a/src/HexManiac.Core/Core/SystemExtensions.cs +++ b/src/HexManiac.Core/Core/SystemExtensions.cs @@ -18,6 +18,7 @@ namespace HavenSoft.HexManiac.Core { public static void RaiseCanExecuteChanged(this StubCommand self) => self?.CanExecuteChanged.Invoke(self, EventArgs.Empty); public static void Raise(this EventHandler handler, object source) => handler?.Invoke(source, EventArgs.Empty); + public static void Raise(this EventHandler handler, object source, T args) => handler?.Invoke(source, args); public static T LimitToRange(this T value, T lower, T upper) where T : IComparable { if (upper.CompareTo(lower) < 0) throw new ArgumentException($"upper value {upper} is less than lower value {lower}"); diff --git a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs index 06f58606..2effa711 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs @@ -639,8 +639,12 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { public bool DestinationDataMatchesPointerFormat(IDataModel owner, ModelDelta token, int source, int destination, IReadOnlyList sourceSegments, int parentIndex) { if (destination == Pointer.NULL) return true; var run = owner.GetNextRun(destination); - if (run.Start < destination && run is not ITableRun) return false; - if (run.Start > destination || (run.Start == destination && (run is NoInfoRun || run is PointerRun))) { + bool possibleFalseRun = false; + if (run.Start < destination && run is not ITableRun) { + if (run is PCSRun || run is PointerRun) possibleFalseRun = true; // this could have been added automatically and is blocking the correct interpretation of the data + else return false; + } + if ((run.Start < destination && possibleFalseRun) || run.Start > destination || (run.Start == destination && (run is NoInfoRun || run is PointerRun))) { // hard case: no format found, so check the data if (destination < 0 || destination >= owner.Count) return false; return Factory.GetStrategy(InnerFormat)?.TryAddFormatAtDestination(owner, token, source, destination, Name, sourceSegments, parentIndex) ?? false; diff --git a/src/HexManiac.Core/Models/Runs/ModelCacheScope.cs b/src/HexManiac.Core/Models/Runs/ModelCacheScope.cs index 7c789759..bf20fabe 100644 --- a/src/HexManiac.Core/Models/Runs/ModelCacheScope.cs +++ b/src/HexManiac.Core/Models/Runs/ModelCacheScope.cs @@ -90,7 +90,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { var banks = new ModelTable(model, bankTable.Start, null, bankTable); for (int i = 0; i < banks.Count; i++) { var maps = banks[i].GetSubTable("maps"); - if (maps == null) return results; + if (maps == null) continue; for (int j = 0; j < maps.Count; j++) { var mapText = "maps." + BlockMapViewModel.MapIDToText(model, i * 1000 + j); results.Add(new(i, j, mapText)); diff --git a/src/HexManiac.Core/Models/Runs/Sprites/BlockmapRun.cs b/src/HexManiac.Core/Models/Runs/Sprites/BlockmapRun.cs index 277f8c84..b7d290f8 100644 --- a/src/HexManiac.Core/Models/Runs/Sprites/BlockmapRun.cs +++ b/src/HexManiac.Core/Models/Runs/Sprites/BlockmapRun.cs @@ -449,7 +449,8 @@ namespace HexManiac.Core.Models.Runs.Sprites { return canvas; } - private void EstimateBlockCount(ref int blockCount, int blockStart, int attributeStart) { + private void EstimateBlockCount(ref int blockCount, int blockStart, int attributeStart) => EstimateBlockCount(model, ref blockCount, blockStart, attributeStart); + public static void EstimateBlockCount(IDataModel model, ref int blockCount, int blockStart, int attributeStart) { var blockLength = 0x10; IFormattedRun run; for (run = model.GetNextRun(blockStart + 1); run.Start < attributeStart; run = model.GetNextRun(run.Start + run.Length)) { diff --git a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs index 0d9a18ee..e7f63ee1 100644 --- a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs +++ b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs @@ -216,7 +216,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } var matchedMaps = viewPort.Model.GetMatchingMaps(currentSelection); - if (matchedMaps.Count == 1) { + if (matchedMaps.Count == 1 || matchedMaps.Any(map => map.Name == currentSelection)) { viewPort?.Goto?.Execute(currentSelection); ControlVisible = false; ShowAutoCompleteOptions = false; diff --git a/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs b/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs index 35b002ea..8e4ed52b 100644 --- a/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Map/BlockMapViewModel.cs @@ -13,11 +13,10 @@ using System.Runtime.CompilerServices; using System.Text; using System.Windows.Input; using static HavenSoft.HexManiac.Core.ViewModels.Map.MapSliderIcons; -using static IronPython.Modules._ast; namespace HavenSoft.HexManiac.Core.ViewModels.Map { public class BlockMapViewModel : ViewModelCore, IPixelViewModel { - + private readonly Format format; private readonly IFileSystem fileSystem; private readonly IEditableViewPort viewPort; private readonly IDataModel model; @@ -387,38 +386,23 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { private MapRepointer mapRepointer; public MapRepointer MapRepointer => mapRepointer; - public BlockMapViewModel(IFileSystem fileSystem, IEditableViewPort viewPort, int group, int map) { + public BlockMapViewModel(IFileSystem fileSystem, IEditableViewPort viewPort, Format format, int group, int map) { + this.format = format; this.fileSystem = fileSystem; this.viewPort = viewPort; this.model = viewPort.Model; this.tokenFactory = () => viewPort.ChangeHistory.CurrentChange; (this.group, this.map) = (group, map); Transparent = -1; - InitTableRef(!model.IsFRLG()); - Header = new(GetMapModel(), tokenFactory); + var mapModel = GetMapModel(); + Header = new(mapModel, tokenFactory); RefreshMapSize(); PrimaryTiles = PrimaryBlocks = model.IsFRLG() ? 640 : 512; PrimaryPalettes = model.IsFRLG() ? 7 : 6; (LeftEdge, TopEdge) = (-PixelWidth / 2, -PixelHeight / 2); - mapRepointer = new MapRepointer(); - } - - private string _bld, _layout, _objects, _warps, _scripts, _signposts, _events, _connections, _header, _map; - private void InitTableRef(bool isRSE) { - _bld = $"[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> animation<> attributes<>]1"; - if (isRSE) _bld = $"[isCompressed. isSecondary. padding: tileset<> pal<`ucp4:0123456789ABCDEF`> block<> attributes<> animation<>]1"; - _layout = $"[width:: height:: borderblock<> blockmap<`blm`> blockdata1<{_bld}> blockdata2<{_bld}> borderwidth. borderheight. unused:]1"; - if (isRSE) _layout = $"[width:: height:: borderblock<> blockmap<`blm`> blockdata1<{_bld}> blockdata2<{_bld}>]1"; - _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"; - _events = $"[objectCount. warpCount. scriptCount. signpostCount. objects<{_objects}> warps<{_warps}> scripts<{_scripts}> signposts<{_signposts}>]1"; - _connections = "[count:: connections<[direction:: offset:: mapGroup. mapNum. unused:]/count>]1"; - _header = "music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags.|t|allowEscaping.|allowRunning.|showMapName::: floorNum. battleType."; - _map = $"[layout<{_layout}> events<{_events}> mapscripts<[type. pointer<>]!00> connections<{_connections}> {_header}]"; + mapRepointer = new MapRepointer(format, fileSystem, model, viewPort.ChangeHistory, MapID); } public IReadOnlyList GetNeighbors(MapDirection direction) { @@ -778,12 +762,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { private void ConnectNewMap(object obj) { var token = tokenFactory(); var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start, tokenFactory); - var enumViewModel = new EnumViewModel(mapBanks.Count.Range(i => i.ToString()).ToArray()); - var option = fileSystem.ShowOptions( - "Pick a group", - "Which map group do you want to add the new map to?", - new[] { new[] { enumViewModel } }, - new VisualOption { Index = 1, Option = "OK", ShortDescription = "Insert New Map" }); + var option = MapRepointer.GetMapBankForNewMap("Which map group do you want to add the new map to?"); if (option == -1) return; var info = (ConnectionInfo)obj; @@ -797,27 +776,19 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { newConnection.Offset = info.Offset; newConnection.Direction = info.Direction; - newConnection.MapGroup = enumViewModel.Choice; + newConnection.MapGroup = option; + + var mapTable = MapRepointer.AddNewMapToBank(option); + + newConnection.MapNum = mapTable.ElementCount - 1; + var address = MapRepointer.CreateNewMap(token); + var layoutStart = MapRepointer.CreateNewLayout(token); + WritePointerAndSource(token, layoutStart + 12, MapRepointer.CreateNewBlockMap(token, info.Size, info.Size)); + WritePointerAndSource(token, address + 0, layoutStart); - ITableRun mapTable; - if (mapBanks.Count == newConnection.MapGroup) { - var newTable = model.RelocateForExpansion(token, mapBanks.Run, mapBanks.Run.Length + mapBanks.Run.ElementLength); - newTable = newTable.Append(token, 1); - model.ObserveRunWritten(token, newTable); - mapBanks = new ModelTable(model, newTable.Start, tokenFactory, newTable); - var tableStart = model.FindFreeSpace(model.FreeSpaceStart, 8); - mapTable = new TableStreamRun(model, tableStart, SortedSpan.One(mapBanks[newConnection.MapGroup].Start), $"[map<{_map}1>]", null, new DynamicStreamStrategy(model, null), 0); - model.UpdateArrayPointer(token, null, null, -1, mapBanks[newConnection.MapGroup].Start, tableStart); - } else { - mapTable = mapBanks[newConnection.MapGroup].GetSubTable("maps").Run; - } - newConnection.MapNum = mapTable.ElementCount; - mapTable = mapTable.Append(token, 1); - model.ObserveRunWritten(token, mapTable); - var address = CreateNewMap(token, info.Size, info.Size); model.UpdateArrayPointer(token, null, null, -1, mapTable.Start + mapTable.Length - 4, address); - var otherMap = new BlockMapViewModel(fileSystem, viewPort, newConnection.MapGroup, newConnection.MapNum) { allOverworldSprites = allOverworldSprites }; + var otherMap = new BlockMapViewModel(fileSystem, viewPort, format, newConnection.MapGroup, newConnection.MapNum) { allOverworldSprites = allOverworldSprites }; otherMap.UpdateLayoutID(); info = new ConnectionInfo(info.Size, -info.Offset, info.OppositeDirection); newConnection = otherMap.AddConnection(info); @@ -841,7 +812,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { var bank = mapBanks[group]; var maps = bank.GetSubTable("maps"); for (int map = 0; map < maps.Count; map++) { - var mapVM = new BlockMapViewModel(fileSystem, viewPort, group, map) { allOverworldSprites = allOverworldSprites }; + var mapVM = new BlockMapViewModel(fileSystem, viewPort, format, group, map) { allOverworldSprites = allOverworldSprites }; var newInfo = mapVM.CanConnect(info.OppositeDirection); if (newInfo != null) options[mapVM.MapID] = newInfo; } @@ -850,11 +821,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { // select which map to add var keys = options.Keys.ToList(); var enumViewModel = new EnumViewModel(keys.Select(key => MapIDToText(model, key)).ToArray()); + var option = fileSystem.ShowOptions( "Pick a group", - "Which map group do you want to add the new map to?", + "Which map do you want to connect to?", new[] { new[] { enumViewModel } }, - new VisualOption { Index = 1, Option = "OK", ShortDescription = "Insert New Map" }); + new VisualOption { Index = 1, Option = "OK", ShortDescription = "Connect Existing Map" }); if (option == -1) return; var choice = keys[enumViewModel.Choice]; @@ -864,7 +836,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { newConnection.MapGroup = choice / 1000; newConnection.MapNum = choice % 1000; - var otherMap = new BlockMapViewModel(fileSystem, viewPort, newConnection.MapGroup, newConnection.MapNum) { allOverworldSprites = allOverworldSprites }; + var otherMap = new BlockMapViewModel(fileSystem, viewPort, format, newConnection.MapGroup, newConnection.MapNum) { allOverworldSprites = allOverworldSprites }; info = options[choice]; newConnection = otherMap.AddConnection(info); newConnection.Offset = info.Offset; @@ -907,7 +879,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { var map = GetMapModel(); var connectionsAndCountTable = map.GetSubTable("connections"); if (connectionsAndCountTable == null) { - var newConnectionsAndCountTable = CreateNewConnections(token); + var newConnectionsAndCountTable = MapRepointer.CreateNewConnections(token); model.UpdateArrayPointer(token, null, null, -1, map.Start + 12, newConnectionsAndCountTable); connectionsAndCountTable = map.GetSubTable("connections"); } @@ -1011,28 +983,6 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { return new ModelArrayElement(model, newRun.Start, newRun.ElementCount - 1, tokenFactory, newRun); } - /// - /// Inserts a new map using the existing borderblocks and block data. - /// Creates event data with 0 events, creates connection data with 0 connections. - /// Copies all the flags from the current map - /// - private int CreateNewMap(ModelDelta token, int width, int height) { - var currentMap = GetMapModel(); - var mapStart = model.FindFreeSpace(model.FreeSpaceStart, 28); - // music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags. floorNum. battleType. - for (int i = 16; i < 28; i++) token.ChangeData(model, mapStart + i, model[currentMap.Start + i]); - - WritePointerAndSource(token, mapStart + 0, CreateNewLayout(token, width, height)); - WritePointerAndSource(token, mapStart + 4, CreateNewEvents(token)); - WritePointerAndSource(token, mapStart + 8, CreateNewMapScripts(token)); - WritePointerAndSource(token, mapStart + 12, CreateNewConnections(token)); - - var table = new TableStreamRun(model, mapStart, SortedSpan.None, _map, null, new FixedLengthStreamStrategy(1)); - model.ObserveRunWritten(token, table); - - return mapStart; - } - private void Erase(ITableRun table, ModelDelta token) { foreach (var source in table.PointerSources) { model.ClearPointer(token, source, table.Start); @@ -1072,7 +1022,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { } private BlockMapViewModel GetNeighbor(ConnectionModel connection, Border border) { - var vm = new BlockMapViewModel(fileSystem, viewPort, connection.MapGroup, connection.MapNum) { + var vm = new BlockMapViewModel(fileSystem, viewPort, format, connection.MapGroup, connection.MapNum) { IncludeBorders = IncludeBorders, SpriteScale = SpriteScale, allOverworldSprites = allOverworldSprites, @@ -1090,8 +1040,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { 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("blockdata1")); - if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2")); + 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); @@ -1129,8 +1079,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { 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 (blockModel1 == null) blockModel1 = new BlocksetModel(model, layout.GetAddress("blockdata1")); - if (blockModel2 == null) blockModel2 = new BlocksetModel(model, layout.GetAddress("blockdata2")); + 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); @@ -1353,12 +1303,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { for (int i = 0; i < connections.Count; i++) { if (connections[i].Direction != direction) continue; if (direction == MapDirection.Up || direction == MapDirection.Down) { - var map = new BlockMapViewModel(fileSystem, viewPort, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites }; + var map = new BlockMapViewModel(fileSystem, viewPort, format, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites }; 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, viewPort, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites }; + var map = new BlockMapViewModel(fileSystem, viewPort, format, connections[i].MapGroup, connections[i].MapNum) { allOverworldSprites = allOverworldSprites }; var removeHeight = map.pixelHeight / 16; var removeOffset = connections[i].Offset; foreach (int j in removeHeight.Range()) availableSpace.Remove(j + removeOffset); @@ -1402,56 +1352,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { return null; } - private int CreateNewLayout(ModelDelta token, int width, int height) { - // width:: height:: borderblock<> blockmap<`blm`> blockdata1<> blockdata2<> borderwidth. borderheight. unused: - var myLayout = GetLayout(); - var blockmapLength = width * height * 2; - var blockmapStart = model.FindFreeSpace(model.FreeSpaceStart, blockmapLength + 28); - var layoutStart = blockmapStart + blockmapLength; - token.ChangeData(model, blockmapStart, new byte[blockmapLength]); - - model.WriteValue(token, layoutStart + 0, width); - model.WriteValue(token, layoutStart + 4, height); - WritePointerAndSource(token, layoutStart + 8, myLayout.GetAddress("borderblock")); - WritePointerAndSource(token, layoutStart + 12, blockmapStart); - WritePointerAndSource(token, layoutStart + 16, myLayout.GetAddress("blockdata1")); - WritePointerAndSource(token, layoutStart + 20, myLayout.GetAddress("blockdata2")); - if (myLayout.HasField("borderwidth")) { - model.WriteValue(token, layoutStart + 24, myLayout.GetValue("borderwidth")); - model.WriteValue(token, layoutStart + 25, myLayout.GetValue("borderwidth")); - model.WriteMultiByteValue(layoutStart + 26, 2, token, 0); - } - return layoutStart; - } - private void WritePointerAndSource(ModelDelta token, int source, int destination) { model.WritePointer(token, source, destination); model.ObserveRunWritten(token, NoInfoRun.FromPointer(model, source)); } - private int CreateNewEvents(ModelDelta token) { - // objectCount. warpCount. scriptCount. signpostCount. objects<> warps<> scripts<> signposts<> - var eventStart = model.FindFreeSpace(model.FreeSpaceStart, 20); - token.ChangeData(model, eventStart, new byte[20]); - return eventStart; - } - - private int CreateNewMapScripts(ModelDelta token) { - // mapscripts<[type. pointer<>]!00> - var eventStart = model.FindFreeSpace(model.FreeSpaceStart, 4); - token.ChangeData(model, eventStart, new byte[] { 0, 0xFF, 0xFF, 0xFF }); - return eventStart; - } - - private int CreateNewConnections(ModelDelta token) { - // count:: connections<> - var connectionStart = model.FindFreeSpace(model.FreeSpaceStart, 8); - token.ChangeData(model, connectionStart, new byte[8]); - var run = new TableStreamRun(model, connectionStart, SortedSpan.None, $"[{ConnectionInfo.ConnectionTableContent}]", null, new FixedLengthStreamStrategy(1)); - model.ObserveRunWritten(token, run); - return connectionStart; - } - private void GotoAddress(int address) => viewPort.Goto.Execute(address); private void HandleBlocksChanged(object sender, byte[][] blocks) { @@ -1546,6 +1451,56 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { public record Border(int North, int East, int South, int West); + public class Format { + public static string Layout => "layout"; + public static string Warps => "warps"; + public static string Objects => "objects"; + public static string Connections => "connections"; + public static string Scripts => "scripts"; + public static string Signposts => "signposts"; + public static string ObjectCount => "objectCount"; + public static string WarpCount => "warpCount"; + public static string ScriptCount => "scriptCount"; + public static string SignpostCount => "signpostCount"; + public static string BorderBlock => "borderblock"; + public static string BlockMap => "blockmap"; + public static string PrimaryBlockset => "blockdata1"; + public static string SecondaryBlockset => "blockdata2"; + public static string Tileset => "tileset"; + public static string BlockAttributes => "attributes"; + public static string Blocks => "block"; + public static string TileAnimationRoutine => "animation"; + public static string Palette => "pal"; + public static string BorderWidth => "borderwidth"; + public static string BorderHeight => "borderheight"; + + public string BlockDataFormat { get; } + public string LayoutFormat { get; } + public string ObjectsFormat { get; } + public string WarpsFormat { get; } + public string ScriptsFormat { get; } + public string SignpostsFormat { get; } + public string EventsFormat { get; } + public string ConnectionsFormat { get; } + public string HeaderFormat { get; } + public string MapFormat { get; } + + public Format(bool isRSE) { + BlockDataFormat = $"[isCompressed. isSecondary. padding: {Tileset}<> {Palette}<`ucp4:0123456789ABCDEF`> {Blocks}<> {TileAnimationRoutine}<> {BlockAttributes}<>]1"; + if (isRSE) BlockDataFormat = $"[isCompressed. isSecondary. padding: {Tileset}<> {Palette}<`ucp4:0123456789ABCDEF`> {Blocks}<> {BlockAttributes}<> {TileAnimationRoutine}<>]1"; + LayoutFormat = $"[width:: height:: {BorderBlock}<> {BlockMap}<`blm`> {PrimaryBlockset}<{BlockDataFormat}> {SecondaryBlockset}<{BlockDataFormat}> {BorderWidth}. {BorderHeight}. unused:]1"; + if (isRSE) LayoutFormat = $"[width:: height:: {BorderBlock}<> {BlockMap}<`blm`> {PrimaryBlockset}<{BlockDataFormat}> {PrimaryBlockset}<{BlockDataFormat}>]1"; + ObjectsFormat = $"[id. graphics. unused: x:500 y:500 elevation. moveType. range:|t|x::|y:: trainerType: trainerRangeOrBerryID: script<`xse`> flag: unused:]/{ObjectCount}"; + WarpsFormat = $"[x:500 y:500 elevation. warpID. map. bank.]/{WarpCount}"; + ScriptsFormat = $"[x:500 y:500 elevation: trigger: index:: script<`xse`>]/{ScriptCount}"; + SignpostsFormat = $"[x:500 y:500 elevation. kind. unused: arg::|h]/{SignpostCount}"; + EventsFormat = $"[{ObjectCount}. {WarpCount}. {ScriptCount}. {SignpostCount}. {Objects}<{ObjectsFormat}> {Warps}<{WarpsFormat}> {Scripts}<{ScriptsFormat}> {Signposts}<{SignpostsFormat}>]1"; + ConnectionsFormat = "[count:: connections<[direction:: offset:: mapGroup. mapNum. unused:]/count>]1"; + HeaderFormat = "music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags.|t|allowEscaping.|allowRunning.|showMapName::: floorNum. battleType."; + MapFormat = $"[{Layout}<{LayoutFormat}> events<{EventsFormat}> mapscripts<[type. pointer<>]!00> {Connections}<{ConnectionsFormat}> {HeaderFormat}]"; + } + } + public record ConnectionInfo(int Size, int Offset, MapDirection Direction) { public const string SingleConnectionContent = "direction:: offset:: mapGroup. mapNum. unused:"; public const string SingleConnectionLength = "/count"; diff --git a/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs b/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs index 703c5e10..48779b1d 100644 --- a/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs +++ b/src/HexManiac.Core/ViewModels/Map/MapEditorViewModel.cs @@ -14,6 +14,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { /// Represents the entire map editor tab, with all visible controls, maps, edit boxes, etc /// public class MapEditorViewModel : ViewModelCore, ITabContent { + private readonly Format format; private readonly IFileSystem fileSystem; private readonly IEditableViewPort viewPort; private readonly IDataModel model; @@ -23,6 +24,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { public IViewPort ViewPort => viewPort; public IFileSystem FileSystem => fileSystem; + public Format Format => format; private long blockset; private int blockset1, blockset2; @@ -211,7 +213,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { } } - UpdatePrimaryMap(new BlockMapViewModel(fileSystem, viewPort, bank, map) { + UpdatePrimaryMap(new BlockMapViewModel(fileSystem, viewPort, format, bank, map) { IncludeBorders = primaryMap?.IncludeBorders ?? true, SpriteScale = primaryMap?.SpriteScale ?? .5, }); @@ -239,9 +241,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Map { history.Redo.CanExecuteChanged += (sender, e) => redo.RaiseCanExecuteChanged(); history.Bind(nameof(history.HasDataChange), (sender, e) => NotifyPropertyChanged(nameof(Name))); - PrimaryTiles = model.IsFRLG() ? 640 : 512; + var isFRLG = model.IsFRLG(); + PrimaryTiles = isFRLG ? 640 : 512; + this.format = new Format(!isFRLG); - var map = new BlockMapViewModel(fileSystem, viewPort, 3, 19); + var map = new BlockMapViewModel(fileSystem, viewPort, format, 3, 19); templates = new(model, viewPort.Tools.CodeTool.ScriptParser, map.AllOverworldSprites); UpdatePrimaryMap(map); for (int i = 0; i < 0x40; i++) CollisionOptions.Add(i.ToString("X2")); diff --git a/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs b/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs index ae923843..d4837b83 100644 --- a/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs +++ b/src/HexManiac.Core/ViewModels/Map/MapRepointer.cs @@ -1,5 +1,536 @@ -namespace HavenSoft.HexManiac.Core.ViewModels.Map { +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; +using HavenSoft.HexManiac.Core.ViewModels.Tools; +using HexManiac.Core.Models.Runs.Sprites; +using System; +using System.Linq; +using System.Windows.Input; + +namespace HavenSoft.HexManiac.Core.ViewModels.Map { + public record ChangeMapEventArgs(int Bank, int Map); + public class MapRepointer : ViewModelCore { + private readonly Format format; + private readonly IFileSystem fileSystem; + private readonly IDataModel model; + private readonly ChangeHistory history; + private readonly int mapID; // bank * 1000 + map + + #region Commands + + private StubCommand + repointLayout, + repointBorderBlock, + repointBlockMap, + repointPrimaryBlockset, + repointSecondaryBlockset, + repointPrimaryTileset, + repointSecondaryTileset, + expandPrimaryTileset, + expandSecondaryTileset, + repointPrimaryBlocks, + repointSecondaryBlocks, + expandPrimaryBlocks, + expandSecondaryBlocks, + repointPrimaryPalette, + repointSecondaryPalette, + expandPrimaryPalette, + expandSecondaryPalette, + createPrimaryTilesetAnimations, + createSecondaryTilesetAnimations, + duplicateMap; + + public ICommand RepointLayout => StubCommand(ref repointLayout, ExecuteRepointLayout, CanRepointLayout); + public ICommand RepointBorderBlock => StubCommand(ref repointBorderBlock, ExecuteRepointBorderBlock, CanRepointBorderBlock); + public ICommand RepointBlockMap => StubCommand(ref repointBlockMap, ExecuteRepointBlockMap, CanRepointBlockMap); + public ICommand RepointPrimaryBlockset => StubCommand(ref repointPrimaryBlockset, ExecuteRepointPrimaryBlockset, CanRepointPrimaryBlockset); + public ICommand RepointSecondaryBlockset => StubCommand(ref repointSecondaryBlockset, ExecuteRepointSecondaryBlockset, CanRepointSecondaryBlockset); + public ICommand RepointPrimaryTileset => StubCommand(ref repointPrimaryTileset, ExecuteRepointPrimaryTileset, CanRepointPrimaryTileset); + public ICommand RepointSecondaryTileset => StubCommand(ref repointSecondaryTileset, ExecuteRepointSecondaryTileset, CanRepointSecondaryTileset); + public ICommand ExpandPrimaryTileset => StubCommand(ref expandPrimaryTileset, ExecuteExpandPrimaryTileset, CanExpandPrimaryTileset); + public ICommand ExpandSecondaryTileset => StubCommand(ref expandSecondaryTileset, ExecuteExpandSecondaryTileset, CanExpandSecondaryTileset); + public ICommand RepointPrimaryBlocks => StubCommand(ref repointPrimaryBlocks, ExecuteRepointPrimaryBlocks, CanRepointPrimaryBlocks); + public ICommand RepointSecondaryBlocks => StubCommand(ref repointSecondaryBlocks, ExecuteRepointSecondaryBlocks, CanRepointSecondaryBlocks); + public ICommand ExpandPrimaryBlocks => StubCommand(ref expandPrimaryBlocks, ExecuteExpandPrimaryBlocks, CanExpandPrimaryBlocks); + public ICommand ExpandSecondaryBlocks => StubCommand(ref expandSecondaryBlocks, ExecuteExpandSecondaryBlocks, CanExpandSecondaryBlocks); + public ICommand RepointPrimaryPalette => StubCommand(ref repointPrimaryPalette, ExecuteRepointPrimaryPalette, CanRepointPrimaryPalette); + public ICommand RepointSecondaryPalette => StubCommand(ref repointSecondaryPalette, ExecuteRepointSecondaryPalette, CanRepointSecondaryPalette); + public ICommand ExpandPrimaryPalette => StubCommand(ref expandPrimaryPalette, ExecuteExpandPrimaryPalette, CanExpandPrimaryPalette); + public ICommand ExpandSecondaryPalette => StubCommand(ref expandSecondaryPalette, ExecuteExpandSecondaryPalette, CanExpandSecondaryPalette); + public ICommand CreatePrimaryTilesetAnimations => StubCommand(ref createPrimaryTilesetAnimations, ExecuteCreatePrimaryTilesetAnimations, CanCreatePrimaryTilesetAnimations); + public ICommand CreateSecondaryTilesetAnimations => StubCommand(ref createSecondaryTilesetAnimations, ExecuteCreateSecondaryTilesetAnimations, CanCreateSecondaryTilesetAnimations); + public ICommand DuplicateMap => StubCommand(ref duplicateMap, ExecuteDuplicateMap, CanDuplicateMap); + + #endregion + + public event EventHandler DataMoved; + public event EventHandler ChangeMap; + + public MapRepointer(Format format, IFileSystem fileSystem, IDataModel model, ChangeHistory history, int mapID) { + this.format = format; + this.fileSystem = fileSystem; + this.model = model; + this.history = history; + this.mapID = mapID; + } + + #region General + + private bool CanRepointLayout() { + var map = GetMapModel(); + if (map == null) return false; + var layoutStart = map.GetAddress(Format.Layout); + if (layoutStart < 0 || layoutStart >= model.Count) return false; + var layoutRun = model.GetNextRun(layoutStart); + return layoutRun.PointerSources != null && layoutRun.PointerSources.Count > 1; + } + + private void ExecuteRepointLayout() { + var map = GetMapModel(); + var layoutStart = map.GetAddress(Format.Layout); + var layoutRun = model.GetNextRun(layoutStart); + var newDataStart = DuplicateData(layoutRun.Start, layoutRun.Length); + map.SetAddress(Format.Layout, newDataStart); + DataMoved.Raise(this); + repointLayout.RaiseCanExecuteChanged(); + repointBorderBlock.RaiseCanExecuteChanged(); + repointBlockMap.RaiseCanExecuteChanged(); + repointPrimaryBlockset.RaiseCanExecuteChanged(); + repointSecondaryBlockset.RaiseCanExecuteChanged(); + } + + private bool CanRepointBorderBlock() => CanRepointLayoutMember(Format.BorderBlock); + + private void ExecuteRepointBorderBlock() { + var layout = GetLayout(); + var count = 4; + if (layout.HasField(Format.BorderWidth) && layout.HasField(Format.BorderHeight)) { + count = Math.Max(4, layout.GetValue(Format.BorderWidth) * layout.GetValue(Format.BorderHeight)); + } + ExecuteRepointLayoutMember(Format.BorderBlock, count * 2, repointBorderBlock); + } + + private bool CanRepointBlockMap() => CanRepointLayoutMember(Format.BlockMap); + + private void ExecuteRepointBlockMap() { + var layout = GetLayout(); + var (width, height) = (layout.GetValue("width"), layout.GetValue("height")); + ExecuteRepointLayoutMember(Format.BlockMap, width * height * 2, repointBlockMap); + } + + private bool CanRepointPrimaryBlockset() => CanRepointLayoutMember(Format.PrimaryBlockset); + + private void ExecuteRepointPrimaryBlockset() { + ExecuteRepointLayoutMember(Format.PrimaryBlockset, 6 * 4, + repointPrimaryBlockset, repointPrimaryBlocks, + repointPrimaryTileset, repointPrimaryPalette, + expandPrimaryBlocks, expandPrimaryTileset, + expandPrimaryPalette, createPrimaryTilesetAnimations); + } + + private bool CanRepointSecondaryBlockset() => CanRepointLayoutMember(Format.SecondaryBlockset); + + private void ExecuteRepointSecondaryBlockset() { + ExecuteRepointLayoutMember(Format.SecondaryBlockset, 6 * 4, + repointSecondaryBlockset, repointSecondaryBlocks, + repointSecondaryTileset, repointSecondaryPalette, + expandSecondaryBlocks, expandSecondaryTileset, + expandSecondaryPalette, createSecondaryTilesetAnimations); + } + + private bool CanDuplicateMap() { + return true; + } + + private void ExecuteDuplicateMap() { + var option = GetMapBankForNewMap("Duplicate map into which group?"); + if (option == -1) return; + var table = AddNewMapToBank(option); + var newMapStart = CreateNewMap(history.CurrentChange); + model.UpdateArrayPointer(history.CurrentChange, null, null, -1, table.Start + table.Length - 4, newMapStart); + ChangeMap.Raise(this, new(option, table.ElementCount - 1)); + repointLayout.RaiseCanExecuteChanged(); + } + + private bool CanRepointLayoutMember(string member) { + var layout = GetLayout(); + if (layout == null) return false; + var start = layout.GetAddress(member); + var run = model.GetNextRun(start); + return run.PointerSources != null && run.PointerSources.Count > 1; + } + + private void ExecuteRepointLayoutMember(string member, int length, params StubCommand[] commands) { + var layout = GetLayout(); + var start = DuplicateData(layout.GetAddress(member), length); + layout.SetAddress(member, start); + DataMoved.Raise(this); + foreach (var command in commands) command.RaiseCanExecuteChanged(); + } + + #endregion + + #region Blocks / Tilesets / Palettes + + private bool CanRepointPrimaryBlocks() => CanRepointBlocksetMember(Format.PrimaryBlockset, Format.Blocks); + + private void ExecuteRepointPrimaryBlocks() { + var layout = GetLayout(); + var attributeSize = model.IsFRLG() ? 2 : 1; + var (blockCount, _) = EstimateBlockCount(layout, true); + ExecuteRepointBlocksetMember(Format.PrimaryBlockset, Format.Blocks, blockCount * 16); + ExecuteRepointBlocksetMember(Format.PrimaryBlockset, Format.BlockAttributes, blockCount * attributeSize, repointPrimaryBlocks); + } + + private bool CanRepointSecondaryBlocks() => CanRepointBlocksetMember(Format.SecondaryBlockset, Format.Blocks); + + private void ExecuteRepointSecondaryBlocks() { + var layout = GetLayout(); + var attributeSize = model.IsFRLG() ? 2 : 1; + var (blockCount, _) = EstimateBlockCount(layout, false); + ExecuteRepointBlocksetMember(Format.PrimaryBlockset, Format.Blocks, blockCount * 16); + ExecuteRepointBlocksetMember(Format.PrimaryBlockset, Format.BlockAttributes, blockCount * attributeSize, repointPrimaryBlocks); + } + + private bool CanExpandPrimaryBlocks() { + var layout = GetLayout(); + var (blockCount, maxBlockCount) = EstimateBlockCount(layout, true); + return blockCount < maxBlockCount; + } + + private void ExecuteExpandPrimaryBlocks() { + ExecuteExpandBlocks(true); + expandPrimaryBlocks.RaiseCanExecuteChanged(); + } + + private bool CanExpandSecondaryBlocks() { + var layout = GetLayout(); + var (blockCount, maxBlockCount) = EstimateBlockCount(layout, false); + return blockCount < maxBlockCount; + } + + private void ExecuteExpandSecondaryBlocks() { + ExecuteExpandBlocks(false); + expandPrimaryBlocks.RaiseCanExecuteChanged(); + } + + private void ExecuteExpandBlocks(bool primary) { + // expand blocks and attributes + // neither have any formatting, so here's the plan: + var token = history.CurrentChange; + var layout = GetLayout(); + var (blockCount, maxBlockCount) = EstimateBlockCount(layout, primary); + var attributeSize = model.IsFRLG() ? 2 : 1; + // (1) copy all block/attribute data into a temp array + var blocksetName = primary ? Format.PrimaryBlockset : Format.SecondaryBlockset; + var blockStart = layout.GetSubTable(blocksetName)[0].GetAddress(Format.Blocks); + var attributeStart = layout.GetSubTable(blocksetName)[0].GetAddress(Format.BlockAttributes); + var blockData = Cut(blockStart, blockCount * 16); + var attributeData = Cut(attributeStart, blockCount * attributeSize); + // (2) repoint blocks/attrbutes + blockStart = model.RelocateForExpansion(token, model.GetNextRun(blockStart), maxBlockCount * 16).Start; + attributeStart = model.RelocateForExpansion(token, model.GetNextRun(attributeStart), maxBlockCount * attributeSize).Start; + // (3) paste old data and expand + Paste(blockStart, blockData, maxBlockCount * 16); + Paste(attributeStart, attributeData, maxBlockCount * attributeSize); + + DataMoved.Raise(this); + } + + private byte[] Cut(int start, int length) { + var result = new byte[length]; + var token = history.CurrentChange; + for (int i = 0; i < length; i++) { + result[i] = model[start + i]; + token.ChangeData(model, start + i, 0xFF); + } + return result; + } + + private void Paste(int start, byte[] data, int length) { + var token = history.CurrentChange; + token.ChangeData(model, start, data); + for (int i = data.Length; i < length; i++) token.ChangeData(model, start + i, 0); + } + + private (int, int) EstimateBlockCount(ModelArrayElement layout, bool primary) { + var blocksetName = primary ? Format.PrimaryBlockset : Format.SecondaryBlockset; + var blockset = layout.GetSubTable(blocksetName)[0]; + var blockCount = model.IsFRLG() ? 640 : 512; + if (!primary) blockCount = 1024 - blockCount; + var maxBlockCount = blockCount; + var blockStart = blockset.GetAddress(Format.Blocks); + var attributeStart = blockset.GetAddress(Format.BlockAttributes); + BlocksetModel.EstimateBlockCount(model, ref blockCount, blockStart, attributeStart); + return (blockCount, maxBlockCount); + } + + + private bool CanRepointPrimaryTileset() => CanRepointBlocksetMember(Format.PrimaryBlockset, Format.Tileset); + + private void ExecuteRepointPrimaryTileset() => ExecuteRepointTileset(Format.PrimaryBlockset); + private void ExecuteRepointTileset(string blocksetName) { + var maxTileCount = model.IsFRLG() ? 640 : 512; + if (blocksetName == Format.SecondaryBlockset) maxTileCount = 1024 - maxTileCount; + var layout = GetLayout(); + var blockset = layout.GetSubTable(blocksetName)[0]; + var isCompressed = blockset.GetValue("isCompressed") != 0; + if (isCompressed) RepointCompressedTileset(blocksetName, maxTileCount); + else RepointUncompressedTileset(blocksetName, maxTileCount); + } + + private bool CanRepointSecondaryTileset() => CanRepointBlocksetMember(Format.SecondaryBlockset, Format.Tileset); + + private void ExecuteRepointSecondaryTileset() => ExecuteRepointTileset(Format.PrimaryBlockset); + + private bool CanExpandPrimaryTileset() { + return false; + } + + private void ExecuteExpandPrimaryTileset() { + // example uncompressed: emerald bank 25 map 0 -> primary tileset starts at 37AA48 and is 0x8000 bytes. 0x20 bytes is one tile, so that's 0x400 tiles, but the limit should be 0x200? + // secondary tileset starts at 36B9DF and appears to be only 0xA60 bytes (0x53 tiles) long + } + + private bool CanExpandSecondaryTileset() { + return false; + } + + private void ExecuteExpandSecondaryTileset() { + + } + + private void RepointCompressedTileset(string blocksetName, int maxTileCount) { + throw new NotImplementedException(); + } + + private void RepointUncompressedTileset(string blocksetName, int maxTileCount) { + throw new NotImplementedException(); + } + + private bool CanRepointPrimaryPalette() => CanRepointBlocksetMember(Format.PrimaryBlockset, Format.Palette); + + private void ExecuteRepointPrimaryPalette() { + ExecuteRepointBlocksetMember(Format.PrimaryBlockset, Format.Palette, 32, repointPrimaryPalette); + } + + private bool CanRepointSecondaryPalette() => CanRepointBlocksetMember(Format.SecondaryBlockset, Format.Palette); + + private void ExecuteRepointSecondaryPalette() { + ExecuteRepointBlocksetMember(Format.SecondaryBlockset, Format.Palette, 32, repointSecondaryPalette); + } + + private bool CanExpandPrimaryPalette() { + return false; + } + + private void ExecuteExpandPrimaryPalette() { + + } + + private bool CanExpandSecondaryPalette() { + return false; + } + + private void ExecuteExpandSecondaryPalette() { + + } + + + private bool CanRepointBlocksetMember(string blocksetName, string member) { + var layout = GetLayout(); + if (layout == null) return false; + var blockset = layout.GetSubTable(blocksetName); + if (blockset == null) return false; + var start = blockset[0].GetAddress(member); + var run = model.GetNextRun(start); + return run.PointerSources != null && run.PointerSources.Count > 1; + } + + private void ExecuteRepointBlocksetMember(string blocksetName, string member, int length, params StubCommand[] commands) { + var layout = GetLayout(); + var blockset = layout.GetSubTable(blocksetName)[0]; + var start = DuplicateData(blockset.GetAddress(member), length); + blockset.SetAddress(member, start); + DataMoved.Raise(this); + foreach (var command in commands) command.RaiseCanExecuteChanged(); + } + + #endregion + + #region Animations + + private bool CanCreatePrimaryTilesetAnimations() { + // we should be able to run the utility to create a tileset animation unless we already have + return false; + } + + private void ExecuteCreatePrimaryTilesetAnimations() { + // TODO grab the code from the utility and use that here + throw new NotImplementedException(); + } + + private bool CanCreateSecondaryTilesetAnimations() { + // we should be able to run the utility to create a tileset animation unless we already have + return false; + } + + private void ExecuteCreateSecondaryTilesetAnimations() { + // grab the code from the utility and use that here + } + + #endregion + + #region Helpers + + public int GetMapBankForNewMap(string prompt) { + var tokenFactory = () => history.CurrentChange; + var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start, tokenFactory); + var enumViewModel = new EnumViewModel(mapBanks.Count.Range(i => i.ToString()).ToArray()); + var option = fileSystem.ShowOptions( + "Pick a group", + prompt, + new[] { new[] { enumViewModel } }, + new VisualOption { Index = 1, Option = "OK", ShortDescription = "Insert New Map" }); + if (option == -1) return option; + return enumViewModel.Choice; + } + + /// + /// Expands the chosen map bank by one, adding a new map to the end. + /// + /// The table that contains the new map. + public ITableRun AddNewMapToBank(int option) { + var tokenFactory = () => history.CurrentChange; + var token = history.CurrentChange; + var mapBanks = new ModelTable(model, model.GetTable(HardcodeTablesModel.MapBankTable).Start, tokenFactory); + ITableRun mapTable; + if (mapBanks.Count == option) { + var newTable = model.RelocateForExpansion(token, mapBanks.Run, mapBanks.Run.Length + mapBanks.Run.ElementLength); + newTable = newTable.Append(token, 1); + model.ObserveRunWritten(token, newTable); + mapBanks = new ModelTable(model, newTable.Start, tokenFactory, newTable); + var tableStart = model.FindFreeSpace(model.FreeSpaceStart, 8); + mapTable = new TableStreamRun(model, tableStart, SortedSpan.One(mapBanks[option].Start), $"[map<{format.MapFormat}1>]", null, new DynamicStreamStrategy(model, null), 0); + model.UpdateArrayPointer(token, null, null, -1, mapBanks[option].Start, tableStart); + } else { + mapTable = mapBanks[option].GetSubTable("maps").Run; + } + mapTable = mapTable.Append(token, 1); + model.ObserveRunWritten(token, mapTable); + return mapTable; + } + + /// + /// Inserts a new map using the current map's layout. + /// Creates event data with 0 events, map scripts data with 0 scripts, and connection data with 0 connections. + /// Copies all the flags/header from the current map. + /// + public int CreateNewMap(ModelDelta token) { + var currentMap = GetMapModel(); + var mapStart = model.FindFreeSpace(model.FreeSpaceStart, 28); + // music: layoutID: regionSectionID. cave. weather. mapType. allowBiking. flags. floorNum. battleType. + for (int i = 16; i < 28; i++) token.ChangeData(model, mapStart + i, model[currentMap.Start + i]); + + WritePointerAndSource(token, mapStart + 4, CreateNewEvents(token)); + WritePointerAndSource(token, mapStart + 8, CreateNewMapScripts(token)); + WritePointerAndSource(token, mapStart + 12, CreateNewConnections(token)); + + var table = new TableStreamRun(model, mapStart, SortedSpan.None, format.MapFormat, null, new FixedLengthStreamStrategy(1)); + model.ObserveRunWritten(token, table); + + return mapStart; + } + + /// + /// Creates a new layout using the existing layout's borderblock, blockmap, primary blockset, and secondary blockset. + /// Once this new layout is assigned to a map, you'll want to update that map's layout ID by calling BlockMapViewModel.UpdateLayoutID() + /// + public int CreateNewLayout(ModelDelta token) { + var layoutStart = model.FindFreeSpace(model.FreeSpaceStart, 28); + var myLayout = GetLayout(); + + model.WriteValue(token, layoutStart + 0, myLayout.GetValue("width")); + model.WriteValue(token, layoutStart + 4, myLayout.GetValue("height")); + WritePointerAndSource(token, layoutStart + 8, myLayout.GetAddress(Format.BorderBlock)); + WritePointerAndSource(token, layoutStart + 12, myLayout.GetAddress(Format.BlockMap)); + WritePointerAndSource(token, layoutStart + 16, myLayout.GetAddress(Format.PrimaryBlockset)); + WritePointerAndSource(token, layoutStart + 20, myLayout.GetAddress(Format.SecondaryBlockset)); + if (myLayout.HasField(Format.BorderWidth)) { + model.WriteValue(token, layoutStart + 24, myLayout.GetValue(Format.BorderWidth)); + model.WriteValue(token, layoutStart + 25, myLayout.GetValue(Format.BorderHeight)); + model.WriteMultiByteValue(layoutStart + 26, 2, token, 0); + } + return layoutStart; + } + + public int CreateNewBlockMap(ModelDelta token, int width, int height) { + var blockmapLength = width * height * 2; + var blockmapStart = model.FindFreeSpace(model.FreeSpaceStart, blockmapLength); + token.ChangeData(model, blockmapStart, new byte[blockmapLength]); + return blockmapStart; + } + + public int CreateNewBorderBlock(ModelDelta token) { + var layout = GetLayout(); + var count = 4; + if (layout.HasField(Format.BorderWidth) && layout.HasField(Format.BorderHeight)) { + count = Math.Max(4, layout.GetValue(Format.BorderWidth) * layout.GetValue(Format.BorderHeight)); + } + var myStart = layout.GetAddress(Format.BorderBlock); + var start = model.FindFreeSpace(model.FreeSpaceStart, count * 2); + for (int i = 0; i < count; i++) model.WriteMultiByteValue(start + i * 2, 2, token, model.ReadMultiByteValue(myStart + i * 2, 2)); + return start; + } + + public int CreateNewEvents(ModelDelta token) { + // objectCount. warpCount. scriptCount. signpostCount. objects<> warps<> scripts<> signposts<> + var eventStart = model.FindFreeSpace(model.FreeSpaceStart, 20); + token.ChangeData(model, eventStart, new byte[20]); + return eventStart; + } + + public int CreateNewMapScripts(ModelDelta token) { + // mapscripts<[type. pointer<>]!00> + var eventStart = model.FindFreeSpace(model.FreeSpaceStart, 4); + token.ChangeData(model, eventStart, new byte[] { 0, 0xFF, 0xFF, 0xFF }); + return eventStart; + } + + public int CreateNewConnections(ModelDelta token) { + // count:: connections<> + var connectionStart = model.FindFreeSpace(model.FreeSpaceStart, 8); + token.ChangeData(model, connectionStart, new byte[8]); + var run = new TableStreamRun(model, connectionStart, SortedSpan.None, $"[{ConnectionInfo.ConnectionTableContent}]", null, new FixedLengthStreamStrategy(1)); + model.ObserveRunWritten(token, run); + return connectionStart; + } + + private ModelArrayElement GetMapModel() { + return BlockMapViewModel.GetMapModel(model, mapID / 1000, mapID % 1000, () => history.CurrentChange); + } + + private ModelArrayElement GetLayout(ModelArrayElement map = null) { + if (map == null) map = GetMapModel(); + if (map == null) return null; + var layout = map.GetSubTable(Format.Layout); + if (layout == null) return null; + return layout[0]; + } + + private int DuplicateData(int start, int length) { + var newStart = model.FindFreeSpace(model.FreeSpaceStart, length); + var token = history.CurrentChange; + for (int i = 0; i < length; i++) token.ChangeData(model, newStart + i, model[start + i]); + return newStart; + } + + private void WritePointerAndSource(ModelDelta token, int source, int destination) { + model.WritePointer(token, source, destination); + model.ObserveRunWritten(token, NoInfoRun.FromPointer(model, source)); + } + + #endregion } } diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 2ae0daa4..77d195b2 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -216,7 +216,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var maps = Model.GetMatchingMaps(str); if (maps.Count == 1 && mapper != null && !str.TryParseHex(out _)) { var previousMap = mapper.PrimaryMap; - mapper.PrimaryMap = new BlockMapViewModel(mapper.FileSystem, this, maps[0].Group, maps[0].Map); + mapper.PrimaryMap = new BlockMapViewModel(mapper.FileSystem, this, mapper.Format, maps[0].Group, maps[0].Map); args = new TabChangeRequestedEventArgs(mapper); RequestTabChange?.Invoke(this, args); if (args.RequestAccepted) {