diff --git a/src/HexManiac.Core/Models/AutoSearchModel.cs b/src/HexManiac.Core/Models/AutoSearchModel.cs index 5cc52945..43e1b011 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)) { 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 && run.PointerSources.Count < 4)) { 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); } } @@ -91,7 +91,17 @@ 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)) { + // 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"; + 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 4f0a04b8..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++; } @@ -164,7 +167,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 +181,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,18 +193,38 @@ 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; } + + 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 { break; } } - if (bestLength < currentLength) { + + // 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; + + if (bestLength < currentLength && !dataEmpty) { bestLength = currentLength; bestAddress = run.Start; } @@ -223,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)); @@ -236,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; @@ -455,8 +487,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(".:")) { @@ -490,35 +523,41 @@ 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, 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, 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: int readLength = PCSString.ReadString(owner, start, true, segment.Length); - if (readLength == -1) return false; + if (readLength < 2) return false; 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. - // 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) + // 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; + 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. + // 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]; - if (lastByteInText != 0x00 && lastByteInText != 0xFF) return false; + var lastByteIsReasonablEnd = lastByteInText == 0x00 || lastByteInText == 0xFF; + if (!flags.HasFlag(FormatMatchFlags.AllowJunkAfterText) && !lastByteIsReasonablEnd && flags.HasFlag(FormatMatchFlags.IsSingleSegment)) return false; return true; case ElementContentType.Integer: @@ -535,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/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/Models/Runs/EggMoveRun.cs b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs index 6a22b867..690144be 100644 --- a/src/HexManiac.Core/Models/Runs/EggMoveRun.cs +++ b/src/HexManiac.Core/Models/Runs/EggMoveRun.cs @@ -42,22 +42,27 @@ 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); // 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; + 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 == EndStream) 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.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/CompleteEditOperation.cs b/src/HexManiac.Core/ViewModels/Visitors/CompleteEditOperation.cs index abb4e250..0fadc495 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/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.Core/ViewModels/Visitors/StartCellEdit.cs b/src/HexManiac.Core/ViewModels/Visitors/StartCellEdit.cs index 1cf29833..83df205a 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; diff --git a/src/HexManiac.Tests/AutoSearchTests.cs b/src/HexManiac.Tests/AutoSearchTests.cs index 350fc9ff..c398d215 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; @@ -15,13 +16,16 @@ 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 + "Clover", // from FireRed + "Gaia v3.2", // from FireRed + "Altair", // from Emerald }.Select(game => new object[] { "sampleFiles/Pokemon " + game + ".gba" }); [SkippableTheory] @@ -32,7 +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); + if (game.Contains("Gaia")) Assert.Equal(914, run.ElementCount); + else Assert.Equal(412, run.ElementCount); } [SkippableTheory] @@ -43,7 +48,10 @@ 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 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); } [SkippableTheory] @@ -54,7 +62,9 @@ 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 if (game.Contains("Gaia")) Assert.Equal(188, run.ElementCount); + else Assert.Equal(78, run.ElementCount); } [SkippableTheory] @@ -65,7 +75,9 @@ 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 if (game.Contains("Gaia")) Assert.Equal(188, run.ElementCount); + else Assert.Equal(78, run.ElementCount); } [SkippableTheory] @@ -76,7 +88,9 @@ 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 if (game.Contains("Gaia")) Assert.Equal(24, run.ElementCount); + else Assert.Equal(18, run.ElementCount); } [SkippableTheory] @@ -88,12 +102,16 @@ 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 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(); } [SkippableTheory] @@ -104,13 +122,14 @@ 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("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("Altair")) Assert.Equal(66, 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 if (game.Contains("Vega")) Assert.Equal(107, run.ElementCount); } [SkippableTheory] @@ -122,9 +141,11 @@ 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 + 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]); } [SkippableTheory] @@ -150,7 +171,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. 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 }; + } } }