From ab7159025e64596b4443bfaf82c9a593a8a03a70 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Sat, 18 May 2019 13:54:31 -0500 Subject: [PATCH 1/8] Autosearch Updates preparing to try and support more ROMs --- src/HexManiac.Core/Models/Runs/ArrayRun.cs | 16 +++++++++++----- src/HexManiac.Tests/AutoSearchTests.cs | 12 ++++++------ 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index 4f0a04b8..18078c97 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -201,7 +201,12 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { break; } } - if (bestLength < currentLength) { + + // we think we found some data! Make sure it's not just a bunch of 00's and FF's + var dataEmpty = true; + for (int i = 0; i < currentLength && currentLength > bestLength && dataEmpty; i++) dataEmpty = data[run.Start + i] == 0xFF || data[run.Start + i] == 0x00; + + if (bestLength < currentLength && !dataEmpty) { bestLength = currentLength; bestAddress = run.Start; } @@ -455,8 +460,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { var format = ElementContentType.PCS; var formatLength = 2; while (formatLength < segments.Length && char.IsDigit(segments[formatLength])) formatLength++; - var segmentLength = int.Parse(segments.Substring(2, formatLength - 2)); - return (format, formatLength, segmentLength); + if (int.TryParse(segments.Substring(2, formatLength - 2), out var segmentLength)) { + return (format, formatLength, segmentLength); + } } else if (segments.StartsWith(DoubleByteIntegerFormat + string.Empty + DoubleByteIntegerFormat)) { return (ElementContentType.Integer, 2, 4); } else if (segments.StartsWith(DoubleByteIntegerFormat + string.Empty + SingleByteIntegerFormat) || segments.StartsWith(".:")) { @@ -504,11 +510,11 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { switch (segment.Type) { case ElementContentType.PCS: int readLength = PCSString.ReadString(owner, start, true, segment.Length); - if (readLength == -1) return false; + if (readLength < 2) return false; + if (owner[start] == 0x00) return false; // don't auto-find text that starts with a space, that's not how text ever starts. if (readLength > segment.Length) return false; if (Enumerable.Range(start, segment.Length).All(i => owner[i] == 0xFF)) return false; - // TODO test this with Altair // in the initial 5 ROMs, any data after the close quote is either 0x00 or 0xFF // but in fan games, this data may contain leftover junk bytes from what the text 'used' to be. // this is because other popular existing editors don't clean up after themselves. diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 350fc9ff..21a265fe 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -104,13 +104,13 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "trainerclassnames"); var run = (ArrayRun)model.GetNextAnchor(address); - if (game.Contains("Altair")) Assert.Equal(67, run.ElementCount); - if (game.Contains("Emerald")) Assert.Equal(67, run.ElementCount); - if (game.Contains("FireRed")) Assert.Equal(108, run.ElementCount); + if (game.Contains("Altair")) Assert.Equal(66, run.ElementCount); + if (game.Contains("Emerald")) Assert.Equal(66, run.ElementCount); + if (game.Contains("FireRed")) Assert.Equal(107, run.ElementCount); if (game.Contains("DarkRisingKAIZO")) Assert.Equal(107, run.ElementCount); - if (game.Contains("LeafGreen")) Assert.Equal(108, run.ElementCount); - if (game.Contains("Ruby")) Assert.Equal(59, run.ElementCount); - if (game.Contains("Sapphire")) Assert.Equal(59, run.ElementCount); + if (game.Contains("LeafGreen")) Assert.Equal(107, run.ElementCount); + if (game.Contains("Ruby")) Assert.Equal(58, run.ElementCount); + if (game.Contains("Sapphire")) Assert.Equal(58, run.ElementCount); } [SkippableTheory] From 4811b7d9ec09adfd8ea81ecd161bd8cda2fe4669 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Sat, 18 May 2019 13:57:25 -0500 Subject: [PATCH 2/8] Autosearch Updates 2 --- src/HexManiac.Tests/AutoSearchTests.cs | 27 ++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 21a265fe..9ec8a66b 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -1,6 +1,7 @@  using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.Models.Runs; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -88,12 +89,13 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "items"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Altair")) Assert.Equal(377, run.ElementCount); - if (game.Contains("Emerald")) Assert.Equal(377, run.ElementCount); - if (game.Contains("FireRed")) Assert.Equal(375, run.ElementCount); - if (game.Contains("DarkRisingKAIZO")) Assert.Equal(375, run.ElementCount); - if (game.Contains("LeafGreen")) Assert.Equal(375, run.ElementCount); - if (game.Contains("Ruby")) Assert.Equal(349, run.ElementCount); - if (game.Contains("Sapphire")) Assert.Equal(349, run.ElementCount); + else if (game.Contains("Emerald")) Assert.Equal(377, run.ElementCount); + else if (game.Contains("FireRed")) Assert.Equal(375, run.ElementCount); + else if (game.Contains("DarkRisingKAIZO")) Assert.Equal(375, run.ElementCount); + else if (game.Contains("LeafGreen")) Assert.Equal(375, run.ElementCount); + else if (game.Contains("Ruby")) Assert.Equal(349, run.ElementCount); + else if (game.Contains("Sapphire")) Assert.Equal(349, run.ElementCount); + else throw new NotImplementedException(); } [SkippableTheory] @@ -105,12 +107,13 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "trainerclassnames"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Altair")) Assert.Equal(66, run.ElementCount); - if (game.Contains("Emerald")) Assert.Equal(66, run.ElementCount); - if (game.Contains("FireRed")) Assert.Equal(107, run.ElementCount); - if (game.Contains("DarkRisingKAIZO")) Assert.Equal(107, run.ElementCount); - if (game.Contains("LeafGreen")) Assert.Equal(107, run.ElementCount); - if (game.Contains("Ruby")) Assert.Equal(58, run.ElementCount); - if (game.Contains("Sapphire")) Assert.Equal(58, run.ElementCount); + else if (game.Contains("Emerald")) Assert.Equal(66, run.ElementCount); + else if (game.Contains("FireRed")) Assert.Equal(107, run.ElementCount); + else if (game.Contains("DarkRisingKAIZO")) Assert.Equal(107, run.ElementCount); + else if (game.Contains("LeafGreen")) Assert.Equal(107, run.ElementCount); + else if (game.Contains("Ruby")) Assert.Equal(58, run.ElementCount); + else if (game.Contains("Sapphire")) Assert.Equal(58, run.ElementCount); + else throw new NotImplementedException(); } [SkippableTheory] From 79287250ffe944f4586c76866b123d766fa8bbef Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Sun, 19 May 2019 20:17:38 -0500 Subject: [PATCH 3/8] Officially Support Vega * relax egg move compatibility: allow more than 2 pointers to it * more strict when finding pcs tables - some fan games include new versions of tables, but leave the old tables where they were with nothing pointing to it. Before we used the new anchor to tell where a table ended. Lacking that anchor, we can determine that the new text is part of the NEXT table and not the CURRENT table using a huristic. - if the additional element appears to be text, but the _next_ element would start with a space, then the additional element might be a different width. - if the extra spaces after the additional element are only enough to get 4-byte aligned, then the additional element is in this table, but is the last element. The spaces are just for word alignment. * require that abilitynames's first pointer lives somewhere in the first 0x100000 bytes. This prevents us from finding false-positive pointers much later in the ROM, even if the data has been moved. (Note that FR, LG, and Emerald all have this pointer within the first few pages of data, while Ruby/Sapphire have it much later.) * require that trainerclassnames have more than one thing pointing to them. This reduces false positives, even given our relaxed PCS Table searching logic. --- src/HexManiac.Core/Models/AutoSearchModel.cs | 8 +++--- src/HexManiac.Core/Models/Runs/ArrayRun.cs | 20 ++++++++++++--- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 2 +- src/HexManiac.Tests/AutoSearchTests.cs | 27 ++++++++++++-------- 4 files changed, 37 insertions(+), 20 deletions(-) diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index 5cc52945..38441e61 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -65,17 +65,17 @@ namespace HavenSoft.HexManiac.Core.Models { // abilitynames / trainer names if (gameCode == Ruby || gameCode == Sapphire || gameCode == Emerald) { - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames, run => run.PointerSources.FirstOrDefault() < 0x100000)) { ObserveAnchorWritten(noChangeDelta, "abilitynames", abilitynames); } - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames, run => run.PointerSources.Count > 1)) { ObserveAnchorWritten(noChangeDelta, "trainerclassnames", trainerclassnames); } } else { - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames, run => run.PointerSources.Count > 1)) { ObserveAnchorWritten(noChangeDelta, "trainerclassnames", trainerclassnames); } - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames, run => run.PointerSources.FirstOrDefault() < 0x100000)) { ObserveAnchorWritten(noChangeDelta, "abilitynames", abilitynames); } } diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index 18078c97..ea8a1da9 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -164,7 +164,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { var elementLength = elementContent.Sum(e => e.Length); if (string.IsNullOrEmpty(length)) { - var bestAddress = StandardSearch(data, elementContent, elementLength, out int bestLength); + var bestAddress = StandardSearch(data, elementContent, elementLength, out int bestLength, runFilter); if (bestAddress == Pointer.NULL) return false; self = new ArrayRun(data, originalFormat + bestLength, string.Empty, bestAddress, bestLength, elementContent, data.GetNextRun(bestAddress).PointerSources, null); } else { @@ -178,7 +178,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return true; } - private static int StandardSearch(IDataModel data, List elementContent, int elementLength, out int bestLength) { + private static int StandardSearch(IDataModel data, List elementContent, int elementLength, out int bestLength, Func runFilter) { int bestAddress = Pointer.NULL; bestLength = 0; @@ -190,6 +190,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } var nextArray = nextRun; + // some searches allow special conditions on the run. For example, we could only be intersted in runs with >100 pointers leading to it. + if (runFilter != null && !runFilter(run)) { run = nextRun; continue; } + int currentLength = 0; int currentAddress = run.Start; while (true) { @@ -499,13 +502,13 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { private static bool DataMatchesElementFormat(IDataModel owner, int start, IReadOnlyList segments, IFormattedRun nextAnchor) { foreach (var segment in segments) { if (start + segment.Length > owner.Count) return false; - if (!DataMatchesSegmentFormat(owner, start, segment, nextAnchor)) return false; + if (!DataMatchesSegmentFormat(owner, start, segment, segments.Count, nextAnchor)) return false; start += segment.Length; } return true; } - private static bool DataMatchesSegmentFormat(IDataModel owner, int start, ArrayRunElementSegment segment, IFormattedRun nextAnchor) { + private static bool DataMatchesSegmentFormat(IDataModel owner, int start, ArrayRunElementSegment segment, int segmentCount, IFormattedRun nextAnchor) { if (start + segment.Length > nextAnchor.Start && nextAnchor is ArrayRun) return false; // don't blap over existing arrays switch (segment.Type) { case ElementContentType.PCS: @@ -522,6 +525,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { // to match arrays with junk PCS characters after the closing quote. if (Enumerable.Range(start + readLength, segment.Length - readLength).Any(i => PCSString.PCS[owner[i]] == null)) return false; + // if we end with a space, and the next one starts with a space, we probably have the data width wrong. + // We might be the start of a different data segment that is no longer pointed to. (Example: Vega/pokenames) + if (segmentCount == 1 && start % 4 == 0 && owner[start + segment.Length - 1] == 0x00 && owner[start + segment.Length] == 0x00) { + // if the next one starts on a 4-byte boundary, then we probably just skipped a few bytes between different data types, and _this_ section is still part of the _last_ run (example, Emerald Ability names) + // if the next one doesn't start on a 4-byte boundary, then we probably have the length wrong + var nextWordStart = (start + segment.Length + 3) / 4 * 4; + if (Enumerable.Range(start + segment.Length, nextWordStart - start - segment.Length).Any(i => owner[i] != 0x00) || owner[nextWordStart] == 0x00) return false; + } + // require that the overall thing still ends with 'FF' or '00' to avoid finding text of the wrong width. var lastByteInText = owner[start + segment.Length - 1]; if (lastByteInText != 0x00 && lastByteInText != 0xFF) return false; diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index b9e7113e..7f8b22a0 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -39,7 +39,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (run is ArrayRun || run is PCSRun || run.PointerSources == null) continue; // verify expected pointers to this - if (run.PointerSources.Count != 2) continue; + if (run.PointerSources.Count < 2 || run.PointerSources.Count > 10) continue; // verify limiter var length = data.ReadMultiByteValue(run.PointerSources[1] - 4, 4); diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 9ec8a66b..175a3cf3 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -16,13 +16,14 @@ namespace HavenSoft.HexManiac.Tests { public class AutoSearchTests { public static IEnumerable PokemonGames => new[] { - "Altair", - "Emerald", - "FireRed", - "LeafGreen", "Ruby", "Sapphire", - "DarkRisingKAIZO", + "FireRed", + "LeafGreen", + "Emerald", + "DarkRisingKAIZO", // from FireRed + "Vega 2019-04-20", // from FireRed + "Altair", // from Emerald }.Select(game => new object[] { "sampleFiles/Pokemon " + game + ".gba" }); [SkippableTheory] @@ -44,7 +45,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.MoveNamesTable); var run = (ArrayRun)model.GetNextAnchor(address); - Assert.Equal(355, run.ElementCount); + if (game.Contains("Vega")) Assert.Equal(512, run.ElementCount); + else Assert.Equal(355, run.ElementCount); } [SkippableTheory] @@ -95,6 +97,7 @@ namespace HavenSoft.HexManiac.Tests { else if (game.Contains("LeafGreen")) Assert.Equal(375, run.ElementCount); else if (game.Contains("Ruby")) Assert.Equal(349, run.ElementCount); else if (game.Contains("Sapphire")) Assert.Equal(349, run.ElementCount); + else if (game.Contains("Vega")) Assert.Equal(375, run.ElementCount); else throw new NotImplementedException(); } @@ -113,7 +116,7 @@ namespace HavenSoft.HexManiac.Tests { else if (game.Contains("LeafGreen")) Assert.Equal(107, run.ElementCount); else if (game.Contains("Ruby")) Assert.Equal(58, run.ElementCount); else if (game.Contains("Sapphire")) Assert.Equal(58, run.ElementCount); - else throw new NotImplementedException(); + else if (game.Contains("Vega")) Assert.Equal(107, run.ElementCount); } [SkippableTheory] @@ -125,9 +128,10 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "pokestats"); var run = (ArrayRun)model.GetNextAnchor(address); - var bulbasaurStats = model.Skip(run.Start + run.ElementLength).Take(6).ToArray(); - var compareSet = new[] { 45, 49, 49, 45, 65, 65 }; - for (int i = 0; i < compareSet.Length; i++) Assert.Equal(compareSet[i], bulbasaurStats[i]); + var firstPokemonStats = model.Skip(run.Start + run.ElementLength).Take(6).ToArray(); + var compareSet = new[] { 45, 49, 49, 45, 65, 65 }; // bulbasaur + if (game.Contains("Vega")) compareSet = new[] { 42, 53, 40, 70, 63, 40 }; // Nimbleaf + for (int i = 0; i < compareSet.Length; i++) Assert.Equal(compareSet[i], firstPokemonStats[i]); } [SkippableTheory] @@ -153,7 +157,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "eggmoves"); var run = (EggMoveRun)model.GetNextAnchor(address); - Assert.Equal(2, run.PointerSources.Count); + if (game.Contains("Vega")) Assert.Equal(3, run.PointerSources.Count); // there's a false positive in Vega... for now! Would be nice if this were 2, but it doesn't much matter. + else 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. From 4d9bf563196537776b5fef312c5c4e557a1cc770 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Sun, 19 May 2019 20:18:38 -0500 Subject: [PATCH 4/8] Bugfixes * allow editing PCSERROR cells. * GetOptions() for an enum with no source should return an empty array instead of null. --- .../Models/Runs/ArrayRunElementSegment.cs | 2 +- .../ViewModels/Visitors/CompleteEditOperation.cs | 14 ++++++++------ .../ViewModels/Visitors/StartCellEdit.cs | 4 +--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs index af34e9f8..6bf7b40a 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRunElementSegment.cs @@ -121,7 +121,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } public static IReadOnlyList GetOptions(IDataModel model, string enumName) { - if (!model.TryGetNameArray(enumName, out var enumArray)) return null; + if (!model.TryGetNameArray(enumName, out var enumArray)) return new string[0]; // array must be at least as long as than the current value var optionCount = enumArray.ElementCount; diff --git a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index 19176325..1265e063 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs @@ -64,7 +64,9 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { if (NewCell != null) NewCell = new HexElement(NewCell.Value, new Anchor(NewCell.Format, anchor.Name, anchor.Format, anchor.Sources)); } - public void Visit(PCS pcs, byte data) { + public void Visit(PCS pcs, byte data) => VisitPCS(pcs); + + private void VisitPCS(IDataFormatInstance pcs) { var currentText = CurrentText; if (currentText.StartsWith(StringDelimeter.ToString())) currentText = currentText.Substring(1); if (pcs.Position != 0 && CurrentText == StringDelimeter.ToString()) { @@ -82,7 +84,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Result = true; } - public void Visit(ErrorPCS pcs, byte data) => throw new NotImplementedException(); + public void Visit(ErrorPCS pcs, byte data) => VisitPCS(pcs); public void Visit(Ascii ascii, byte data) { CompleteAsciiEdit(ascii); @@ -229,7 +231,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { private void CompleteCharacterEdit(IDataFormat originalFormat) { var editText = CurrentText; if (editText.StartsWith("\"")) editText = editText.Substring(1); - var pcs = originalFormat as PCS; + var pcs = originalFormat as IDataFormatInstance; var escaped = originalFormat as EscapedPCS; var run = Model.GetNextRun(memoryLocation); @@ -237,11 +239,11 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { byte.Parse(CurrentText, NumberStyles.HexNumber) : (byte)Enumerable.Range(0, 0x100).First(i => PCSString.PCS[i] == editText); - var position = pcs != null ? pcs.Position : escaped.Position; - HandleLastCharacterChange(memoryLocation, editText, pcs, run, position, byteValue); + var position = pcs == null ? escaped.Position : pcs.Position; + HandleLastCharacterChange(memoryLocation, editText, run, position, byteValue); } - private void HandleLastCharacterChange(int memoryLocation, string editText, PCS pcs, IFormattedRun run, int position, byte byteValue) { + private void HandleLastCharacterChange(int memoryLocation, string editText, IFormattedRun run, int position, byte byteValue) { if (run is PCSRun) { // if its the last character being edited on a normal string, try to expand if (run.Length == position + 1) { diff --git a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index eb14afb5..894dabf6 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs @@ -97,9 +97,7 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Result = ViewPort.AllHexCharacters.Contains(Input); } - public void Visit(ErrorPCS pcs, byte data) { - throw new NotImplementedException(); - } + public void Visit(ErrorPCS pcs, byte data) => Visit((PCS)null, data); public void Visit(Ascii ascii, byte data) => Result = true; From 79a8a083af1b44e0dbe39a23de1afcbe20af4098 Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Tue, 21 May 2019 22:38:47 -0500 Subject: [PATCH 5/8] Clover support * Instead of bailing out of text tables as soon as we catch an element that starts with a space, just trim off any elements that start with a space after another fail condition. This lets us find the Fairy type, even after a number of " " types. * only do 'text must end in FF or 00' checks if it's a purely text table. For mixed tables, this restriction is less required. * if an egg stream has the same pokemon or move listed twice in a row, that's dumb. Recognize that as _not_ what we're looking for. --- src/HexManiac.Core/Models/AutoSearchModel.cs | 7 ++++--- src/HexManiac.Core/Models/Runs/ArrayRun.cs | 13 +++++++++++-- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 7 +++++++ src/HexManiac.Tests/AutoSearchTests.cs | 15 +++++++++++---- 4 files changed, 33 insertions(+), 9 deletions(-) diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index 38441e61..a86366ba 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -68,11 +68,11 @@ namespace HavenSoft.HexManiac.Core.Models { if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames, run => run.PointerSources.FirstOrDefault() < 0x100000)) { ObserveAnchorWritten(noChangeDelta, "abilitynames", abilitynames); } - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames, run => run.PointerSources.Count > 1)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames)) { ObserveAnchorWritten(noChangeDelta, "trainerclassnames", trainerclassnames); } } else { - if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames, run => run.PointerSources.Count > 1)) { + if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var trainerclassnames, run => run.PointerSources.Count > 1 && run.PointerSources.Count < 4)) { ObserveAnchorWritten(noChangeDelta, "trainerclassnames", trainerclassnames); } if (TrySearch(this, noChangeDelta, "[name\"\"13]", out var abilitynames, run => run.PointerSources.FirstOrDefault() < 0x100000)) { @@ -91,7 +91,8 @@ namespace HavenSoft.HexManiac.Core.Models { ObserveAnchorWritten(noChangeDelta, "items", itemdata); } - if (TrySearch(this, noChangeDelta, "[hp. attack. def. speed. spatk. spdef. type1.types type2.types catchRate. baseExp. evs: item1:items item2:items genderratio. steps2hatch. basehappiness. growthrate. egg1. egg2. ability1.abilitynames ability2.abilitynames runrate. unknown. padding:]pokenames", out var pokestatdata)) { + var format = "[hp. attack. def. speed. spatk. spdef. type1.types type2.types catchRate. baseExp. evs: item1:items item2:items genderratio. steps2hatch. basehappiness. growthrate. egg1. egg2. ability1.abilitynames ability2.abilitynames runrate. unknown. padding:]pokenames"; + if (TrySearch(this, noChangeDelta, format, out var pokestatdata, run => run.PointerSources.Count > 5)) { ObserveAnchorWritten(noChangeDelta, "pokestats", pokestatdata); } diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index ea8a1da9..8f0e783e 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -205,6 +205,14 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } } + // if what we found is just a text array, then remove any trailing elements starting with a space. + if (elementContent.Count == 1 && elementContent[0].Type == ElementContentType.PCS) { + while (data[currentAddress - elementLength] == 0x00) { + currentLength--; + currentAddress -= elementLength; + } + } + // we think we found some data! Make sure it's not just a bunch of 00's and FF's var dataEmpty = true; for (int i = 0; i < currentLength && currentLength > bestLength && dataEmpty; i++) dataEmpty = data[run.Start + i] == 0xFF || data[run.Start + i] == 0x00; @@ -514,7 +522,6 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { case ElementContentType.PCS: int readLength = PCSString.ReadString(owner, start, true, segment.Length); if (readLength < 2) return false; - if (owner[start] == 0x00) return false; // don't auto-find text that starts with a space, that's not how text ever starts. if (readLength > segment.Length) return false; if (Enumerable.Range(start, segment.Length).All(i => owner[i] == 0xFF)) return false; @@ -535,8 +542,10 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { } // require that the overall thing still ends with 'FF' or '00' to avoid finding text of the wrong width. + // the width check is less important if we have more complex data, so relax the condition (example: Clover) var lastByteInText = owner[start + segment.Length - 1]; - if (lastByteInText != 0x00 && lastByteInText != 0xFF) return false; + var lastByteIsReasonablEnd = lastByteInText == 0x00 || lastByteInText == 0xFF; + if (!lastByteIsReasonablEnd && segmentCount == 1) return false; return true; case ElementContentType.Integer: diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 7f8b22a0..d0aa9db7 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -53,8 +53,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { // verify content bool possibleMatch = true; + int lastValue = -1; for (int i = 0; i < length - 2; i++) { var value = data.ReadMultiByteValue(run.Start + i * 2, 2); + + // if the same byte pairs are repeated multiple times, then this pokemon is listed twice or has the same egg move twice. + // that seems unlikely... this is probably the wrong data. + if (value == lastValue) { possibleMatch = false; break; } + lastValue = value; + if (value == 0xFFFF) break; // early exit, the data was edited, but that's ok. Everything still matches up. if (value >= MagicNumber) { value -= MagicNumber; diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 175a3cf3..9ba1a950 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -23,6 +23,7 @@ namespace HavenSoft.HexManiac.Tests { "Emerald", "DarkRisingKAIZO", // from FireRed "Vega 2019-04-20", // from FireRed + "Clover", // from FireRed "Altair", // from Emerald }.Select(game => new object[] { "sampleFiles/Pokemon " + game + ".gba" }); @@ -46,6 +47,7 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.MoveNamesTable); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Vega")) Assert.Equal(512, run.ElementCount); + else if (game.Contains("Clover")) Assert.Equal(512, run.ElementCount); else Assert.Equal(355, run.ElementCount); } @@ -57,7 +59,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "abilitynames"); var run = (ArrayRun)model.GetNextAnchor(address); - Assert.Equal(78, run.ElementCount); + if (game.Contains("Clover")) Assert.Equal(156, run.ElementCount); + else Assert.Equal(78, run.ElementCount); } [SkippableTheory] @@ -68,7 +71,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "abilitydescriptions"); var run = (ArrayRun)model.GetNextAnchor(address); - Assert.Equal(78, run.ElementCount); + if (game.Contains("Clover")) Assert.Equal(156, run.ElementCount); + else Assert.Equal(78, run.ElementCount); } [SkippableTheory] @@ -79,7 +83,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "types"); var run = (ArrayRun)model.GetNextAnchor(address); - Assert.Equal(18, run.ElementCount); + if (game.Contains("Clover")) Assert.Equal(24, run.ElementCount); + else Assert.Equal(18, run.ElementCount); } [SkippableTheory] @@ -98,6 +103,7 @@ namespace HavenSoft.HexManiac.Tests { else if (game.Contains("Ruby")) Assert.Equal(349, run.ElementCount); else if (game.Contains("Sapphire")) Assert.Equal(349, run.ElementCount); else if (game.Contains("Vega")) Assert.Equal(375, run.ElementCount); + else if (game.Contains("Clover")) Assert.Equal(375, run.ElementCount); else throw new NotImplementedException(); } @@ -129,8 +135,9 @@ namespace HavenSoft.HexManiac.Tests { var run = (ArrayRun)model.GetNextAnchor(address); var firstPokemonStats = model.Skip(run.Start + run.ElementLength).Take(6).ToArray(); - var compareSet = new[] { 45, 49, 49, 45, 65, 65 }; // bulbasaur + var compareSet = new[] { 45, 49, 49, 45, 65, 65 }; // Bulbasaur if (game.Contains("Vega")) compareSet = new[] { 42, 53, 40, 70, 63, 40 }; // Nimbleaf + if (game.Contains("Clover")) compareSet = new[] { 56, 60, 55, 50, 47, 50 }; // Grasshole for (int i = 0; i < compareSet.Length; i++) Assert.Equal(compareSet[i], firstPokemonStats[i]); } From df472429da38920da80ecf756770a892079dfd6b Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Wed, 22 May 2019 17:46:48 -0500 Subject: [PATCH 6/8] added gaia, tests failing --- src/HexManiac.Tests/AutoSearchTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 9ba1a950..e1440af6 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -24,6 +24,7 @@ namespace HavenSoft.HexManiac.Tests { "DarkRisingKAIZO", // from FireRed "Vega 2019-04-20", // from FireRed "Clover", // from FireRed + "Gaia v3.2", // from FireRed "Altair", // from Emerald }.Select(game => new object[] { "sampleFiles/Pokemon " + game + ".gba" }); @@ -36,6 +37,7 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.PokemonNameTable); var run = (ArrayRun)model.GetNextAnchor(address); Assert.Equal(412, run.ElementCount); + // Gaia: 1110 or 1110 - 75 } [SkippableTheory] From b16e150967885a70ac33c697f3580a5c1ddd532a Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 24 May 2019 08:15:49 -0500 Subject: [PATCH 7/8] Gaia Support * allow the egg move limiter to be even more wrong (has to be within a given range, but doesn't have to point to FFFF) * have pokenames and pokestats negotiate for a reasonable length, instead of pokenames always winning * once a text table reaches length 100, allow for junk data in the bytes after the string closing FF. --- src/HexManiac.Core/Models/AutoSearchModel.cs | 11 ++++- src/HexManiac.Core/Models/Runs/ArrayRun.cs | 52 +++++++++++++------- src/HexManiac.Core/Models/Runs/EggMoveRun.cs | 4 +- src/HexManiac.Tests/AutoSearchTests.cs | 9 +++- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index a86366ba..43e1b011 100644 --- a/src/HexManiac.Core/Models/AutoSearchModel.cs +++ b/src/HexManiac.Core/Models/AutoSearchModel.cs @@ -91,8 +91,17 @@ namespace HavenSoft.HexManiac.Core.Models { ObserveAnchorWritten(noChangeDelta, "items", itemdata); } + // if the stat data doesn't match the pokenames length, use whichever is shorter. var format = "[hp. attack. def. speed. spatk. spdef. type1.types type2.types catchRate. baseExp. evs: item1:items item2:items genderratio. steps2hatch. basehappiness. growthrate. egg1. egg2. ability1.abilitynames ability2.abilitynames runrate. unknown. padding:]pokenames"; - if (TrySearch(this, noChangeDelta, format, out var pokestatdata, run => run.PointerSources.Count > 5)) { + var pokenames = GetNextRun(GetAddressFromAnchor(noChangeDelta, -1, EggMoveRun.PokemonNameTable)) as ArrayRun; + if (pokenames != null && TrySearch(this, noChangeDelta, format, out var pokestatdata, run => run.PointerSources.Count > 5)) { + if (pokestatdata.ElementCount < pokenames.ElementCount) { + pokenames = pokenames.Append(pokestatdata.ElementCount - pokenames.ElementCount); + ObserveAnchorWritten(noChangeDelta, EggMoveRun.PokemonNameTable, pokenames); + } else if (pokestatdata.ElementCount > pokenames.ElementCount) { + pokestatdata = pokestatdata.Append(pokenames.ElementCount - pokestatdata.ElementCount); + } + ObserveAnchorWritten(noChangeDelta, "pokestats", pokestatdata); } diff --git a/src/HexManiac.Core/Models/Runs/ArrayRun.cs b/src/HexManiac.Core/Models/Runs/ArrayRun.cs index 8f0e783e..4be6e058 100644 --- a/src/HexManiac.Core/Models/Runs/ArrayRun.cs +++ b/src/HexManiac.Core/Models/Runs/ArrayRun.cs @@ -103,12 +103,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (ElementContent.Count == 0) throw new ArrayRunParseException("Array Content must not be empty."); ElementLength = ElementContent.Sum(e => e.Length); + FormatMatchFlags flags = default; + if (ElementContent.Count == 1) flags |= FormatMatchFlags.IsSingleSegment; + if (length.Length == 0) { var nextRun = owner.GetNextRun(Start); while (nextRun is NoInfoRun && nextRun.Start < owner.Count) nextRun = owner.GetNextRun(nextRun.Start + 1); var byteLength = 0; var elementCount = 0; - while (Start + byteLength + ElementLength <= nextRun.Start && DataMatchesElementFormat(owner, Start + byteLength, ElementContent, nextRun)) { + while (Start + byteLength + ElementLength <= nextRun.Start && DataMatchesElementFormat(owner, Start + byteLength, ElementContent, flags, nextRun)) { byteLength += ElementLength; elementCount++; } @@ -193,11 +196,15 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { // some searches allow special conditions on the run. For example, we could only be intersted in runs with >100 pointers leading to it. if (runFilter != null && !runFilter(run)) { run = nextRun; continue; } + FormatMatchFlags flags = default; + if (elementContent.Count == 1) flags |= FormatMatchFlags.IsSingleSegment; + int currentLength = 0; int currentAddress = run.Start; while (true) { + if (currentLength > 100) flags |= FormatMatchFlags.AllowJunkAfterText; // we've gone long enough without junk data to be fairly sure that we're looking at something real if (nextArray.Start < currentAddress) nextArray = data.GetNextAnchor(nextArray.Start + 1); - if (DataMatchesElementFormat(data, currentAddress, elementContent, nextArray)) { + if (DataMatchesElementFormat(data, currentAddress, elementContent, flags, nextArray)) { currentLength++; currentAddress += elementLength; } else { @@ -239,6 +246,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { bestLength = matchedRun.ElementCount; } + FormatMatchFlags flags = default; + if (elementContent.Count == 1) flags |= FormatMatchFlags.IsSingleSegment; + for (var run = data.GetNextRun(0); run.Start < data.Count; run = data.GetNextRun(run.Start + run.Length + 1)) { if (!(run is PointerRun)) continue; var targetRun = data.GetNextRun(data.ReadPointer(run.Start)); @@ -252,16 +262,22 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { bool earlyExit = false; for (int i = 0; i < bestLength; i++) { var nextArray = data.GetNextAnchor(currentAddress + 1); - if (DataMatchesElementFormat(data, currentAddress, elementContent, nextArray)) { + if (DataMatchesElementFormat(data, currentAddress, elementContent, flags, nextArray)) { currentLength++; currentAddress += elementLength; } else { - earlyExit = true; + // as long as this array is at least 80% of the passed in array, we're fine and can say that these are matched. + // (the other one might have bad data at the end that needs to be removed) (example: see Gaia) + earlyExit = bestLength * .8 > currentLength; break; } } - if (!earlyExit) return targetRun.Start; + + if (!earlyExit) { + bestLength = currentLength; + return targetRun.Start; + } } return Pointer.NULL; @@ -507,16 +523,16 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { return run.ElementCount; } - private static bool DataMatchesElementFormat(IDataModel owner, int start, IReadOnlyList segments, IFormattedRun nextAnchor) { + private static bool DataMatchesElementFormat(IDataModel owner, int start, IReadOnlyList segments, FormatMatchFlags flags, IFormattedRun nextAnchor) { foreach (var segment in segments) { if (start + segment.Length > owner.Count) return false; - if (!DataMatchesSegmentFormat(owner, start, segment, segments.Count, nextAnchor)) return false; + if (!DataMatchesSegmentFormat(owner, start, segment, flags, nextAnchor)) return false; start += segment.Length; } return true; } - private static bool DataMatchesSegmentFormat(IDataModel owner, int start, ArrayRunElementSegment segment, int segmentCount, IFormattedRun nextAnchor) { + private static bool DataMatchesSegmentFormat(IDataModel owner, int start, ArrayRunElementSegment segment, FormatMatchFlags flags, IFormattedRun nextAnchor) { if (start + segment.Length > nextAnchor.Start && nextAnchor is ArrayRun) return false; // don't blap over existing arrays switch (segment.Type) { case ElementContentType.PCS: @@ -525,16 +541,11 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { if (readLength > segment.Length) return false; if (Enumerable.Range(start, segment.Length).All(i => owner[i] == 0xFF)) return false; - // in the initial 5 ROMs, any data after the close quote is either 0x00 or 0xFF - // but in fan games, this data may contain leftover junk bytes from what the text 'used' to be. - // this is because other popular existing editors don't clean up after themselves. - // in order to be compatible with games made with those editors, we have to allow automatic matches - // to match arrays with junk PCS characters after the closing quote. - if (Enumerable.Range(start + readLength, segment.Length - readLength).Any(i => PCSString.PCS[owner[i]] == null)) return false; - // if we end with a space, and the next one starts with a space, we probably have the data width wrong. // We might be the start of a different data segment that is no longer pointed to. (Example: Vega/pokenames) - if (segmentCount == 1 && start % 4 == 0 && owner[start + segment.Length - 1] == 0x00 && owner[start + segment.Length] == 0x00) { + // only do this check if the current element seems useful + var isBlank = Enumerable.Range(start, segment.Length).All(i => owner[i] == 0x00 || owner[i] == 0xFF); + if (!isBlank && flags.HasFlag(FormatMatchFlags.IsSingleSegment) && start % 4 == 0 && owner[start + segment.Length - 1] == 0x00 && owner[start + segment.Length] == 0x00) { // if the next one starts on a 4-byte boundary, then we probably just skipped a few bytes between different data types, and _this_ section is still part of the _last_ run (example, Emerald Ability names) // if the next one doesn't start on a 4-byte boundary, then we probably have the length wrong var nextWordStart = (start + segment.Length + 3) / 4 * 4; @@ -543,9 +554,10 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { // require that the overall thing still ends with 'FF' or '00' to avoid finding text of the wrong width. // the width check is less important if we have more complex data, so relax the condition (example: Clover) + // the width check is less important if we're already known to be in a long run (example: Gaia moves) var lastByteInText = owner[start + segment.Length - 1]; var lastByteIsReasonablEnd = lastByteInText == 0x00 || lastByteInText == 0xFF; - if (!lastByteIsReasonablEnd && segmentCount == 1) return false; + if (!flags.HasFlag(FormatMatchFlags.AllowJunkAfterText) && !lastByteIsReasonablEnd && flags.HasFlag(FormatMatchFlags.IsSingleSegment)) return false; return true; case ElementContentType.Integer: @@ -562,6 +574,12 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { throw new NotImplementedException(); } } + + [Flags] + public enum FormatMatchFlags { + IsSingleSegment = 0x01, + AllowJunkAfterText = 0x02, + } } public class ArrayRunParseException : Exception { diff --git a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 0dca54b9..690144be 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -48,11 +48,9 @@ namespace HavenSoft.HexManiac.Core.Models.Runs { 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 (length < 1000 || length > 7000) 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 != EndStream) continue; // verify content bool possibleMatch = true; diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index e1440af6..c398d215 100644 --- a/src/HexManiac.Tests/AutoSearchTests.cs +++ b/src/HexManiac.Tests/AutoSearchTests.cs @@ -36,8 +36,8 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, EggMoveRun.PokemonNameTable); var run = (ArrayRun)model.GetNextAnchor(address); - Assert.Equal(412, run.ElementCount); - // Gaia: 1110 or 1110 - 75 + if (game.Contains("Gaia")) Assert.Equal(914, run.ElementCount); + else Assert.Equal(412, run.ElementCount); } [SkippableTheory] @@ -50,6 +50,7 @@ namespace HavenSoft.HexManiac.Tests { var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Vega")) Assert.Equal(512, run.ElementCount); else if (game.Contains("Clover")) Assert.Equal(512, run.ElementCount); + else if (game.Contains("Gaia")) Assert.Equal(511, run.ElementCount); else Assert.Equal(355, run.ElementCount); } @@ -62,6 +63,7 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "abilitynames"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Clover")) Assert.Equal(156, run.ElementCount); + else if (game.Contains("Gaia")) Assert.Equal(188, run.ElementCount); else Assert.Equal(78, run.ElementCount); } @@ -74,6 +76,7 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "abilitydescriptions"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Clover")) Assert.Equal(156, run.ElementCount); + else if (game.Contains("Gaia")) Assert.Equal(188, run.ElementCount); else Assert.Equal(78, run.ElementCount); } @@ -86,6 +89,7 @@ namespace HavenSoft.HexManiac.Tests { var address = model.GetAddressFromAnchor(noChange, -1, "types"); var run = (ArrayRun)model.GetNextAnchor(address); if (game.Contains("Clover")) Assert.Equal(24, run.ElementCount); + else if (game.Contains("Gaia")) Assert.Equal(24, run.ElementCount); else Assert.Equal(18, run.ElementCount); } @@ -106,6 +110,7 @@ namespace HavenSoft.HexManiac.Tests { else if (game.Contains("Sapphire")) Assert.Equal(349, run.ElementCount); else if (game.Contains("Vega")) Assert.Equal(375, run.ElementCount); else if (game.Contains("Clover")) Assert.Equal(375, run.ElementCount); + else if (game.Contains("Gaia")) Assert.Equal(375, run.ElementCount); else throw new NotImplementedException(); } From 8cdecaa13168621c4e3af2c08cd23b8bc0d86fab Mon Sep 17 00:00:00 2001 From: Benjamin Popp Date: Fri, 24 May 2019 21:03:50 -0500 Subject: [PATCH 8/8] Context Menu Update Add Copy/Paste context items for basically everything. Add "Remove Format" option for egg moves. --- src/HexManiac.Core/ViewModels/ViewPort.cs | 7 ++- .../ViewModels/Visitors/ContextItemFactory.cs | 6 +- src/HexManiac.Tests/EggMoveTests.cs | 9 +++ src/HexManiac.Tests/ViewPortCursorTests.cs | 63 ++++++++++++------- 4 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/HexManiac.Core/ViewModels/ViewPort.cs b/src/HexManiac.Core/ViewModels/ViewPort.cs index 060c7d58..7823187c 100644 --- a/src/HexManiac.Core/ViewModels/ViewPort.cs +++ b/src/HexManiac.Core/ViewModels/ViewPort.cs @@ -488,7 +488,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels { var factory = new ContextItemFactory(this); var cell = currentView[SelectionStart.X, SelectionStart.Y]; cell.Format.Visit(factory, cell.Value); - return factory.Results; + var results = factory.Results.ToList(); + if (!SelectionStart.Equals(SelectionEnd)) { + results.Add(new ContextItem("Copy", Copy.Execute) { ShortcutText = "Ctrl+C" }); + } + results.Add(new ContextItem("Paste", arg => Edit(((IFileSystem)arg).CopyText)) { ShortcutText = "Ctrl+V" }); + return results; } public bool IsSelected(Point point) => selection.IsSelected(point); diff --git a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs index 26ea4f8b..5b025328 100644 --- a/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs +++ b/src/HexManiac.Core/ViewModels/Visitors/ContextItemFactory.cs @@ -117,14 +117,12 @@ namespace HavenSoft.HexManiac.Core.ViewModels.Visitors { Results.AddRange(GetTableChildren(arrayRun)); } - 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(EggSection section, byte data) => Visit((EggItem)null, 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" }); + Results.AddRange(GetFormattedChildren()); } private IEnumerable GetTableChildren(ArrayRun array) { diff --git a/src/HexManiac.Tests/EggMoveTests.cs b/src/HexManiac.Tests/EggMoveTests.cs index 47deeaf5..fc13fcb2 100644 --- a/src/HexManiac.Tests/EggMoveTests.cs +++ b/src/HexManiac.Tests/EggMoveTests.cs @@ -230,5 +230,14 @@ Water"; var format = (UnderEdit)viewPort[2, 0].Format; Assert.Equal(string.Empty, format.CurrentText); } + + [Fact] + public void CanRemoveEggMoveFormatFromContextMenu() { + CreateSimpleRun(); + + viewPort.SelectionStart = new Point(2, 0); + var items = viewPort.GetContextMenuItems(viewPort.SelectionStart); + items.Single(item => item.Text == "Clear Format"); + } } } diff --git a/src/HexManiac.Tests/ViewPortCursorTests.cs b/src/HexManiac.Tests/ViewPortCursorTests.cs index 111afaac..cd56f419 100644 --- a/src/HexManiac.Tests/ViewPortCursorTests.cs +++ b/src/HexManiac.Tests/ViewPortCursorTests.cs @@ -1,5 +1,6 @@ using HavenSoft.HexManiac.Core.Models; using HavenSoft.HexManiac.Core.ViewModels; +using System.Linq; using Xunit; namespace HavenSoft.HexManiac.Tests { @@ -212,9 +213,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void CanExpandSelection() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.Edit("<000100>"); viewPort.SelectionStart = new Point(1, 0); @@ -233,9 +232,7 @@ namespace HavenSoft.HexManiac.Tests { [InlineData(2)] [InlineData(3)] public void SelectingAnyOfAPointerSelectsAllOfAPointer(int index) { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.Edit("<000100>"); viewPort.SelectionStart = new Point(index, 0); @@ -248,9 +245,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void SelectLeftSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 0); viewPort.Edit("<000100>"); @@ -265,9 +260,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void SelectRightSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 0); viewPort.Edit("<000100>"); @@ -282,9 +275,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void SelectUpSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 0); viewPort.Edit("<000100>"); @@ -299,9 +290,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void SelectDownSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 1); viewPort.Edit("<000100>"); @@ -316,9 +305,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void HighlightLeftSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 1); viewPort.Edit("<000100>"); @@ -333,9 +320,7 @@ namespace HavenSoft.HexManiac.Tests { [Fact] public void HighlightRightSelectsWholePointer() { - var data = new byte[0x200]; - var model = new PokemonModel(data); - var viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + CreateStandardTestSetup(out var viewPort, out var model, out var data); viewPort.SelectionStart = new Point(4, 1); viewPort.Edit("<000100>"); @@ -347,5 +332,35 @@ namespace HavenSoft.HexManiac.Tests { Assert.True(viewPort.IsSelected(new Point(6, 1))); Assert.True(viewPort.IsSelected(new Point(7, 1))); } + + [Fact] + public void ContextMenuContainsCopyPaste() { + CreateStandardTestSetup(out var viewPort, out var model, out var data); + + viewPort.SelectionStart = new Point(2, 2); + viewPort.SelectionEnd = new Point(5, 2); + var items = viewPort.GetContextMenuItems(viewPort.SelectionStart); + items.Single(item => item.Text == "Copy"); + items.Single(item => item.Text == "Paste"); + + viewPort.Edit("<000100>"); + viewPort.SelectionStart = new Point(3, 2); + items = viewPort.GetContextMenuItems(viewPort.SelectionStart); + items.Single(item => item.Text == "Copy"); + items.Single(item => item.Text == "Paste"); + + viewPort.Edit("^text\"\" Hello World!\""); + viewPort.SelectionStart = new Point(5, 2); + viewPort.ExpandSelection(5, 2); + items = viewPort.GetContextMenuItems(viewPort.SelectionStart); + items.Single(item => item.Text == "Copy"); + items.Single(item => item.Text == "Paste"); + } + + private static void CreateStandardTestSetup(out ViewPort viewPort, out PokemonModel model, out byte[] data) { + data = new byte[0x200]; + model = new PokemonModel(data); + viewPort = new ViewPort("file.txt", model) { Width = 0x10, Height = 0x10 }; + } } }