Merge pull request #30 from haven1433/dev

Autosearch Improvements
This commit is contained in:
haven1433
2019-05-24 21:08:40 -05:00
committed by GitHub
11 changed files with 205 additions and 96 deletions

View File

@@ -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);
}

View File

@@ -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<ArrayRunElementSegment> elementContent, int elementLength, out int bestLength) {
private static int StandardSearch(IDataModel data, List<ArrayRunElementSegment> elementContent, int elementLength, out int bestLength, Func<IFormattedRun, bool> 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<ArrayRunElementSegment> segments, IFormattedRun nextAnchor) {
private static bool DataMatchesElementFormat(IDataModel owner, int start, IReadOnlyList<ArrayRunElementSegment> 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 {

View File

@@ -121,7 +121,7 @@ namespace HavenSoft.HexManiac.Core.Models.Runs {
}
public static IReadOnlyList<string> 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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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) {

View File

@@ -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<IContextItem> GetTableChildren(ArrayRun array) {

View File

@@ -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;

View File

@@ -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<object[]> 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.

View File

@@ -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");
}
}
}

View File

@@ -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 };
}
}
}