From 1c70f8379a9fcfc94a920cdd8b9b2ce8fc111b1a Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Sun, 12 May 2019 21:53:24 -0500 Subject: [PATCH 01/20] Initial Introduction of EggMoveStream recognition The Egg Move stream has 2 types of data in it, each of which is 2 bytes long. Either it's a pokemon, or it's a move. (There's also the end, FF FF). This is the initial test to show that we can read an egg move stream from a format. --- src/HexManiac.Core/HexManiac.Core.csproj | 1 + src/HexManiac.Core/Models/PokemonModel.cs | 2 + src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 52 +++++++++++++++++++ src/HexManiac.Core/SystemExtensions.cs | 3 ++ .../ViewModels/ConvertCellToText.cs | 4 ++ src/HexManiac.Core/ViewModels/DataFormats.cs | 38 ++++++++++++++ .../ViewModels/GotoControlViewModel.cs | 3 +- .../Visitors/CompleteEditOperation.cs | 4 ++ .../ViewModels/Visitors/ContextItemFactory.cs | 4 ++ .../ViewModels/Visitors/ContinueCellEdit.cs | 4 ++ .../ViewModels/Visitors/DataClear.cs | 4 ++ .../ViewModels/Visitors/StartCellEdit.cs | 4 ++ src/HexManiac.Tests/EggMoveTests.cs | 25 +++++++++ src/HexManiac.Tests/HexManiac.Tests.csproj | 1 + .../Implementations/FormatDrawer.cs | 5 ++ src/SharedAssemblyInfo.cs | 4 +- 16 files changed, 155 insertions(+), 3 deletions(-) create mode 100644 src/HexManiac.Core/Models/Runs/EggMoveRun.cs create mode 100644 src/HexManiac.Tests/EggMoveTests.cs diff --git a/src/HexManiac.Core/HexManiac.Core.csproj b/src/HexManiac.Core/HexManiac.Core.csproj index ab544a3e..c60b63a4 100644 --- a/src/HexManiac.Core/HexManiac.Core.csproj +++ b/src/HexManiac.Core/HexManiac.Core.csproj @@ -52,6 +52,7 @@ + diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index 64b19dc0..ccc7752a 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -979,6 +979,8 @@ namespace HavenSoft.HexManiac.Core.Models { } else { return new ErrorInfo($"Ascii runs must include a length."); } + } else if (format.StartsWith(StreamDelimeter + "egg" + StreamDelimeter)) { + run = new EggMoveRun(model, dataIndex); } else { var errorInfo = TryParse(model, format, dataIndex, null, out var arrayRun); if (errorInfo == ErrorInfo.NoError) { diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs new file mode 100644 index 00000000..728179c1 --- /dev/null +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -0,0 +1,52 @@ +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace HavenSoft.HexManiac.Core.Models.Runs { + public class EggMoveRun : IFormattedRun { + private const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move + private readonly IDataModel model; + + public int Start { get; } + public int Length { get; } + public IReadOnlyList PointerSources { get; private set; } + public string FormatString => AsciiRun.StreamDelimeter + "egg" + AsciiRun.StreamDelimeter; + + public EggMoveRun(IDataModel dataModel, int dataIndex) { + model = dataModel; + Start = dataIndex; + Length = 0; + for (int i = Start; i < model.Count; i += 2) { + if (model[i] == 0xFF && model[i + 1] == 0xFF) { + Length = i - Start + 2; + break; + } + } + } + + public IDataFormat CreateDataFormat(IDataModel data, int dataIndex) { + Debug.Assert(data == model); + var position = dataIndex - Start; + var groupStart = position % 2 == 1 ? position - 1 : position; + var value = data.ReadMultiByteValue(groupStart, 2); + if (value >= MagicNumber) { + return new EggSection(); + } else { + return new EggItem(); + } + } + + public IFormattedRun MergeAnchor(IReadOnlyList sources) { + var newSources = new HashSet(); + if (sources != null) newSources.AddRange(sources); + if (PointerSources != null) newSources.AddRange(PointerSources); + return new EggMoveRun(model, Start) { PointerSources = newSources.ToList() }; + } + + public IFormattedRun RemoveSource(int source) { + throw new NotImplementedException(); + } + } +} diff --git a/src/HexManiac.Core/SystemExtensions.cs b/src/HexManiac.Core/SystemExtensions.cs index 12eced9b..a456cd41 100644 --- a/src/HexManiac.Core/SystemExtensions.cs +++ b/src/HexManiac.Core/SystemExtensions.cs @@ -21,5 +21,8 @@ namespace HavenSoft.HexManiac.Core { return true; } + public static void AddRange(this HashSet set, IEnumerable items) { + foreach (var item in items) set.Add(item); + } } } diff --git a/src/HexManiac.Core/ViewModels/ConvertCellToText.cs b/src/HexManiac.Core/ViewModels/ConvertCellToText.cs index 2ac6e205..c8ce34d6 100644 --- a/src/HexManiac.Core/ViewModels/ConvertCellToText.cs +++ b/src/HexManiac.Core/ViewModels/ConvertCellToText.cs @@ -46,5 +46,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public void Visit(Integer integer, byte data) => Result = integer.Value.ToString(); public void Visit(IntegerEnum integerEnum, byte data) => Result = integerEnum.Value; + + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); } } diff --git a/src/HexManiac.Core/ViewModels/DataFormats.cs b/src/HexManiac.Core/ViewModels/DataFormats.cs index 7dc38456..7faf472d 100644 --- a/src/HexManiac.Core/ViewModels/DataFormats.cs +++ b/src/HexManiac.Core/ViewModels/DataFormats.cs @@ -23,6 +23,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { void Visit(Ascii ascii, byte data); void Visit(Integer integer, byte data); void Visit(IntegerEnum integer, byte data); + void Visit(EggSection section, byte data); + void Visit(EggItem item, byte data); } /// @@ -227,4 +229,40 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public override void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } + + public class EggSection : IDataFormat { + public int Source { get; } + public int Position { get; } + public int Length { get; } + public string SectionName { get; } + + public bool Equals(IDataFormat other) { + if (other is EggSection that) { + return that.SectionName == SectionName && + that.Source == Source && + that.Length == Length; + } + return false; + } + + public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); + } + + public class EggItem : IDataFormat { + public int Source { get; } + public int Position { get; } + public int Length { get; } + public string ItemName { get; } + public int Index { get; } + public bool Equals(IDataFormat other) { + if (other is EggItem that) { + return that.ItemName == ItemName && + that.Source == Source && + that.Length == Length; + } + return false; + } + + public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); + } } diff --git a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs index 5c3bd8ba..1e6640be 100644 --- a/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs +++ b/src/HexManiac.Core/ViewModels/GotoControlViewModel.cs @@ -86,7 +86,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { CanExecute = arg => viewPort?.Goto != null, Execute = arg => { var text = Text; - if (CompletionIndex != -1) text = AutoCompleteOptions[CompletionIndex].CompletionText; + var index = completionIndex.LimitToRange(-1, AutoCompleteOptions.Count - 1); + if (index != -1) text = AutoCompleteOptions[index].CompletionText; if (arg is string) text = (string)arg; viewPort?.Goto?.Execute(text); ControlVisible = false; diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 3bb15014..f6bef3e6 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -109,6 +109,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } } + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + private void CompleteIntegerEdit(Integer integer) { if (!int.TryParse(CurrentText, out var result)) { ErrorText = $"Could not parse {CurrentText} as a number"; diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs index 94676de2..82cd6acc 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs @@ -117,6 +117,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.AddRange(GetTableChildren(arrayRun)); } + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + private IEnumerable GetTableChildren(ArrayRun array) { if (ViewPort.Tools.TableTool.Append.CanExecute(null)) { yield return new ContextItem("Extend Table", ViewPort.Tools.TableTool.Append.Execute); diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs index 461d3e6a..0fe44c37 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs @@ -71,5 +71,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { ".~".Contains(Input) || char.IsWhiteSpace(Input); } + + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs b/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs index 5d7c5b4b..a5d8c0d6 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs @@ -43,5 +43,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(Integer integer, byte data) => buffer.WriteValue(currentChange, index, 0); public void Visit(IntegerEnum integerEnum, byte data) => buffer.WriteValue(currentChange, index, 0); + + public void Visit(EggSection section, byte data) => currentChange.ChangeData(buffer, index, 0xFF); + + public void Visit(EggItem item, byte data) => currentChange.ChangeData(buffer, index, 0xFF); } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index babd636a..a46a0fe0 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs @@ -132,5 +132,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { NewFormat = new UnderEdit(integer, Input.ToString(), integer.Length, autocomplete); Result = true; } + + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs new file mode 100644 index 00000000..a0840b56 --- /dev/null +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -0,0 +1,25 @@ +using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.ViewModels; +using System.Linq; +using Xunit; + +namespace HavenSoft.HexManiac.Tests { + public class EggMoveTests { + private readonly byte[] data; + private readonly PokemonModel model; + private readonly ViewPort viewPort; + + public EggMoveTests() { + data = Enumerable.Range(0, 0x200).Select(i => (byte)0xFF).ToArray(); + model = new PokemonModel(data); + viewPort = new ViewPort("file.gba", model); + } + + [Fact] + public void CanCreateEggMoveStream() { + viewPort.Edit("^eggmoves`egg` "); + + Assert.Equal(2, model.GetNextRun(0).Length); + } + } +} diff --git a/src/HexManiac.Tests/HexManiac.Tests.csproj b/src/HexManiac.Tests/HexManiac.Tests.csproj index c8cbb7e8..f56c37dd 100644 --- a/src/HexManiac.Tests/HexManiac.Tests.csproj +++ b/src/HexManiac.Tests/HexManiac.Tests.csproj @@ -71,6 +71,7 @@ + diff --git a/src/HexManiac.WPF/Implementations/FormatDrawer.cs b/src/HexManiac.WPF/Implementations/FormatDrawer.cs index 88842e87..246a9100 100644 --- a/src/HexManiac.WPF/Implementations/FormatDrawer.cs +++ b/src/HexManiac.WPF/Implementations/FormatDrawer.cs @@ -1,6 +1,7 @@ using HavenSoft.HexManiac.Core.ViewModels; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using HavenSoft.HexManiac.WPF.Controls; +using System; using System.Collections.Generic; using System.Globalization; using System.Linq; @@ -195,6 +196,10 @@ namespace HavenSoft.HexManiac.WPF.Implementations { context.Pop(); } + public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + + public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + private void Underline(Brush brush, bool isStart, bool isEnd) { int startPoint = isStart ? 5 : 0; int endPoint = (int)HexContent.CellWidth - (isEnd ? 5 : 0); diff --git a/src/SharedAssemblyInfo.cs b/src/SharedAssemblyInfo.cs index f50d7168..afc894d7 100644 --- a/src/SharedAssemblyInfo.cs +++ b/src/SharedAssemblyInfo.cs @@ -6,5 +6,5 @@ [assembly: AssemblyTrademark("")] [assembly: AssemblyCulture("")] -[assembly: AssemblyVersion("0.2.0.0")] -[assembly: AssemblyFileVersion("0.2.0.0")] +[assembly: AssemblyVersion("0.2.1.0")] +[assembly: AssemblyFileVersion("0.2.1.0")] From e312a47e989be2b8d884b386dc1b46ff7242401e Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Mon, 13 May 2019 20:22:07 -0500 Subject: [PATCH 02/20] Make GetOptions public/static so I can use it for egg moves --- src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs index 656fead1..af34e9f8 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs @@ -116,8 +116,12 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { private IReadOnlyList cachedOptions; public IReadOnlyList GetOptions(IDataModel model) { if (cachedOptions != null) return cachedOptions; + cachedOptions = GetOptions(model, EnumName); + return cachedOptions; + } - if (!model.TryGetNameArray(EnumName, out var enumArray)) return null; + public static IReadOnlyList GetOptions(IDataModel model, string enumName) { + if (!model.TryGetNameArray(enumName, out var enumArray)) return null; // array must be at least as long as than the current value var optionCount = enumArray.ElementCount; @@ -139,7 +143,6 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { results.Add(value); } - cachedOptions = results; return results; } From 61ab55ea92c7a395f5678aae65e6e3f0d4b5a1d0 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Mon, 13 May 2019 20:22:57 -0500 Subject: [PATCH 03/20] Egg moves ViewModel exposes data the UI needs. --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 24 ++++++++++++++--- src/HexManiac.Core/ViewModels/DataFormats.cs | 8 ++++-- src/HexManiac.Tests/EggMoveTests.cs | 27 ++++++++++++++++++++ 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 728179c1..346f14c4 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -6,7 +6,7 @@ using System.Linq; namespace HavenSoft.HexManiac.Core.Models.Runs { public class EggMoveRun : IFormattedRun { - private const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move + public const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move private readonly IDataModel model; public int Start { get; } @@ -26,15 +26,29 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } } + private IReadOnlyList cachedPokenames, cachedMovenames; + private int lastFormatRequest = int.MinValue; public IDataFormat CreateDataFormat(IDataModel data, int dataIndex) { Debug.Assert(data == model); + if (dataIndex != lastFormatRequest + 1) { + cachedPokenames = ArrayRunEnumSegment.GetOptions(model, "pokenames") ?? new List(); + cachedMovenames = ArrayRunEnumSegment.GetOptions(model, "movenames") ?? new List(); + } + lastFormatRequest = dataIndex; + var position = dataIndex - Start; var groupStart = position % 2 == 1 ? position - 1 : position; var value = data.ReadMultiByteValue(groupStart, 2); if (value >= MagicNumber) { - return new EggSection(); + value -= MagicNumber; + string content = cachedPokenames.Count > value ? cachedPokenames[value] : value.ToString(); + if (value == 0xFFFF - MagicNumber) content = string.Empty; + if (content.StartsWith("\"")) content = content.Substring(1); + if (content.EndsWith("\"")) content = content.Substring(0, content.Length - 1); + return new EggSection(groupStart, position, $"[{content}]"); } else { - return new EggItem(); + string content = cachedMovenames.Count > value ? cachedMovenames[value] : value.ToString(); + return new EggItem(groupStart, position, content); } } @@ -46,7 +60,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } public IFormattedRun RemoveSource(int source) { - throw new NotImplementedException(); + var sources = PointerSources.ToList(); + sources.Remove(source); + return new EggMoveRun(model, Start) { PointerSources = sources }; } } } diff --git a/src/HexManiac.Core/ViewModels/DataFormats.cs b/src/HexManiac.Core/ViewModels/DataFormats.cs index 7faf472d..647f290b 100644 --- a/src/HexManiac.Core/ViewModels/DataFormats.cs +++ b/src/HexManiac.Core/ViewModels/DataFormats.cs @@ -233,9 +233,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public class EggSection : IDataFormat { public int Source { get; } public int Position { get; } - public int Length { get; } + public int Length => 2; public string SectionName { get; } + public EggSection(int source, int position, string name) => (Source, Position, SectionName) = (source, position, name); + public bool Equals(IDataFormat other) { if (other is EggSection that) { return that.SectionName == SectionName && @@ -253,7 +255,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public int Position { get; } public int Length { get; } public string ItemName { get; } - public int Index { get; } + + public EggItem(int source, int position, string name) => (Source, Position, ItemName) = (source, position, name); + public bool Equals(IDataFormat other) { if (other is EggItem that) { return that.ItemName == ItemName && diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index a0840b56..876e6a8f 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -1,5 +1,7 @@ using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels; +using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using System.Linq; using Xunit; @@ -13,6 +15,14 @@ namespace HavenSoft.HexManiac.Tests { data = Enumerable.Range(0, 0x200).Select(i => (byte)0xFF).ToArray(); model = new PokemonModel(data); viewPort = new ViewPort("file.gba", model); + + viewPort.Goto.Execute("000080"); + viewPort.Edit("^pokenames[name\"\"8]8 \"Bob\" \"Steve\" \"Carl\" \"Sam\" \"Bryan\" \"Ryan\" \"Ian\" \"Matt\""); + + viewPort.Goto.Execute("000100"); + viewPort.Edit("^movenames[name\"\"8]8 \"Fire\" \"Water\" \"Earth\" \"Wind\" \"Light\" \"Dark\" \"Normal\" \"Magic\""); + + viewPort.Goto.Execute("000000"); } [Fact] @@ -21,5 +31,22 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal(2, model.GetNextRun(0).Length); } + + [Fact] + public void CanSeeEggMoveStreamWithCorrectFormat() { + var token = new ModelDelta(); + model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl + model.WriteMultiByteValue(2, 2, token, 3); // Wind + + viewPort.Edit("^eggmoves`egg` "); + + Assert.Equal(6, model.GetNextRun(0).Length); + var section = (EggSection)viewPort[1, 0].Format; + var item = (EggItem)viewPort[2, 0].Format; + var endSection = (EggSection)viewPort[5, 0].Format; + Assert.Equal("[Carl]", section.SectionName); + Assert.Equal("Wind", item.ItemName); + Assert.Equal("[]", endSection.SectionName); + } } } From 0efa5a4c59eaf687c8764f11bac87b40bee4351e Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Tue, 14 May 2019 21:18:24 -0500 Subject: [PATCH 04/20] Autosearch for egg move stream Altair messes this up, because in that fangame, the egg moves were edited but the limiter wasn't. So the two lengths don't match, and the egg-moves were filled with "00 00". To support other ROMs with this kind of problem, accept early exits from the format searcher. --- src/HexManiac.Core/Models/AutoSearchModel.cs | 11 ++++- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 51 +++++++++++++++++++- src/HexManiac.Tests/AutoSearchTests.cs | 20 +++++++- 3 files changed, 76 insertions(+), 6 deletions(-) diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index 4e086a2a..5cc52945 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -38,6 +38,7 @@ namespace HavenSoft.HexManiac.Core.Models { DecodeHeader(); DecodeNameArrays(); DecodeDataArrays(); + DecodeStreams(); } } @@ -54,12 +55,12 @@ namespace HavenSoft.HexManiac.Core.Models { private void DecodeNameArrays() { // movenames if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var movenames)) { - ObserveAnchorWritten(noChangeDelta, "movenames", movenames); + ObserveAnchorWritten(noChangeDelta, EggMoveRun.MoveNamesTable, movenames); } // pokenames if (TrySearch(this, noChangeDelta, "[name\"\"11]", out var pokenames)) { - ObserveAnchorWritten(noChangeDelta, "pokenames", pokenames); + ObserveAnchorWritten(noChangeDelta, EggMoveRun.PokemonNameTable, pokenames); } // abilitynames / trainer names @@ -116,5 +117,11 @@ namespace HavenSoft.HexManiac.Core.Models { // @3D4294 ^itemicons[image<> palette<>]items // @4886E8 ^movedescriptions[description<>]354 } + + private void DecodeStreams() { + if (EggMoveRun.TrySearch(this, noChangeDelta, out var eggmoves)) { + ObserveAnchorWritten(noChangeDelta, "eggmoves", eggmoves); + } + } } } diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 346f14c4..c6ac4d84 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -7,6 +7,8 @@ using System.Linq; namespace HavenSoft.HexManiac.Core.Models.Runs { public class EggMoveRun : IFormattedRun { public const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move + public const string PokemonNameTable = "pokenames"; + public const string MoveNamesTable = "movenames"; private readonly IDataModel model; public int Start { get; } @@ -26,13 +28,58 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } } + public static bool TrySearch(IDataModel data, ModelDelta token, out EggMoveRun eggmoves) { + eggmoves = default; + var pokenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, PokemonNameTable)) as ArrayRun; + var movenames = data.GetNextRun(data.GetAddressFromAnchor(token, -1, MoveNamesTable)) as ArrayRun; + if (pokenames == null || movenames == null) return false; + + for (var run = data.GetNextRun(0); run.Start < int.MaxValue; run = data.GetNextRun(run.Start + run.Length)) { + if (run is ArrayRun || run is PCSRun || run.PointerSources == null) continue; + + // verify expected pointers to this + if (run.PointerSources.Count != 2) continue; + + // verify limiter + var length = data.ReadMultiByteValue(run.PointerSources[1] - 4, 4); + + // we just read the 'length' from basically a random byte... verify that it could make sense as a length + if (length < 0) continue; + if (run.Start + length * 2 + 3 < 0) continue; + if (run.Start + length * 2 + 3 > data.Count) continue; + var endValue = data.ReadMultiByteValue(run.Start + length * 2 + 2, 2); + if (endValue != 0xFFFF) continue; + + // verify content + bool possibleMatch = true; + for (int i = 0; i < length - 2; i++) { + var value = data.ReadMultiByteValue(run.Start + i * 2, 2); + if (value == 0xFFFF) break; // early exit, the data was edited, but that's ok. Everything still matches up. + if (value >= MagicNumber) { + value -= MagicNumber; + if (value < pokenames.ElementCount) continue; + } + if (value < movenames.ElementCount) continue; + possibleMatch = false; + break; + } + if (!possibleMatch) continue; + + // content is correct, length is correct: this is it! + eggmoves = new EggMoveRun(data, run.Start); + return true; + } + + return false; + } + private IReadOnlyList cachedPokenames, cachedMovenames; private int lastFormatRequest = int.MinValue; public IDataFormat CreateDataFormat(IDataModel data, int dataIndex) { Debug.Assert(data == model); if (dataIndex != lastFormatRequest + 1) { - cachedPokenames = ArrayRunEnumSegment.GetOptions(model, "pokenames") ?? new List(); - cachedMovenames = ArrayRunEnumSegment.GetOptions(model, "movenames") ?? new List(); + cachedPokenames = ArrayRunEnumSegment.GetOptions(model, PokemonNameTable) ?? new List(); + cachedMovenames = ArrayRunEnumSegment.GetOptions(model, MoveNamesTable) ?? new List(); } lastFormatRequest = dataIndex; diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 81003cbd..350fc9ff 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -30,7 +30,7 @@ namespace HavenSoft.HexManiac.Tests { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); - var address = model.GetAddressFromAnchor(noChange, -1, "pokenames"); + var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.PokemonNameTable); var run = (ArrayRun)model.GetNextAnchor(address); Assert.Equal(412, run.ElementCount); } @@ -41,7 +41,7 @@ namespace HavenSoft.HexManiac.Tests { var model = LoadModel(game); var noChange = new NoDataChangeDeltaModel(); - var address = model.GetAddressFromAnchor(noChange, -1, "movenames"); + var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.MoveNamesTable); var run = (ArrayRun)model.GetNextAnchor(address); Assert.Equal(355, run.ElementCount); } @@ -141,6 +141,22 @@ namespace HavenSoft.HexManiac.Tests { for (int i = 0; i < compareSet.Length; i++) Assert.Equal(compareSet[i], poundStats[i]); } + [SkippableTheory] + [MemberData(nameof(PokemonGames))] + public void EggMoveDataFound(string game) { + var model = LoadModel(game); + var noChange = new NoDataChangeDeltaModel(); + + var address = model.GetAddressFromAnchor(noChange, -1, "eggmoves"); + var run = (EggMoveRun)model.GetNextAnchor(address); + + Assert.Equal(2, run.PointerSources.Count); + var expectedLastElement = model.ReadMultiByteValue(run.PointerSources[1] - 4, 4); + var expectedLength = expectedLastElement + 1; + var actualLength = run.Length / 2 - 1; // remove the closing element. + Assert.InRange(actualLength, 790, expectedLength); + } + /// /// Loading the model can take a while. /// We want to know that loading the model created the correct arrays, From 317582ff68833c3207ea88f1779baf03d1ada6ba Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Tue, 14 May 2019 21:49:45 -0500 Subject: [PATCH 05/20] UI for egg move stream --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 3 +- .../ViewModels/Visitors/ContextItemFactory.cs | 4 +- .../Implementations/FormatDrawer.cs | 109 +++++++----------- 3 files changed, 47 insertions(+), 69 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index c6ac4d84..16bfa686 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -85,7 +85,8 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { var position = dataIndex - Start; var groupStart = position % 2 == 1 ? position - 1 : position; - var value = data.ReadMultiByteValue(groupStart, 2); + position -= groupStart; + var value = data.ReadMultiByteValue(Start + groupStart, 2); if (value >= MagicNumber) { value -= MagicNumber; string content = cachedPokenames.Count > value ? cachedPokenames[value] : value.ToString(); diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs index 82cd6acc..083d80dc 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs @@ -117,9 +117,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.AddRange(GetTableChildren(arrayRun)); } - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) { } - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + public void Visit(EggItem item, byte data) { } private IEnumerable GetTableChildren(ArrayRun array) { if (ViewPort.Tools.TableTool.Append.CanExecute(null)) { diff --git a/src/HexManiac.WPF/Implementations/FormatDrawer.cs b/src/HexManiac.WPF/Implementations/FormatDrawer.cs index 246a9100..8394b396 100644 --- a/src/HexManiac.WPF/Implementations/FormatDrawer.cs +++ b/src/HexManiac.WPF/Implementations/FormatDrawer.cs @@ -46,14 +46,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations { var content = dataFormat.CurrentText; - var text = new FormattedText( - content, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - brush, - 1.0); + var text = CreateText(content, FontSize, brush); var offset = CellTextOffset; var widthOverflow = text.Width - HexContent.CellWidth * dataFormat.EditWidth; @@ -73,18 +66,10 @@ namespace HavenSoft.HexManiac.WPF.Implementations { if (dataFormat.Destination < 0) brush = Brush(nameof(Theme.Error)); Underline(brush, dataFormat.Position == 0, dataFormat.Position == 3); - var typeface = new Typeface("Consolas"); var destination = dataFormat.DestinationAsText; if (destination.Length > 13) destination = destination.Substring(0, 11) + "…>"; var xOffset = 51 - (dataFormat.Position * HexContent.CellWidth) - destination.Length * 4.2; // centering - var text = new FormattedText( - destination, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - brush, - 1.0); + var text = CreateText(destination, FontSize, brush); if (dataFormat.Position > Position.X || Position.X - dataFormat.Position > modelWidth - 4) { context.PushClip(rectangleGeometry); @@ -104,15 +89,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations { } public void Visit(PCS pcs, byte data) { - var typeface = new Typeface("Consolas"); - var text = new FormattedText( - pcs.ThisCharacter, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - Brush(nameof(Theme.Text1)), - 1.0); + var text = CreateText(pcs.ThisCharacter, FontSize, Brush(nameof(Theme.Text1))); var xOffset = 1 - pcs.ThisCharacter.Length; context.DrawText(text, new Point(CellTextOffset.X + xOffset, CellTextOffset.Y)); @@ -124,33 +101,16 @@ namespace HavenSoft.HexManiac.WPF.Implementations { public void Visit(ErrorPCS pcs, byte data) { var brush = Brush(nameof(Theme.Error)); - var typeface = new Typeface("Consolas"); var content = data.ToString("X2"); - var text = new FormattedText( - content, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - brush, - 1.0); + var text = CreateText(content, FontSize, brush); context.DrawText(text, CellTextOffset); } public void Visit(Ascii ascii, byte data) { - var typeface = new Typeface("Consolas"); - var text = new FormattedText( - ascii.ThisCharacter.ToString(), - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - Brush(nameof(Theme.Text2)), - 1.0); - + var text = CreateText(ascii.ThisCharacter.ToString(), FontSize, Brush(nameof(Theme.Text2))); context.DrawText(text, CellTextOffset); } @@ -159,15 +119,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations { var stringValue = integer.Value.ToString(); - var typeface = new Typeface("Consolas"); - var text = new FormattedText( - stringValue, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize, - Brush(nameof(Theme.Data1)), - 1.0); + var text = CreateText(stringValue, FontSize, Brush(nameof(Theme.Data1))); var xOffset = CellTextOffset.X; xOffset += HexContent.CellWidth / 2 * (integer.Length - 1); // adjust based on number of cells to use @@ -179,16 +131,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations { if (integerEnum.Position != 0) return; var stringValue = integerEnum.Value; - - var typeface = new Typeface("Consolas"); - var text = new FormattedText( - stringValue, - CultureInfo.CurrentCulture, - FlowDirection.LeftToRight, - typeface, - FontSize * 3 / 4, - Brush(nameof(Theme.Data2)), - 1.0); + var text = CreateText(stringValue, FontSize * 3 / 4, Brush(nameof(Theme.Data2))); var xOffset = CellTextOffset.X / 2; context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * integerEnum.Length, HexContent.CellHeight))); @@ -196,9 +139,31 @@ namespace HavenSoft.HexManiac.WPF.Implementations { context.Pop(); } - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) { + if (section.Position != 0) return; + var name = section.SectionName; - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + var text = CreateText(name, FontSize * 3 / 4, Brush(nameof(Theme.Stream1))); + var characterWidth = text.Width / name.Length; + var xOffset = HexContent.CellWidth - name.Length * characterWidth / 2; + if (xOffset < 0) xOffset = 0; + context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * 2, HexContent.CellHeight))); + context.DrawText(text, new Point(xOffset, CellTextOffset.Y + 2)); + context.Pop(); + } + + public void Visit(EggItem item, byte data) { + if (item.Position != 0) return; + var name = item.ItemName; + + var text = CreateText(name, FontSize * 3 / 4, Brush(nameof(Theme.Stream2))); + var characterWidth = text.Width / name.Length; + var xOffset = HexContent.CellWidth - name.Length * characterWidth / 2; + if (xOffset < 0) xOffset = 0; + context.PushClip(new RectangleGeometry(new Rect(0, 0, HexContent.CellWidth * 2, HexContent.CellHeight))); + context.DrawText(text, new Point(xOffset, CellTextOffset.Y + 2)); + context.Pop(); + } private void Underline(Brush brush, bool isStart, bool isEnd) { int startPoint = isStart ? 5 : 0; @@ -232,6 +197,18 @@ namespace HavenSoft.HexManiac.WPF.Implementations { noneVisualCache.AddRange(text); } + private static readonly Typeface consolas = new Typeface("Consolas"); + private static FormattedText CreateText(string text, double size, Brush color) { + return new FormattedText( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + consolas, + FontSize * 3 / 4, + color, + 1.0); + } + private static SolidColorBrush Brush(string name) { return (SolidColorBrush)Application.Current.Resources.MergedDictionaries[0][name]; } From b22f693f8cdbdf8d2c8a14b1e7c8cead830eeb23 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 15 May 2019 06:40:39 -0500 Subject: [PATCH 06/20] Egg Move selection is done in pairs --- src/HexManiac.Core/ViewModels/ViewPort.cs | 14 +++++++++++--- src/HexManiac.Tests/EggMoveTests.cs | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index aa99d180..d9785f93 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -1035,12 +1035,20 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var run = Model.GetNextRun(index); if (run.Start > index) return (p, p); - if (run is PointerRun) return (scroll.DataIndexToViewPoint(run.Start), scroll.DataIndexToViewPoint(run.Start + run.Length - 1)); + (Point, Point) pair(int start, int end) => (scroll.DataIndexToViewPoint(start), scroll.DataIndexToViewPoint(end)); + + if (run is PointerRun) return pair(run.Start, run.Start + run.Length - 1); + if (run is EggMoveRun) { + var even = (index - run.Start) % 2 == 0; + if (even) return pair(index, index + 1); + return pair(index - 1, index); + } if (!(run is ArrayRun array)) return (p, p); var offset = array.ConvertByteOffsetToArrayOffset(index); - if (array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Pointer || array.ElementContent[offset.SegmentIndex].Type == ElementContentType.Integer) { - return (scroll.DataIndexToViewPoint(offset.SegmentStart), scroll.DataIndexToViewPoint(offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1)); + var type = array.ElementContent[offset.SegmentIndex].Type; + if (type == ElementContentType.Pointer || type == ElementContentType.Integer) { + return pair(offset.SegmentStart, offset.SegmentStart + array.ElementContent[offset.SegmentIndex].Length - 1); } return (p, p); diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 876e6a8f..1fca59d2 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -48,5 +48,20 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal("Wind", item.ItemName); Assert.Equal("[]", endSection.SectionName); } + + [Fact] + public void SelectionDoneInPairs() { + var token = new ModelDelta(); + model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl + model.WriteMultiByteValue(2, 2, token, 3); // Wind + viewPort.Edit("^eggmoves`egg` "); + + viewPort.SelectionStart = new Point(2, 0); // should select "Wind" + Assert.True(viewPort.IsSelected(new Point(3, 0))); + + viewPort.MoveSelectionStart.Execute(Direction.Right); // should select "[]" + Assert.True(viewPort.IsSelected(new Point(4, 0))); + Assert.True(viewPort.IsSelected(new Point(5, 0))); + } } } From b8ee9253d6cbd632d06c829a0ac6ac45717bcdfb Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 15 May 2019 19:26:23 -0500 Subject: [PATCH 07/20] fix ui bug --- src/HexManiac.WPF/Implementations/FormatDrawer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HexManiac.WPF/Implementations/FormatDrawer.cs b/src/HexManiac.WPF/Implementations/FormatDrawer.cs index 8394b396..efb3a83a 100644 --- a/src/HexManiac.WPF/Implementations/FormatDrawer.cs +++ b/src/HexManiac.WPF/Implementations/FormatDrawer.cs @@ -204,7 +204,7 @@ namespace HavenSoft.HexManiac.WPF.Implementations { CultureInfo.CurrentCulture, FlowDirection.LeftToRight, consolas, - FontSize * 3 / 4, + size, color, 1.0); } From e6e1fd3f2309cfdd43a1e286b89f8c384a7696c7 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 15 May 2019 22:02:35 -0500 Subject: [PATCH 08/20] Egg Moves: Inline Editing / Autocomplete --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 35 +++++++++++++- src/HexManiac.Core/ViewModels/DataFormats.cs | 21 +++++---- src/HexManiac.Core/ViewModels/ViewPort.cs | 10 +++- .../Visitors/CompleteEditOperation.cs | 46 +++++++++++++++++-- .../ViewModels/Visitors/ContinueCellEdit.cs | 14 ++++-- .../ViewModels/Visitors/StartCellEdit.cs | 33 ++++++------- src/HexManiac.Tests/EggMoveTests.cs | 14 ++++++ 7 files changed, 137 insertions(+), 36 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 16bfa686..2efbf2a2 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -93,10 +93,10 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (value == 0xFFFF - MagicNumber) content = string.Empty; if (content.StartsWith("\"")) content = content.Substring(1); if (content.EndsWith("\"")) content = content.Substring(0, content.Length - 1); - return new EggSection(groupStart, position, $"[{content}]"); + return new EggSection(groupStart + Start, position, $"[{content}]"); } else { string content = cachedMovenames.Count > value ? cachedMovenames[value] : value.ToString(); - return new EggItem(groupStart, position, content); + return new EggItem(groupStart + Start, position, content); } } @@ -112,5 +112,36 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { sources.Remove(source); return new EggMoveRun(model, Start) { PointerSources = sources }; } + + public int GetPokemonNumber(string input) { + if (input.StartsWith("[")) input = input.Substring(1, input.Length - 2); + var names = cachedPokenames.Select(name => Dequote(name).ToLower()).ToList(); + return GetNumber(input.ToLower(), names); + } + + public int GetMoveNumber(string input) { + input = Dequote(input).ToLower(); + var names = cachedMovenames.Select(name => Dequote(name).ToLower()).ToList(); + return GetNumber(input, names); + } + + public IEnumerable GetAutoCompleteOptions() { + var pokenames = cachedPokenames.Select(name => $"[{name}]"); + var movenames = cachedMovenames.Select(name => name + " "); + return pokenames.Concat(movenames); + } + + private static int GetNumber(string input, IList names) { + var matchIndex = names.IndexOf(input); + if (matchIndex != -1) return matchIndex; + var match = names.FirstOrDefault(name => name.Contains(input)); + if (match == null) return -1; + return names.IndexOf(match); + } + + private static string Dequote(string name) { + if (!name.StartsWith("\"")) return name; + return name.Substring(1, name.Length - 2); + } } } diff --git a/src/HexManiac.Core/ViewModels/DataFormats.cs b/src/HexManiac.Core/ViewModels/DataFormats.cs index 647f290b..7cb9d0d6 100644 --- a/src/HexManiac.Core/ViewModels/DataFormats.cs +++ b/src/HexManiac.Core/ViewModels/DataFormats.cs @@ -11,6 +11,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { void Visit(IDataFormatVisitor visitor, byte data); } + public interface IDataFormatInstance : IDataFormat { + int Source { get; } // the beginning of the run/group that this instance belongs to + int Position { get; } // the index in the run/group that this instance belongs to + } + public interface IDataFormatVisitor { void Visit(Undefined dataFormat, byte data); void Visit(None dataFormat, byte data); @@ -84,7 +89,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { } } - public class Pointer : IDataFormat { + public class Pointer : IDataFormatInstance { public const int NULL = -0x08000000; public int Source { get; } // 6 hex digits public int Position { get; } // 0 through 3 @@ -129,7 +134,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class PCS : IDataFormat { + public class PCS : IDataFormatInstance { public int Source { get; } public int Position { get; } public string FullString { get; } @@ -145,7 +150,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class EscapedPCS : IDataFormat { + public class EscapedPCS : IDataFormatInstance { public int Source { get; } public int Position { get; } public string FullString { get; } @@ -161,7 +166,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class ErrorPCS : IDataFormat { + public class ErrorPCS : IDataFormatInstance { public int Source { get; } public int Position { get; } public string FullString { get; } @@ -177,7 +182,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class Ascii : IDataFormat { + public class Ascii : IDataFormatInstance { public int Source { get; } public int Position { get; } public char ThisCharacter { get; } @@ -192,7 +197,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class Integer : IDataFormat { + public class Integer : IDataFormatInstance { public int Source { get; } public int Position { get; } public int Value { get; } @@ -230,7 +235,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public override void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class EggSection : IDataFormat { + public class EggSection : IDataFormatInstance { public int Source { get; } public int Position { get; } public int Length => 2; @@ -250,7 +255,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.DataFormats { public void Visit(IDataFormatVisitor visitor, byte data) => visitor.Visit(this, data); } - public class EggItem : IDataFormat { + public class EggItem : IDataFormatInstance { public int Source { get; } public int Position { get; } public int Length { get; } diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index d9785f93..9a6b8710 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -141,7 +141,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels { ClearEdits(location); } else { var endEdit = " "; - if (underEdit.CurrentText.Count(c => c == '"') % 2 == 1) endEdit = "\""; + if (underEdit.CurrentText.Count(c => c == StringDelimeter) % 2 == 1) endEdit = StringDelimeter.ToString(); + var originalFormat = underEdit.OriginalFormat; + if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat; + if (underEdit.CurrentText.StartsWith("[") && (originalFormat is EggSection || originalFormat is EggItem)) endEdit = "]"; currentView[location.X, location.Y] = new HexElement(element.Value, underEdit.Edit(endEdit)); if (!TryCompleteEdit(location)) ClearEdits(location); } @@ -950,6 +953,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } private IReadOnlyList GetAutocompleteOptions(IDataFormat originalFormat, string newText, int selectedIndex = -1) { + if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat; if (newText.StartsWith(PointerStart.ToString())) { return Model.GetNewPointerAutocompleteOptions(newText, selectedIndex); } else if (newText.StartsWith(GotoMarker.ToString())) { @@ -959,6 +963,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var segment = (ArrayRunEnumSegment)array.ElementContent[array.ConvertByteOffsetToArrayOffset(intEnum.Source).SegmentIndex]; var options = segment.GetOptions(Model).Select(option => option + " "); // autocomplete needs to complete after selection, so add a space return AutoCompleteSelectionItem.Generate(options.Where(option => option.MatchesPartial(newText)), selectedIndex); + } else if (originalFormat is EggSection || originalFormat is EggItem) { + var eggRun = (EggMoveRun)Model.GetNextRun(((IDataFormatInstance)originalFormat).Source); + var allOptions = eggRun.GetAutoCompleteOptions(); + return AutoCompleteSelectionItem.Generate(allOptions.Where(option => option.MatchesPartial(newText)), selectedIndex); } else { throw new NotImplementedException(); } diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index f6bef3e6..1c3983d7 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -19,8 +19,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public int NewDataIndex { get; private set; } // for completed edits, where should the selection move to? public bool DataMoved { get; private set; } public string MessageText { get; private set; } // is there a message to display to the user? For example, when data gets moved. - public string ErrorText { get; private set; } // is there an error to display to the user? For example, invalide pointer - public HexElement NewCell { get; private set; } // if result is true and this is not null, asign this one value back to the one cell + public string ErrorText { get; private set; } // is there an error to display to the user? For example, invalid pointer + public HexElement NewCell { get; private set; } // if result is true and this is not null, assign this one value back to the one cell // and refresh the one cell (along with any other UnderEdit cells) // if result is true and this _is_ null, then the entire screen needs to be refreshed. @@ -109,9 +109,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } } - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) => CompleteEggEdit(); - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + public void Visit(EggItem item, byte data) => CompleteEggEdit(); private void CompleteIntegerEdit(Integer integer) { if (!int.TryParse(CurrentText, out var result)) { @@ -283,5 +283,43 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { CurrentChange.ChangeData(Model, memoryLocation, byteValue); NewDataIndex = memoryLocation + 1; } + + private void CompleteEggEdit() { + var endChar = CurrentText[CurrentText.Length - 1]; + if ($"] {StringDelimeter}".All(c => endChar != c)) return; + if (CurrentText.Count(c => c == StringDelimeter) % 2 != 0) return; + + NewDataIndex = memoryLocation + 2; + Result = true; + var run = (EggMoveRun)Model.GetNextRun(memoryLocation); + + if (CurrentText == "[]") { + Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, 0xFFFF); + // TODO update length + } else if (CurrentText.EndsWith("]")) { + var value = run.GetPokemonNumber(CurrentText); + if (value == -1) { + ErrorText = $"Could not parse {CurrentText} as a pokemon name"; + NewDataIndex -= 2; + } else { + Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value + EggMoveRun.MagicNumber); + } + } else { + var text = CurrentText.Trim(); + var value = run.GetMoveNumber(text); + if (value == -1) { + // wasn't a move... try again as a pokemon even though they didn't use the [] + value = run.GetPokemonNumber(text); + if (value == -1) { + ErrorText = $"Could not parse {text} as a move name or pokemon name"; + NewDataIndex -= 2; + } else { + Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value + EggMoveRun.MagicNumber); + } + } else { + Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value); + } + } + } } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs index 0fe44c37..6f700917 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs @@ -72,8 +72,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { char.IsWhiteSpace(Input); } - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); - - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) => VisitEgg(); + public void Visit(EggItem item, byte data) => VisitEgg(); + public void VisitEgg() { + var specialCharacters = ". '-\\"; // mr. mime, farfetch'd, double-edge, nidoran + if (UnderEdit.CurrentText[0] == '[') specialCharacters += ']'; + if (UnderEdit.CurrentText[0] == StringDelimeter) specialCharacters += StringDelimeter; + Result = + char.IsLetterOrDigit(Input) || + specialCharacters.Contains(Input) || + char.IsWhiteSpace(Input); + } } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index a46a0fe0..eb14afb5 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs @@ -67,10 +67,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { } public void Visit(Pointer pointer, byte data) { - if (Input != PointerStart && !char.IsLetterOrDigit(Input)) { - Result = false; - return; - } + if (Input != PointerStart && !char.IsLetterOrDigit(Input)) return; var editText = Input.ToString(); // if the user tries to edit the pointer but forgets the opening bracket, add it for them. @@ -89,10 +86,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(PCS pcs, byte data) { if (Model.GetNextRun(MemoryLocation) is ArrayRun array) { var offsets = array.ConvertByteOffsetToArrayOffset(MemoryLocation); - if (offsets.SegmentStart == MemoryLocation && Input == ' ') { - Result = false; // don't let it start with a space unless it's in quotes (for copy/paste) - return; - } + // don't let it start with a space unless it's in quotes (for copy/paste) + if (offsets.SegmentStart == MemoryLocation && Input == ' ') return; } Result = Input == StringDelimeter || PCSString.PCS.Any(str => str != null && str.StartsWith(Input.ToString())); @@ -109,20 +104,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(Ascii ascii, byte data) => Result = true; public void Visit(Integer intFormat, byte data) { - if (!intFormat.CanStartWithCharacter(Input)) { - Result = false; - return; - } + if (!intFormat.CanStartWithCharacter(Input)) return; NewFormat = new UnderEdit(intFormat, Input.ToString(), intFormat.Length, null); Result = true; } public void Visit(IntegerEnum integer, byte data) { - if (!integer.CanStartWithCharacter(Input)) { - Result = false; - return; - } + if (!integer.CanStartWithCharacter(Input)) return; var arrayRun = (ArrayRun)Model.GetNextRun(MemoryLocation); var offsets = arrayRun.ConvertByteOffsetToArrayOffset(MemoryLocation); @@ -133,8 +122,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Result = true; } - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) => VisitEgg(section); + public void Visit(EggItem item, byte data) => VisitEgg(item); + public void VisitEgg(IDataFormat eggFormat) { + if (!char.IsLetterOrDigit(Input) && !$"{StringDelimeter}[".Contains(Input)) return; - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + var stream = (EggMoveRun)Model.GetNextRun(MemoryLocation); + var allOptions = stream.GetAutoCompleteOptions(); + var autocomplete = AutoCompleteSelectionItem.Generate(allOptions.Where(option => option.MatchesPartial(Input.ToString())), -1); + NewFormat = new UnderEdit(eggFormat, Input.ToString(), 2, autocomplete); + Result = true; + } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 1fca59d2..652e4fe2 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -63,5 +63,19 @@ namespace HavenSoft.HexManiac.Tests { Assert.True(viewPort.IsSelected(new Point(4, 0))); Assert.True(viewPort.IsSelected(new Point(5, 0))); } + + [Fact] + public void CanEditEggStreamManually() { + var token = new ModelDelta(); + model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl + model.WriteMultiByteValue(2, 2, token, 3); // Wind + viewPort.Edit("^eggmoves`egg` "); + + viewPort.Edit("Dark "); + Assert.Equal(5, model[0]); + + viewPort.Edit("[Bryan]"); + Assert.Equal(EggMoveRun.MagicNumber + 4, model.ReadMultiByteValue(2, 2)); + } } } From 61e1fb1e6d60d7098646ce2156029ae6758ac5d9 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 15 May 2019 22:25:45 -0500 Subject: [PATCH 09/20] Copy for egg runs --- src/HexManiac.Core/Models/PokemonModel.cs | 5 +++++ src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 18 ++++++++++++++++++ src/HexManiac.Tests/EggMoveTests.cs | 15 +++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index ccc7752a..c41a8f65 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -784,6 +784,11 @@ namespace HavenSoft.HexManiac.Core.Models { text.Append(" "); length -= run.Start + run.Length - start; start = run.Start + run.Length; + } else if (run is EggMoveRun eggRun) { + eggRun.AppendTo(this, text, start, length); + text.Append(" "); + length -= run.Start + run.Length - start; + start = run.Start + run.Length; } else { throw new NotImplementedException(); } diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 2efbf2a2..0d956133 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Text; namespace HavenSoft.HexManiac.Core.Models.Runs { public class EggMoveRun : IFormattedRun { @@ -143,5 +144,22 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (!name.StartsWith("\"")) return name; return name.Substring(1, name.Length - 2); } + + public void AppendTo(PokemonModel model, StringBuilder text, int start, int length) { + while (length > 0 && start < Start + Length) { + var value = model.ReadMultiByteValue(start, 2); + if (value >= MagicNumber) { + value -= MagicNumber; + if (value >= cachedPokenames.Count) text.Append($"[{value}]"); + else text.Append($"[{Dequote(cachedPokenames[value])}]"); + } else { + if (value >= cachedMovenames.Count) text.Append($"{value}"); + else text.Append($"{cachedMovenames[value]}"); + } + start += 2; + length -= 2; + if (length > 0 && start < Start + Length) text.Append(" "); + } + } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 652e4fe2..3e45bc00 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -77,5 +77,20 @@ namespace HavenSoft.HexManiac.Tests { viewPort.Edit("[Bryan]"); Assert.Equal(EggMoveRun.MagicNumber + 4, model.ReadMultiByteValue(2, 2)); } + + [Fact] + public void CanCopyPaste() { + var fileSystem = new StubFileSystem(); + var token = new ModelDelta(); + model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl + model.WriteMultiByteValue(2, 2, token, 3); // Wind + viewPort.Edit("^eggmoves`egg` "); + + viewPort.SelectionStart = new Point(0, 0); + viewPort.SelectionEnd = new Point(2, 0); + viewPort.Copy.Execute(fileSystem); + + Assert.Equal("^eggmoves`egg` [Carl] Wind", fileSystem.CopyText.value); + } } } From 0350ea47cd7d0264427db8f31a5c6330e5ecb735 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 07:32:22 -0500 Subject: [PATCH 10/20] Egg Moves End Inline Editing * Extension works * Repointing works --- src/HexManiac.Core/Models/PokemonModel.cs | 2 + src/HexManiac.Core/ViewModels/ViewPort.cs | 1 + .../Visitors/CompleteEditOperation.cs | 27 ++++++++-- src/HexManiac.Tests/EggMoveTests.cs | 54 +++++++++++++------ 4 files changed, 64 insertions(+), 20 deletions(-) diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index c41a8f65..485d84dd 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -1090,6 +1090,8 @@ namespace HavenSoft.HexManiac.Core.Models { newRun = new PCSRun(newStart, run.Length, run.PointerSources); } else if (run is ArrayRun array) { newRun = array.Move(newStart); + } else if (run is EggMoveRun egg) { + newRun = new EggMoveRun(this, newStart); } else { throw new NotImplementedException(); } diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 9a6b8710..cb3aba63 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -1117,6 +1117,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { RefreshBackingData(); } var run = Model.GetNextRun(completeEditOperation.NewDataIndex); + if (run.Start > completeEditOperation.NewDataIndex) run = new NoInfoRun(Model.Count); if (completeEditOperation.DataMoved) UpdateToolsFromSelection(run.Start); if (run is ArrayRun) Tools.Schedule(Tools.TableTool.DataForCurrentRunChanged); if (run is ArrayRun || run is PCSRun) Tools.Schedule(Tools.StringTool.DataForCurrentRunChanged); diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 1c3983d7..f9e3fa5f 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -302,7 +302,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { ErrorText = $"Could not parse {CurrentText} as a pokemon name"; NewDataIndex -= 2; } else { - Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value + EggMoveRun.MagicNumber); + WriteNormalEggEdit(run, value + EggMoveRun.MagicNumber); } } else { var text = CurrentText.Trim(); @@ -314,12 +314,33 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { ErrorText = $"Could not parse {text} as a move name or pokemon name"; NewDataIndex -= 2; } else { - Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value + EggMoveRun.MagicNumber); + WriteNormalEggEdit(run, value + EggMoveRun.MagicNumber); } } else { - Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value); + WriteNormalEggEdit(run, value); } } } + + /// + /// Before we write this change to the model, see if we need to extend the egg run to make it fit. + /// + private void WriteNormalEggEdit(EggMoveRun run, int value) { + int memoryLocation = this.memoryLocation; + var initialItemValue = Model.ReadMultiByteValue(memoryLocation, 2); + Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, value); + if (initialItemValue == 0xFFFF) { + var newRun = Model.RelocateForExpansion(CurrentChange, run, run.Length + 2); + if (newRun.Start != run.Start) { + MessageText = $"Egg Moves were automatically moved to {newRun.Start.ToString("X6")}. Pointers were updated."; + memoryLocation += newRun.Start - run.Start; + NewDataIndex = memoryLocation + 2; + DataMoved = true; + } + // TODO write FFFF from here to the end of the current run + Model.WriteMultiByteValue(memoryLocation + 2, 2, CurrentChange, 0xFFFF); + Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, newRun.Start)); + } + } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 3e45bc00..774016e5 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -2,6 +2,7 @@ using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System.Collections.Generic; using System.Linq; using Xunit; @@ -10,6 +11,9 @@ namespace HavenSoft.HexManiac.Tests { private readonly byte[] data; private readonly PokemonModel model; private readonly ViewPort viewPort; + private readonly List messages = new List(); + + #region Setup public EggMoveTests() { data = Enumerable.Range(0, 0x200).Select(i => (byte)0xFF).ToArray(); @@ -23,8 +27,20 @@ namespace HavenSoft.HexManiac.Tests { viewPort.Edit("^movenames[name\"\"8]8 \"Fire\" \"Water\" \"Earth\" \"Wind\" \"Light\" \"Dark\" \"Normal\" \"Magic\""); viewPort.Goto.Execute("000000"); + + viewPort.OnMessage += (sender, e) => messages.Add(e); } + private void CreateSimpleRun() { + var token = new ModelDelta(); + model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl + model.WriteMultiByteValue(2, 2, token, 3); // Wind + + viewPort.Edit("^eggmoves`egg` "); + } + + #endregion + [Fact] public void CanCreateEggMoveStream() { viewPort.Edit("^eggmoves`egg` "); @@ -34,11 +50,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void CanSeeEggMoveStreamWithCorrectFormat() { - var token = new ModelDelta(); - model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl - model.WriteMultiByteValue(2, 2, token, 3); // Wind - - viewPort.Edit("^eggmoves`egg` "); + CreateSimpleRun(); Assert.Equal(6, model.GetNextRun(0).Length); var section = (EggSection)viewPort[1, 0].Format; @@ -51,10 +63,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void SelectionDoneInPairs() { - var token = new ModelDelta(); - model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl - model.WriteMultiByteValue(2, 2, token, 3); // Wind - viewPort.Edit("^eggmoves`egg` "); + CreateSimpleRun(); viewPort.SelectionStart = new Point(2, 0); // should select "Wind" Assert.True(viewPort.IsSelected(new Point(3, 0))); @@ -66,10 +75,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void CanEditEggStreamManually() { - var token = new ModelDelta(); - model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl - model.WriteMultiByteValue(2, 2, token, 3); // Wind - viewPort.Edit("^eggmoves`egg` "); + CreateSimpleRun(); viewPort.Edit("Dark "); Assert.Equal(5, model[0]); @@ -80,11 +86,8 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void CanCopyPaste() { + CreateSimpleRun(); var fileSystem = new StubFileSystem(); - var token = new ModelDelta(); - model.WriteMultiByteValue(0, 2, token, EggMoveRun.MagicNumber + 2); // Carl - model.WriteMultiByteValue(2, 2, token, 3); // Wind - viewPort.Edit("^eggmoves`egg` "); viewPort.SelectionStart = new Point(0, 0); viewPort.SelectionEnd = new Point(2, 0); @@ -92,5 +95,22 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal("^eggmoves`egg` [Carl] Wind", fileSystem.CopyText.value); } + + [Fact] + public void RunAutoExtends() { + viewPort.Edit("^eggmoves`egg` "); + viewPort.Edit("[Carl]"); + + Assert.Equal(4, model.GetNextRun(0).Length); + } + + [Fact] + public void RunAutoMoves() { + viewPort.Edit("^eggmoves`egg` "); + model.WriteMultiByteValue(2, 2, new ModelDelta(), 0x0206); + viewPort.Edit("[Carl]"); + + Assert.Single(messages); + } } } From aef8d5fe622ba14b951532e601f499b77440103d Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 20:44:41 -0500 Subject: [PATCH 11/20] Contract egg run after [] --- .../ViewModels/Visitors/CompleteEditOperation.cs | 9 ++++++--- src/HexManiac.Tests/EggMoveTests.cs | 9 +++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index f9e3fa5f..7c018ef0 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -295,7 +295,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { if (CurrentText == "[]") { Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, 0xFFFF); - // TODO update length + // clear all data after this and shorten the run + for (int i = memoryLocation + 2; i < run.Start + run.Length; i += 2) { + Model.WriteMultiByteValue(i, 2, CurrentChange, 0xFFFF); + } + Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, run.Start)); } else if (CurrentText.EndsWith("]")) { var value = run.GetPokemonNumber(CurrentText); if (value == -1) { @@ -337,9 +341,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { NewDataIndex = memoryLocation + 2; DataMoved = true; } - // TODO write FFFF from here to the end of the current run Model.WriteMultiByteValue(memoryLocation + 2, 2, CurrentChange, 0xFFFF); - Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, newRun.Start)); + Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, newRun.Start)); } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 774016e5..b11d20e8 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -112,5 +112,14 @@ namespace HavenSoft.HexManiac.Tests { Assert.Single(messages); } + + [Fact] + public void RunAutoShortens() { + CreateSimpleRun(); + viewPort.SelectionStart = new Point(2, 0); + viewPort.Edit("[]"); + + Assert.Equal(4, model.GetNextRun(0).Length); + } } } From 68912fd8e664677d120975213555c0497d81c1e4 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 20:54:10 -0500 Subject: [PATCH 12/20] Improve Copy/Paste --- src/HexManiac.Core/Models/PokemonModel.cs | 2 +- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 4 +++- src/HexManiac.Tests/EggMoveTests.cs | 6 +++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index 485d84dd..b1585f54 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -738,7 +738,7 @@ namespace HavenSoft.HexManiac.Core.Models { public override string Copy(Func changeToken, int start, int length) { var text = new StringBuilder(); var run = GetNextRun(start); - if (run.Start < start && !(run is ArrayRun)) { + if (run.Start < start && !(run is ArrayRun) && !(run is EggMoveRun)) { length += start - run.Start; start = run.Start; } diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 0d956133..941636dc 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -148,7 +148,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { public void AppendTo(PokemonModel model, StringBuilder text, int start, int length) { while (length > 0 && start < Start + Length) { var value = model.ReadMultiByteValue(start, 2); - if (value >= MagicNumber) { + if (value == 0xFFFF) { + text.Append($"[]"); + } else if (value >= MagicNumber) { value -= MagicNumber; if (value >= cachedPokenames.Count) text.Append($"[{value}]"); else text.Append($"[{Dequote(cachedPokenames[value])}]"); diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index b11d20e8..92b908ac 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -89,11 +89,11 @@ namespace HavenSoft.HexManiac.Tests { CreateSimpleRun(); var fileSystem = new StubFileSystem(); - viewPort.SelectionStart = new Point(0, 0); - viewPort.SelectionEnd = new Point(2, 0); + viewPort.SelectionStart = new Point(2, 0); + viewPort.SelectionEnd = new Point(4, 0); viewPort.Copy.Execute(fileSystem); - Assert.Equal("^eggmoves`egg` [Carl] Wind", fileSystem.CopyText.value); + Assert.Equal("Wind []", fileSystem.CopyText.value); } [Fact] From eb11a855f4af335537609e869cb150abd66db8d7 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 21:10:09 -0500 Subject: [PATCH 13/20] limiter updates automatically --- src/HexManiac.Core/Models/PokemonModel.cs | 2 +- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 10 +++++++++- .../ViewModels/Visitors/CompleteEditOperation.cs | 10 ++++++++-- src/HexManiac.Tests/EggMoveTests.cs | 10 ++++++++-- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index b1585f54..1227e236 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -785,7 +785,7 @@ namespace HavenSoft.HexManiac.Core.Models { length -= run.Start + run.Length - start; start = run.Start + run.Length; } else if (run is EggMoveRun eggRun) { - eggRun.AppendTo(this, text, start, length); + eggRun.AppendTo(text, start, length); text.Append(" "); length -= run.Start + run.Length - start; start = run.Start + run.Length; diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 941636dc..64542a8e 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -145,7 +145,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return name.Substring(1, name.Length - 2); } - public void AppendTo(PokemonModel model, StringBuilder text, int start, int length) { + public void AppendTo(StringBuilder text, int start, int length) { while (length > 0 && start < Start + Length) { var value = model.ReadMultiByteValue(start, 2); if (value == 0xFFFF) { @@ -163,5 +163,13 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (length > 0 && start < Start + Length) text.Append(" "); } } + + public bool UpdateLimiter(ModelDelta token) { + if (PointerSources.Count != 2) return false; + var address = PointerSources.Last() - 4; + var limiter = Length / 2 - 2; + model.WriteMultiByteValue(address, 4, token, limiter); + return true; + } } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 7c018ef0..19176325 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -299,7 +299,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { for (int i = memoryLocation + 2; i < run.Start + run.Length; i += 2) { Model.WriteMultiByteValue(i, 2, CurrentChange, 0xFFFF); } - Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, run.Start)); + var newRun = new EggMoveRun(Model, run.Start); + Model.ObserveRunWritten(CurrentChange, newRun); + newRun = (EggMoveRun)Model.GetNextRun(newRun.Start); + newRun.UpdateLimiter(CurrentChange); } else if (CurrentText.EndsWith("]")) { var value = run.GetPokemonNumber(CurrentText); if (value == -1) { @@ -342,7 +345,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { DataMoved = true; } Model.WriteMultiByteValue(memoryLocation + 2, 2, CurrentChange, 0xFFFF); - Model.ObserveRunWritten(CurrentChange, new EggMoveRun(Model, newRun.Start)); + var eggRun = new EggMoveRun(Model, newRun.Start); + Model.ObserveRunWritten(CurrentChange, eggRun); + eggRun = (EggMoveRun)Model.GetNextRun(eggRun.Start); + eggRun.UpdateLimiter(CurrentChange); } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 92b908ac..80b0fcc6 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -26,6 +26,9 @@ namespace HavenSoft.HexManiac.Tests { viewPort.Goto.Execute("000100"); viewPort.Edit("^movenames[name\"\"8]8 \"Fire\" \"Water\" \"Earth\" \"Wind\" \"Light\" \"Dark\" \"Normal\" \"Magic\""); + viewPort.Goto.Execute("000180"); + viewPort.Edit("<000000> Dead Beef 01 00 00 00 <000000>"); // limiter is at 188 for an eggrun at 000 + viewPort.Goto.Execute("000000"); viewPort.OnMessage += (sender, e) => messages.Add(e); @@ -98,10 +101,12 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void RunAutoExtends() { - viewPort.Edit("^eggmoves`egg` "); + CreateSimpleRun(); + viewPort.SelectionStart = new Point(4, 0); viewPort.Edit("[Carl]"); - Assert.Equal(4, model.GetNextRun(0).Length); + Assert.Equal(8, model.GetNextRun(0).Length); + Assert.Equal(2, model.ReadMultiByteValue(0x188, 4)); } [Fact] @@ -120,6 +125,7 @@ namespace HavenSoft.HexManiac.Tests { viewPort.Edit("[]"); Assert.Equal(4, model.GetNextRun(0).Length); + Assert.Equal(0, model.ReadMultiByteValue(0x188, 4)); } } } From b10e4fd7a5a558e740677ae134b24c645be40f24 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 22:10:01 -0500 Subject: [PATCH 14/20] Basic Tool Support --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 41 +++++++++++++++++++ .../ViewModels/Tools/PCSTool.cs | 22 +++++++++- src/HexManiac.Tests/EggMoveTests.cs | 22 +++++++++- 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 64542a8e..b99e4bbf 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -126,6 +126,47 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return GetNumber(input, names); } + public string SerializeForTool() { + var builder = new StringBuilder(); + for (int i = 0; i < Length - 2; i += 2) { + var address = Start + i; + var value = model.ReadMultiByteValue(address, 2); + if (value >= MagicNumber) { + value -= MagicNumber; + builder.Append($"[{Dequote(cachedPokenames[value])}]"); + } else { + builder.Append(Dequote(cachedMovenames[value])); + } + if (i < Length - 4) builder.AppendLine(); + } + return builder.ToString(); + } + + public int DeserializeFromTool(string content, ModelDelta token) { + var data = new List(); + var pokemonNames = cachedPokenames.Select(name => $"[{Dequote(name).ToLower()}]").ToList(); + var moveNames = cachedMovenames.Select(name => Dequote(name).ToLower()).ToList(); + var lines = content.ToLower().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); + foreach (var line in lines) { + var index = pokemonNames.IndexOf(line); + if (index != -1) { data.Add(index + MagicNumber); continue; } + index = moveNames.IndexOf(line); + if (index != -1) { data.Add(index); continue; } + var startCount = data.Count; + for (int i = 0; i < pokemonNames.Count; i++) { + if (pokemonNames[i].Contains(line)) { data.Add(i + MagicNumber); break; } + } + if (startCount != data.Count) continue; + for (int i = 0; i < moveNames.Count; i++) { + if (moveNames[i].Contains(line)) { data.Add(i); break; } + } + } + var run = model.RelocateForExpansion(token, this, data.Count * 2 + 2); + for (int i = 0; i < data.Count; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, data[i]); + for (int i = data.Count; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, 0xFFFF); + return run.Start; + } + public IEnumerable GetAutoCompleteOptions() { var pokenames = cachedPokenames.Select(name => $"[{name}]"); var movenames = cachedMovenames.Select(name => name + " "); diff --git a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs index 1eedfc9a..80ac2fcb 100644 --- a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs @@ -66,6 +66,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (run.Start > address) return; // wrong run, don't adjust if (run is PCSRun pcsRun) UpdateRun(pcsRun); if (run is ArrayRun arrayRun) UpdateRun(arrayRun); + if (run is EggMoveRun eggRun) UpdateRun(eggRun); } } } @@ -77,7 +78,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (ignoreExternalUpdates) return; var run = model.GetNextRun(value); if (TryUpdate(ref address, value)) { - if ((run is PCSRun || run is ArrayRun) && run.Start <= value) { + if ((run is PCSRun || run is ArrayRun || run is EggMoveRun) && run.Start <= value) { runner.Schedule(DataForCurrentRunChanged); Enabled = true; ShowMessage = false; @@ -174,6 +175,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } } return; + } else if (run is EggMoveRun egg) { + var newContent = egg.SerializeForTool(); + TryUpdate(ref content, newContent, nameof(Content)); + return; } throw new NotImplementedException(); @@ -272,5 +277,20 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { TryUpdate(ref address, run.Start, nameof(Address)); ignoreExternalUpdates = false; } + + private void UpdateRun(EggMoveRun run) { + var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + var newStart = run.DeserializeFromTool(content, history.CurrentChange); + var newRun = new EggMoveRun(model, newStart); + if (newRun.Length != run.Length) { + model.ObserveRunWritten(history.CurrentChange, newRun); + newRun = (EggMoveRun)model.GetNextRun(newRun.Start); + history.CurrentChange.AddRun(newRun); + newRun.UpdateLimiter(history.CurrentChange); + } + + if (run.Start != newRun.Start) ModelDataMoved?.Invoke(this, (run.Start, newRun.Start)); + ModelDataChanged?.Invoke(this, newRun); + } } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 80b0fcc6..349a1d61 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -1,4 +1,5 @@ -using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core; +using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; @@ -127,5 +128,24 @@ namespace HavenSoft.HexManiac.Tests { Assert.Equal(4, model.GetNextRun(0).Length); Assert.Equal(0, model.ReadMultiByteValue(0x188, 4)); } + + [Fact] + public void CanViewInTextTool() { + CreateSimpleRun(); + viewPort.SelectionStart = new Point(2, 0); + viewPort.Tools.StringToolCommand.Execute(); + + Assert.Equal(@"[Carl] +Wind", viewPort.Tools.StringTool.Content); + + viewPort.Tools.StringTool.Content = @"[Carl] +Earth +Light +[Ryan] +Fire +Water"; + + Assert.Equal(14, model.GetNextRun(0).Length); + } } } From ea5f65e64f61cb7869db7d1f5d31459f73c9e39b Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 22:17:02 -0500 Subject: [PATCH 15/20] Ctrl+Click to open tool on egg moves --- src/HexManiac.Core/ViewModels/ViewPort.cs | 6 ++++++ src/HexManiac.Tests/EggMoveTests.cs | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index cb3aba63..5922cada 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -871,6 +871,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } Tools.SelectedIndex = Enumerable.Range(0, Tools.Count).First(i => Tools[i] is PCSTool); } + if (format is EggSection || format is EggItem) { + var byteOffset = scroll.ViewPointToDataIndex(new Point(x, y)); + var currentRun = Model.GetNextRun(byteOffset); + Tools.StringTool.Address = currentRun.Start; + Tools.SelectedIndex = Enumerable.Range(0, Tools.Count).First(i => Tools[i] is PCSTool); + } } public void ExpandSelection(int x, int y) { diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 349a1d61..9147eeb8 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -146,6 +146,16 @@ Fire Water"; Assert.Equal(14, model.GetNextRun(0).Length); + Assert.Equal(5, model.ReadMultiByteValue(0x188, 4)); + } + + [Fact] + public void FollowLinkOpensTextTool() { + CreateSimpleRun(); + viewPort.SelectionStart = new Point(2, 0); + viewPort.FollowLink(2, 0); + + Assert.Equal(0, viewPort.Tools.SelectedIndex); } } } From 0d8df15efbafd559a70545e46554bb1e47293e58 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Thu, 16 May 2019 22:20:54 -0500 Subject: [PATCH 16/20] Right-Click option to open tool --- .../ViewModels/Visitors/ContextItemFactory.cs | 10 ++++++++-- src/HexManiac.Tests/EggMoveTests.cs | 8 ++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs index 083d80dc..26ea4f8b 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs @@ -117,9 +117,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.AddRange(GetTableChildren(arrayRun)); } - public void Visit(EggSection section, byte data) { } + public void Visit(EggSection section, byte data) { + var point = ViewPort.SelectionStart; + Results.Add(new ContextItem("Open In Text Tool", arg => ViewPort.FollowLink(point.X, point.Y)) { ShortcutText = "Ctrl+Click" }); + } - public void Visit(EggItem item, byte data) { } + public void Visit(EggItem item, byte data) { + var point = ViewPort.SelectionStart; + Results.Add(new ContextItem("Open In Text Tool", arg => ViewPort.FollowLink(point.X, point.Y)) { ShortcutText = "Ctrl+Click" }); + } private IEnumerable GetTableChildren(ArrayRun array) { if (ViewPort.Tools.TableTool.Append.CanExecute(null)) { diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 9147eeb8..e4fc7e69 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -157,5 +157,13 @@ Water"; Assert.Equal(0, viewPort.Tools.SelectedIndex); } + + [Fact] + public void RightClickOptionToOpenTextTool() { + CreateSimpleRun(); + viewPort.SelectionStart = new Point(2, 0); + var items = viewPort.GetContextMenuItems(viewPort.SelectionStart); + Assert.Contains(items, item => item.ShortcutText == "Ctrl+Click"); + } } } From 34e91820c8bcb3f55b2ed23a3992634ed7f39e90 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 17 May 2019 20:24:39 -0500 Subject: [PATCH 17/20] Main Selection updates from text tool selection --- .../ViewModels/Tools/PCSTool.cs | 23 ++++++++++++++++--- src/HexManiac.Core/ViewModels/ViewPort.cs | 2 +- src/HexManiac.Tests/EggMoveTests.cs | 11 +++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs index 80ac2fcb..12c0b19e 100644 --- a/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs +++ b/src/HexManiac.Core/ViewModels/Tools/PCSTool.cs @@ -177,7 +177,10 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { return; } else if (run is EggMoveRun egg) { var newContent = egg.SerializeForTool(); - TryUpdate(ref content, newContent, nameof(Content)); + ignoreSelectionUpdates = true; + using (new StubDisposable { Dispose = () => ignoreSelectionUpdates = false }) { + TryUpdate(ref content, newContent, nameof(Content)); + } return; } @@ -191,11 +194,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { return newContent; } + /// + /// If a selection update is requested due to a change in the Content, ignore it. + /// Otherwise, the selection update could send us back to the begining of the run. + /// private bool ignoreSelectionUpdates; private void UpdateSelectionFromTool() { if (ignoreSelectionUpdates) return; var run = model.GetNextRun(Address); - if (!(run is ArrayRun) && !(run is PCSRun)) return; + if (!(run is ArrayRun) && !(run is PCSRun) && !(run is EggMoveRun)) return; // for arrays, the address must be at the start of a string segment within the first element of the array if (run is ArrayRun arrayRun) { @@ -221,6 +228,15 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { var afterLines = content.Substring(0, contentIndex + selectionLength).Split(new[] { Environment.NewLine }, StringSplitOptions.None); var selectionEnd = textStart + (afterLines.Length - 1) * array.ElementLength + afterLines[afterLines.Length - 1].Length; selectionLength = selectionEnd - selectionStart; + } else if (run is EggMoveRun egg) { + var beforeSelection = content.Substring(0, selectionStart); + var beforeLineCount = (beforeSelection.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length - 1).LimitToRange(0, int.MaxValue); + var withSelection = content.Substring(0, selectionStart + selectionLength); + var withSelectionLineCount = (withSelection.Split(new[] { Environment.NewLine }, StringSplitOptions.None).Length - 1).LimitToRange(0, int.MaxValue); + + selectionStart = egg.Start + beforeLineCount * 2; + var selectionEnd = egg.Start + withSelectionLineCount * 2; + selectionLength = selectionEnd - selectionStart; } selection.SelectionStart = selection.Scroll.DataIndexToViewPoint(selectionStart); @@ -279,7 +295,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { } private void UpdateRun(EggMoveRun run) { - var lines = content.Split(new[] { Environment.NewLine }, StringSplitOptions.None); + ignoreExternalUpdates = true; var newStart = run.DeserializeFromTool(content, history.CurrentChange); var newRun = new EggMoveRun(model, newStart); if (newRun.Length != run.Length) { @@ -291,6 +307,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Tools { if (run.Start != newRun.Start) ModelDataMoved?.Invoke(this, (run.Start, newRun.Start)); ModelDataChanged?.Invoke(this, newRun); + ignoreExternalUpdates = false; } } } diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 5922cada..5f7355da 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -168,7 +168,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var offsets = array.ConvertByteOffsetToArrayOffset(dataIndex); Tools.StringTool.Address = offsets.SegmentStart - offsets.ElementIndex * array.ElementLength; Tools.TableTool.Address = array.Start + array.ElementLength * offsets.ElementIndex; - } else if (run.Start <= dataIndex && run is PCSRun) { + } else if (run.Start <= dataIndex && (run is PCSRun || run is EggMoveRun)) { Tools.StringTool.Address = run.Start; } else { Tools.StringTool.Address = dataIndex; diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index e4fc7e69..70e34f25 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -165,5 +165,16 @@ Water"; var items = viewPort.GetContextMenuItems(viewPort.SelectionStart); Assert.Contains(items, item => item.ShortcutText == "Ctrl+Click"); } + + [Fact] + public void ToolCursorChangesCauseSelectionChanges() { + CreateSimpleRun(); + viewPort.SelectionStart = new Point(2, 0); + viewPort.Tools.StringToolCommand.Execute(); + viewPort.Tools.StringTool.ContentIndex = 5; + + Assert.True(viewPort.IsSelected(new Point(0, 0))); + Assert.True(viewPort.IsSelected(new Point(1, 0))); + } } } From ccc87f3c798f8746c9d1c9ab811dde2df5030fcb Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 17 May 2019 20:45:02 -0500 Subject: [PATCH 18/20] bugfix converting from text tool didn't work right at the end --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 13 +++++++++---- src/HexManiac.Tests/EggMoveTests.cs | 3 ++- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index b99e4bbf..b9e7113e 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -152,18 +152,23 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (index != -1) { data.Add(index + MagicNumber); continue; } index = moveNames.IndexOf(line); if (index != -1) { data.Add(index); continue; } - var startCount = data.Count; + + // didn't find an exact match... look for a partial pokemon match + var matchFound = false; for (int i = 0; i < pokemonNames.Count; i++) { - if (pokemonNames[i].Contains(line)) { data.Add(i + MagicNumber); break; } + if (pokemonNames[i].Contains(line)) { data.Add(i + MagicNumber); matchFound = true; break; } } - if (startCount != data.Count) continue; + if (matchFound) continue; + + // look for a partial move match for (int i = 0; i < moveNames.Count; i++) { if (moveNames[i].Contains(line)) { data.Add(i); break; } } } var run = model.RelocateForExpansion(token, this, data.Count * 2 + 2); for (int i = 0; i < data.Count; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, data[i]); - for (int i = data.Count; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, 0xFFFF); + model.WriteMultiByteValue(run.Start + data.Count * 2, 2, token, 0xFFFF); // write the new end token + for (int i = data.Count + 2; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, 0xFFFF); // fill any remaining old space with FF return run.Start; } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 70e34f25..8175cc63 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -132,7 +132,8 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void CanViewInTextTool() { CreateSimpleRun(); - viewPort.SelectionStart = new Point(2, 0); + viewPort.SelectionStart = new Point(2, 2); // select off it + viewPort.SelectionStart = new Point(2, 0); // and then back on viewPort.Tools.StringToolCommand.Execute(); Assert.Equal(@"[Carl] From 9bfa6b80269a9057f9120018c545a57b75224f16 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 17 May 2019 21:11:26 -0500 Subject: [PATCH 19/20] "Find" can find egg moves --- src/HexManiac.Core/Models/IDataModel.cs | 2 + src/HexManiac.Core/Models/PokemonModel.cs | 1 + src/HexManiac.Core/ViewModels/ViewPort.cs | 72 +++++++++++++++-------- src/HexManiac.Tests/EggMoveTests.cs | 11 ++++ 4 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/HexManiac.Core/Models/IDataModel.cs b/src/HexManiac.Core/Models/IDataModel.cs index 56d73380..f9e15cbe 100644 --- a/src/HexManiac.Core/Models/IDataModel.cs +++ b/src/HexManiac.Core/Models/IDataModel.cs @@ -11,6 +11,7 @@ namespace HavenSoft.HexManiac.Core.Models { byte[] RawData { get; } new byte this[int index] { get; set; } IReadOnlyList Arrays { get; } + IReadOnlyList Streams { get; } /// /// If dataIndex is in the middle of a run, returns that run. @@ -64,6 +65,7 @@ namespace HavenSoft.HexManiac.Core.Models { public BaseModel(byte[] data) => RawData = data; public virtual IReadOnlyList Arrays { get; } = new List(); + public virtual IReadOnlyList Streams { get; } = new List(); public byte this[int index] { get => RawData[index]; set => RawData[index] = value; } diff --git a/src/HexManiac.Core/Models/PokemonModel.cs b/src/HexManiac.Core/Models/PokemonModel.cs index 1227e236..b59127a0 100644 --- a/src/HexManiac.Core/Models/PokemonModel.cs +++ b/src/HexManiac.Core/Models/PokemonModel.cs @@ -29,6 +29,7 @@ namespace HavenSoft.HexManiac.Core.Models { public virtual int EarliestAllowedAnchor => 0; public override IReadOnlyList Arrays => runs.OfType().ToList(); + public override IReadOnlyList Streams => runs.OfType().ToList(); #region Constructor diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 5f7355da..eb10894c 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -700,38 +700,60 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var textResults = Search(searchBytes).ToList(); Model.ConsiderResultsAsTextRuns(history.CurrentChange, textResults); foreach (var result in textResults) { - // if the result is in an array, we care about things that use that array if (Model.GetNextRun(result) is ArrayRun parentArray && parentArray.LengthFromAnchor == string.Empty) { - var offsets = parentArray.ConvertByteOffsetToArrayOffset(result); - var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start); - if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) { - foreach (var child in Model.Arrays) { - // option 1: another table has a row named after this element - if (child.LengthFromAnchor == parentArrayName) { - var address = child.Start + child.ElementLength * offsets.ElementIndex; - yield return (address, address + child.ElementLength - 1); - } + foreach (var dataResult in FindMatchingDataResultsFromArrayElement(parentArray, result)) yield return dataResult; + } - // option 2: another table has an enum named after this element - var segmentOffset = 0; - foreach (var segment in child.ElementContent) { - if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) { - segmentOffset += segment.Length; - continue; + yield return (result, result + pcsBytes.Count - 1); + } + } + + /// + /// When performing a search, sometimes one of the search results is text from a table. + /// If so, then we also care about places where that table value is used. + /// This function finds uses of an element in a table. + /// + private IEnumerable<(int start, int end)> FindMatchingDataResultsFromArrayElement(ArrayRun parentArray, int parentIndex) { + var offsets = parentArray.ConvertByteOffsetToArrayOffset(parentIndex); + var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start); + if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) { + foreach (var child in Model.Arrays) { + // option 1: another table has a row named after this element + if (child.LengthFromAnchor == parentArrayName) { + var address = child.Start + child.ElementLength * offsets.ElementIndex; + yield return (address, address + child.ElementLength - 1); + } + + // option 2: another table has an enum named after this element + var segmentOffset = 0; + foreach (var segment in child.ElementContent) { + if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) { + segmentOffset += segment.Length; + continue; + } + for (int i = 0; i < child.ElementCount; i++) { + var address = child.Start + child.ElementLength * i + segmentOffset; + var enumValue = Model.ReadMultiByteValue(address, segment.Length); + if (enumValue != offsets.ElementIndex) continue; + yield return (address, address + segment.Length - 1); + } + segmentOffset += segment.Length; + } + } + foreach (var child in Model.Streams) { + // option 3: a stream uses this as a datatype + if (child is EggMoveRun eggRun) { + var groupStart = 0; + if (parentArrayName == EggMoveRun.PokemonNameTable) groupStart = EggMoveRun.MagicNumber; + if (parentArrayName == EggMoveRun.PokemonNameTable || parentArrayName == EggMoveRun.MoveNamesTable) { + for (int i = 0; i < eggRun.Length - 2; i += 2) { + if (Model.ReadMultiByteValue(eggRun.Start + i, 2) == offsets.ElementIndex + groupStart) { + yield return (eggRun.Start + i, eggRun.Start + i + 1); } - for (int i = 0; i < child.ElementCount; i++) { - var address = child.Start + child.ElementLength * i + segmentOffset; - var enumValue = Model.ReadMultiByteValue(address, segment.Length); - if (enumValue != offsets.ElementIndex) continue; - yield return (address, address + segment.Length - 1); - } - segmentOffset += segment.Length; } } } } - - yield return (result, result + pcsBytes.Count - 1); } } diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 8175cc63..54810d1d 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -177,5 +177,16 @@ Water"; Assert.True(viewPort.IsSelected(new Point(0, 0))); Assert.True(viewPort.IsSelected(new Point(1, 0))); } + + [Fact] + public void SearchFindsEggMoveData() { + CreateSimpleRun(); + + var pairs = viewPort.Find("wind"); + Assert.Contains((2, 3), pairs); + + pairs = viewPort.Find("carl"); + Assert.Contains((0, 1), pairs); + } } } From 60be3bd7e6cb5f79cafd4f0501be1c36044b471f Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 22 May 2019 22:08:57 -0500 Subject: [PATCH 20/20] Updates from review * code quality improvements * fix a crash when backspacing egg formats * fix a crash when backspacing all the way to empty * fix data corruption when edit-exiting multi-byte formats * --- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 46 ++++---- .../ViewModels/ConvertCellToText.cs | 4 +- src/HexManiac.Core/ViewModels/Selection.cs | 2 +- src/HexManiac.Core/ViewModels/ViewPort.cs | 102 ++++++++++-------- .../Visitors/CompleteEditOperation.cs | 6 +- .../ViewModels/Visitors/ContinueCellEdit.cs | 5 +- .../ViewModels/Visitors/DataClear.cs | 9 +- .../ViewModels/Visitors/StartCellEdit.cs | 2 +- src/HexManiac.Tests/EggMoveTests.cs | 42 ++++++++ 9 files changed, 139 insertions(+), 79 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index b9e7113e..6a22b867 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -8,8 +8,11 @@ using System.Text; namespace HavenSoft.HexManiac.Core.Models.Runs { public class EggMoveRun : IFormattedRun { public const int MagicNumber = 0x4E20; // anything above this number is a pokemon, anything below it is a move + public const int EndStream = 0xFFFF; public const string PokemonNameTable = "pokenames"; public const string MoveNamesTable = "movenames"; + public const string GroupStart = "["; + public const string GroupEnd = "]"; private readonly IDataModel model; public int Start { get; } @@ -49,13 +52,13 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (run.Start + length * 2 + 3 < 0) continue; if (run.Start + length * 2 + 3 > data.Count) continue; var endValue = data.ReadMultiByteValue(run.Start + length * 2 + 2, 2); - if (endValue != 0xFFFF) continue; + if (endValue != EndStream) continue; // verify content bool possibleMatch = true; for (int i = 0; i < length - 2; i++) { var value = data.ReadMultiByteValue(run.Start + i * 2, 2); - if (value == 0xFFFF) break; // early exit, the data was edited, but that's ok. Everything still matches up. + if (value == EndStream) break; // early exit, the data was edited, but that's ok. Everything still matches up. if (value >= MagicNumber) { value -= MagicNumber; if (value < pokenames.ElementCount) continue; @@ -91,10 +94,10 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (value >= MagicNumber) { value -= MagicNumber; string content = cachedPokenames.Count > value ? cachedPokenames[value] : value.ToString(); - if (value == 0xFFFF - MagicNumber) content = string.Empty; + if (value == EndStream - MagicNumber) content = string.Empty; if (content.StartsWith("\"")) content = content.Substring(1); if (content.EndsWith("\"")) content = content.Substring(0, content.Length - 1); - return new EggSection(groupStart + Start, position, $"[{content}]"); + return new EggSection(groupStart + Start, position, $"{GroupStart}{content}{GroupEnd}"); } else { string content = cachedMovenames.Count > value ? cachedMovenames[value] : value.ToString(); return new EggItem(groupStart + Start, position, content); @@ -115,14 +118,14 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } public int GetPokemonNumber(string input) { - if (input.StartsWith("[")) input = input.Substring(1, input.Length - 2); - var names = cachedPokenames.Select(name => Dequote(name).ToLower()).ToList(); + if (input.StartsWith(GroupStart)) input = input.Substring(1, input.Length - 2); + var names = cachedPokenames.Select(name => name.Trim('"').ToLower()).ToList(); return GetNumber(input.ToLower(), names); } public int GetMoveNumber(string input) { - input = Dequote(input).ToLower(); - var names = cachedMovenames.Select(name => Dequote(name).ToLower()).ToList(); + input = input.Trim('"').ToLower(); + var names = cachedMovenames.Select(name => name.Trim('"').ToLower()).ToList(); return GetNumber(input, names); } @@ -133,9 +136,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { var value = model.ReadMultiByteValue(address, 2); if (value >= MagicNumber) { value -= MagicNumber; - builder.Append($"[{Dequote(cachedPokenames[value])}]"); + builder.Append($"{GroupStart}{cachedPokenames[value].Trim('"')}{GroupEnd}"); } else { - builder.Append(Dequote(cachedMovenames[value])); + builder.Append(cachedMovenames[value].Trim('"')); } if (i < Length - 4) builder.AppendLine(); } @@ -144,8 +147,8 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { public int DeserializeFromTool(string content, ModelDelta token) { var data = new List(); - var pokemonNames = cachedPokenames.Select(name => $"[{Dequote(name).ToLower()}]").ToList(); - var moveNames = cachedMovenames.Select(name => Dequote(name).ToLower()).ToList(); + var pokemonNames = cachedPokenames.Select(name => $"{GroupStart}{name.Trim('"').ToLower()}{GroupEnd}").ToList(); + var moveNames = cachedMovenames.Select(name => name.Trim('"').ToLower()).ToList(); var lines = content.ToLower().Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var index = pokemonNames.IndexOf(line); @@ -167,13 +170,13 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } var run = model.RelocateForExpansion(token, this, data.Count * 2 + 2); for (int i = 0; i < data.Count; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, data[i]); - model.WriteMultiByteValue(run.Start + data.Count * 2, 2, token, 0xFFFF); // write the new end token - for (int i = data.Count + 2; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, 0xFFFF); // fill any remaining old space with FF + model.WriteMultiByteValue(run.Start + data.Count * 2, 2, token, EndStream); // write the new end token + for (int i = data.Count + 2; i < Length / 2; i++) model.WriteMultiByteValue(run.Start + i * 2, 2, token, EndStream); // fill any remaining old space with FF return run.Start; } public IEnumerable GetAutoCompleteOptions() { - var pokenames = cachedPokenames.Select(name => $"[{name}]"); + var pokenames = cachedPokenames.Select(name => $"{GroupStart}{name}{GroupEnd}"); var movenames = cachedMovenames.Select(name => name + " "); return pokenames.Concat(movenames); } @@ -186,20 +189,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return names.IndexOf(match); } - private static string Dequote(string name) { - if (!name.StartsWith("\"")) return name; - return name.Substring(1, name.Length - 2); - } - public void AppendTo(StringBuilder text, int start, int length) { while (length > 0 && start < Start + Length) { var value = model.ReadMultiByteValue(start, 2); - if (value == 0xFFFF) { - text.Append($"[]"); + if (value == EndStream) { + text.Append($"{GroupStart}{GroupEnd}"); } else if (value >= MagicNumber) { value -= MagicNumber; - if (value >= cachedPokenames.Count) text.Append($"[{value}]"); - else text.Append($"[{Dequote(cachedPokenames[value])}]"); + if (value >= cachedPokenames.Count) text.Append($"{GroupStart}{value}{GroupEnd}"); + else text.Append($"{GroupStart}{cachedPokenames[value].Trim('"')}{GroupEnd}"); } else { if (value >= cachedMovenames.Count) text.Append($"{value}"); else text.Append($"{cachedMovenames[value]}"); diff --git a/src/HexManiac.Core/ViewModels/ConvertCellToText.cs b/src/HexManiac.Core/ViewModels/ConvertCellToText.cs index c8ce34d6..e69c0567 100644 --- a/src/HexManiac.Core/ViewModels/ConvertCellToText.cs +++ b/src/HexManiac.Core/ViewModels/ConvertCellToText.cs @@ -47,8 +47,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { public void Visit(IntegerEnum integerEnum, byte data) => Result = integerEnum.Value; - public void Visit(EggSection section, byte data) => throw new NotImplementedException(); + public void Visit(EggSection section, byte data) => Result = section.SectionName; - public void Visit(EggItem item, byte data) => throw new NotImplementedException(); + public void Visit(EggItem item, byte data) => Result = item.ItemName; } } diff --git a/src/HexManiac.Core/ViewModels/Selection.cs b/src/HexManiac.Core/ViewModels/Selection.cs index e01537b0..726503f1 100644 --- a/src/HexManiac.Core/ViewModels/Selection.cs +++ b/src/HexManiac.Core/ViewModels/Selection.cs @@ -42,7 +42,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (selectionStart.Equals(value)) return; if (!Scroll.ScrollToPoint(ref value)) { - PreviewSelectionStartChanged?.Invoke(this, rawSelectionStart); + PreviewSelectionStartChanged?.Invoke(this, getSpan(rawSelectionStart).start); } rawSelectionStart = value; diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index eb10894c..060c7d58 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -144,7 +144,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels { if (underEdit.CurrentText.Count(c => c == StringDelimeter) % 2 == 1) endEdit = StringDelimeter.ToString(); var originalFormat = underEdit.OriginalFormat; if (originalFormat is Anchor anchor) originalFormat = anchor.OriginalFormat; - if (underEdit.CurrentText.StartsWith("[") && (originalFormat is EggSection || originalFormat is EggItem)) endEdit = "]"; + if (underEdit.CurrentText.StartsWith(EggMoveRun.GroupStart) && (originalFormat is EggSection || originalFormat is EggItem)) endEdit = EggMoveRun.GroupEnd; currentView[location.X, location.Y] = new HexElement(element.Value, underEdit.Edit(endEdit)); if (!TryCompleteEdit(location)) ClearEdits(location); } @@ -590,6 +590,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels { } run = Model.GetNextRun(index); + var cellToText = new ConvertCellToText(Model, run.Start); + var cell = currentView[point.X, point.Y]; if (run is PCSRun pcs) { for (int i = index; i < run.Start + run.Length; i++) history.CurrentChange.ChangeData(Model, i, 0xFF); @@ -608,19 +610,16 @@ namespace HavenSoft.HexManiac.Core.ViewModels { RefreshBackingData(); SelectionStart = scroll.DataIndexToViewPoint(index - 1); } else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Pointer) { - var cell = currentView[point.X, point.Y]; PrepareForMultiSpaceEdit(point, 4); var destination = ((Pointer)cell.Format).DestinationAsText; destination = destination.Substring(0, destination.Length - 1); currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, destination, 4)); } else if (array.ElementContent[offsets.SegmentIndex].Type == ElementContentType.Integer) { - var cell = currentView[point.X, point.Y]; - var format = (Integer)cell.Format; - PrepareForMultiSpaceEdit(point, format.Length); - var text = format.Value.ToString(); - if (format is IntegerEnum intEnum) text = intEnum.Value; + PrepareForMultiSpaceEdit(point, ((Integer)cell.Format).Length); + cell.Format.Visit(cellToText, cell.Value); + var text = cellToText.Result; text = text.Substring(0, text.Length - 1); - currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(format, text, format.Length)); + currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, text, ((Integer)cell.Format).Length)); } else { throw new NotImplementedException(); } @@ -628,23 +627,32 @@ namespace HavenSoft.HexManiac.Core.ViewModels { return; } + if (run is EggMoveRun eggRun) { + PrepareForMultiSpaceEdit(point, 2); + cell.Format.Visit(cellToText, cell.Value); + var text = cellToText.Result; + text = text.Substring(0, text.Length - 1); + currentView[point.X, point.Y] = new HexElement(cell.Value, new UnderEdit(cell.Format, text, 2)); + NotifyCollectionChanged(ResetArgs); + return; + } + if (run.Start <= index && run.Start + run.Length > index) { // I want to do a backspace at the end of this run SelectionStart = scroll.DataIndexToViewPoint(run.Start); - var cellToText = new ConvertCellToText(Model, run.Start); var element = currentView[SelectionStart.X, SelectionStart.Y]; element.Format.Visit(cellToText, element.Value); var text = cellToText.Result; var editLength = 1; if (element.Format is Pointer pointer) editLength = 4; - // if (element.Format is Integer integer) editLength = integer.Length; for (int i = 0; i < run.Length; i++) { var p = scroll.DataIndexToViewPoint(run.Start + i); string editString = i == 0 ? text.Substring(0, text.Length - 1) : string.Empty; if (i > 0) editLength = 1; - currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, new UnderEdit(currentView[p.X, p.Y].Format, editString, editLength)); + var format = new UnderEdit(currentView[p.X, p.Y].Format, editString, editLength); + currentView[p.X, p.Y] = new HexElement(currentView[p.X, p.Y].Value, format); } } else { SelectionStart = scroll.DataIndexToViewPoint(index); @@ -717,39 +725,49 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var offsets = parentArray.ConvertByteOffsetToArrayOffset(parentIndex); var parentArrayName = Model.GetAnchorFromAddress(-1, parentArray.Start); if (offsets.SegmentIndex == 0 && parentArray.ElementContent[offsets.SegmentIndex].Type == ElementContentType.PCS) { - foreach (var child in Model.Arrays) { - // option 1: another table has a row named after this element - if (child.LengthFromAnchor == parentArrayName) { - var address = child.Start + child.ElementLength * offsets.ElementIndex; - yield return (address, address + child.ElementLength - 1); - } + var arrayUses = FindTableUsages(offsets, parentArrayName); + var streamUses = FindStreamUsages(offsets, parentArrayName); + return arrayUses.Concat(streamUses); + } + return Enumerable.Empty<(int, int)>(); + } - // option 2: another table has an enum named after this element - var segmentOffset = 0; - foreach (var segment in child.ElementContent) { - if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) { - segmentOffset += segment.Length; - continue; - } - for (int i = 0; i < child.ElementCount; i++) { - var address = child.Start + child.ElementLength * i + segmentOffset; - var enumValue = Model.ReadMultiByteValue(address, segment.Length); - if (enumValue != offsets.ElementIndex) continue; - yield return (address, address + segment.Length - 1); - } - segmentOffset += segment.Length; - } + private IEnumerable<(int start, int end)> FindTableUsages(ArrayOffset offsets, string parentArrayName) { + foreach (var child in Model.Arrays) { + // option 1: another table has a row named after this element + if (child.LengthFromAnchor == parentArrayName) { + var address = child.Start + child.ElementLength * offsets.ElementIndex; + yield return (address, address + child.ElementLength - 1); } - foreach (var child in Model.Streams) { - // option 3: a stream uses this as a datatype - if (child is EggMoveRun eggRun) { - var groupStart = 0; - if (parentArrayName == EggMoveRun.PokemonNameTable) groupStart = EggMoveRun.MagicNumber; - if (parentArrayName == EggMoveRun.PokemonNameTable || parentArrayName == EggMoveRun.MoveNamesTable) { - for (int i = 0; i < eggRun.Length - 2; i += 2) { - if (Model.ReadMultiByteValue(eggRun.Start + i, 2) == offsets.ElementIndex + groupStart) { - yield return (eggRun.Start + i, eggRun.Start + i + 1); - } + + // option 2: another table has an enum named after this element + var segmentOffset = 0; + foreach (var segment in child.ElementContent) { + if (!(segment is ArrayRunEnumSegment enumSegment) || enumSegment.EnumName != parentArrayName) { + segmentOffset += segment.Length; + continue; + } + for (int i = 0; i < child.ElementCount; i++) { + var address = child.Start + child.ElementLength * i + segmentOffset; + var enumValue = Model.ReadMultiByteValue(address, segment.Length); + if (enumValue != offsets.ElementIndex) continue; + yield return (address, address + segment.Length - 1); + } + segmentOffset += segment.Length; + } + } + } + + private IEnumerable<(int start, int end)> FindStreamUsages(ArrayOffset offsets, string parentArrayName) { + foreach (var child in Model.Streams) { + // option 1: the value is used by egg moves + if (child is EggMoveRun eggRun) { + var groupStart = 0; + if (parentArrayName == EggMoveRun.PokemonNameTable) groupStart = EggMoveRun.MagicNumber; + if (parentArrayName == EggMoveRun.PokemonNameTable || parentArrayName == EggMoveRun.MoveNamesTable) { + for (int i = 0; i < eggRun.Length - 2; i += 2) { + if (Model.ReadMultiByteValue(eggRun.Start + i, 2) == offsets.ElementIndex + groupStart) { + yield return (eggRun.Start + i, eggRun.Start + i + 1); } } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 19176325..abb4e250 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -286,14 +286,14 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { private void CompleteEggEdit() { var endChar = CurrentText[CurrentText.Length - 1]; - if ($"] {StringDelimeter}".All(c => endChar != c)) return; + if (!$"{EggMoveRun.GroupEnd} {StringDelimeter}".Contains(endChar)) return; if (CurrentText.Count(c => c == StringDelimeter) % 2 != 0) return; NewDataIndex = memoryLocation + 2; Result = true; var run = (EggMoveRun)Model.GetNextRun(memoryLocation); - if (CurrentText == "[]") { + if (CurrentText == EggMoveRun.GroupStart + EggMoveRun.GroupEnd) { Model.WriteMultiByteValue(memoryLocation, 2, CurrentChange, 0xFFFF); // clear all data after this and shorten the run for (int i = memoryLocation + 2; i < run.Start + run.Length; i += 2) { @@ -303,7 +303,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Model.ObserveRunWritten(CurrentChange, newRun); newRun = (EggMoveRun)Model.GetNextRun(newRun.Start); newRun.UpdateLimiter(CurrentChange); - } else if (CurrentText.EndsWith("]")) { + } else if (CurrentText.EndsWith(EggMoveRun.GroupEnd)) { var value = run.GetPokemonNumber(CurrentText); if (value == -1) { ErrorText = $"Could not parse {CurrentText} as a pokemon name"; diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs index 6f700917..ee9acf5b 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContinueCellEdit.cs @@ -1,4 +1,5 @@ using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using System; using System.Linq; @@ -76,8 +77,8 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(EggItem item, byte data) => VisitEgg(); public void VisitEgg() { var specialCharacters = ". '-\\"; // mr. mime, farfetch'd, double-edge, nidoran - if (UnderEdit.CurrentText[0] == '[') specialCharacters += ']'; - if (UnderEdit.CurrentText[0] == StringDelimeter) specialCharacters += StringDelimeter; + if (UnderEdit.CurrentText.StartsWith(EggMoveRun.GroupStart)) specialCharacters += ']'; + if (UnderEdit.CurrentText.StartsWith(StringDelimeter.ToString())) specialCharacters += StringDelimeter; Result = char.IsLetterOrDigit(Input) || specialCharacters.Contains(Input) || diff --git a/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs b/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs index a5d8c0d6..409e3581 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/DataClear.cs @@ -1,4 +1,5 @@ using HavenSoft.HexManiac.Core.Models; +using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; using System; @@ -40,12 +41,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { public void Visit(Ascii ascii, byte data) => currentChange.ChangeData(buffer, index, 0xFF); - public void Visit(Integer integer, byte data) => buffer.WriteValue(currentChange, index, 0); + public void Visit(Integer integer, byte data) => buffer.WriteMultiByteValue(index, integer.Length, currentChange, 0); - public void Visit(IntegerEnum integerEnum, byte data) => buffer.WriteValue(currentChange, index, 0); + public void Visit(IntegerEnum integerEnum, byte data) => buffer.WriteMultiByteValue(index, integerEnum.Length, currentChange, 0); - public void Visit(EggSection section, byte data) => currentChange.ChangeData(buffer, index, 0xFF); + public void Visit(EggSection section, byte data) => buffer.WriteMultiByteValue(index, 2, currentChange, EggMoveRun.MagicNumber); - public void Visit(EggItem item, byte data) => currentChange.ChangeData(buffer, index, 0xFF); + public void Visit(EggItem item, byte data) => buffer.WriteMultiByteValue(index, 2, currentChange, 0x0000); } } diff --git a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index eb14afb5..1cf29833 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs @@ -49,7 +49,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { // just continue the existing edit. public void Visit(UnderEdit underEdit, byte data) { // handle special cases of "anywhere" formats first - if (underEdit.CurrentText[0] == ViewPort.GotoMarker) { + if (underEdit.CurrentText.StartsWith(ViewPort.GotoMarker.ToString())) { Result = char.IsLetterOrDigit(Input) || Input == ArrayAnchorSeparator || char.IsWhiteSpace(Input); return; } else if (underEdit.CurrentText.StartsWith(AnchorStart.ToString())) { diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 54810d1d..47deeaf5 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -3,6 +3,7 @@ using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.Models.Runs; using HavenSoft.HexManiac.Core.ViewModels; using HavenSoft.HexManiac.Core.ViewModels.DataFormats; +using System; using System.Collections.Generic; using System.Linq; using Xunit; @@ -188,5 +189,46 @@ Water"; pairs = viewPort.Find("carl"); Assert.Contains((0, 1), pairs); } + + [Fact] + public void BackspaceWorks() { + CreateSimpleRun(); + + viewPort.SelectionStart = new Point(2, 0); + viewPort.Edit(ConsoleKey.Backspace); + + var format = (UnderEdit)viewPort[2, 0].Format; + Assert.Equal("Win", format.CurrentText); // Wind, but backspaced + } + + /// + /// Since egg moves are multiple cells long, we want to know that + /// changes get reverted when 'selectionstart' is the rightmost cell + /// + [Fact] + public void SelectLeftThenBackspaceThenDownCompletesEdits() { + CreateSimpleRun(); + + viewPort.SelectionStart = new Point(2, 0); + viewPort.MoveSelectionStart.Execute(Direction.Left); + viewPort.Edit(ConsoleKey.Backspace); + viewPort.MoveSelectionStart.Execute(Direction.Down); + + Assert.IsNotType(viewPort[1, 0].Format); + } + + [Fact] + public void CanClearCellContent() { + CreateSimpleRun(); + + viewPort.SelectionStart = new Point(2, 0); + viewPort.Edit(ConsoleKey.Backspace); // d + viewPort.Edit(ConsoleKey.Backspace); // n + viewPort.Edit(ConsoleKey.Backspace); // i + viewPort.Edit(ConsoleKey.Backspace); // W + + var format = (UnderEdit)viewPort[2, 0].Format; + Assert.Equal(string.Empty, format.CurrentText); + } } }