diff --git a/src/Gen3Hex.Core/Gen3Hex.Core.csproj b/src/Gen3Hex.Core/Gen3Hex.Core.csproj index aed3e12f..d2219b23 100644 --- a/src/Gen3Hex.Core/Gen3Hex.Core.csproj +++ b/src/Gen3Hex.Core/Gen3Hex.Core.csproj @@ -46,6 +46,7 @@ + diff --git a/src/Gen3Hex.Core/Models/PCSString.cs b/src/Gen3Hex.Core/Models/PCSString.cs index b1c26e9d..a48c1c30 100644 --- a/src/Gen3Hex.Core/Models/PCSString.cs +++ b/src/Gen3Hex.Core/Models/PCSString.cs @@ -50,6 +50,7 @@ namespace HavenSoft.Gen3Hex.Core.Models { } public static List Convert(string input) { + if (input.StartsWith("\"")) input = input.Substring(1); // trim leading " at start of string var result = new List(); int index = 0; diff --git a/src/Gen3Hex.Core/Models/SearchByte.cs b/src/Gen3Hex.Core/Models/SearchByte.cs new file mode 100644 index 00000000..d91d0c13 --- /dev/null +++ b/src/Gen3Hex.Core/Models/SearchByte.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace HavenSoft.Gen3Hex.Core.Models { + public interface ISearchByte { + bool Match(byte value); + } + public class SearchByte : ISearchByte { + private readonly byte value; + public SearchByte(int value) => this.value = (byte)value; + public static explicit operator SearchByte (byte value) => new SearchByte(value); + public bool Match(byte value) => value == this.value; + } + public class PCSSearchByte : ISearchByte { + private readonly byte match1, match2; + public PCSSearchByte(int value) { + match1 = (byte)value; + match2 = match1; + if (PCSString.PCS[match1] == null) return; + var valueAsChar = PCSString.PCS[match1][0]; + if (char.IsUpper(valueAsChar)) { + Debug.Assert(IndexOf(PCSString.PCS, "a") - IndexOf(PCSString.PCS, "A") == 0x1A); + match2 += 0x1A; + } + } + public bool Match(byte value) => value == match1 || value == match2; + private static int IndexOf(IReadOnlyList pcs, string value) => Enumerable.Range(0, 0x100).Single(i => pcs[i] == value); + } +} diff --git a/src/Gen3Hex.Core/ViewModels/ViewPort.cs b/src/Gen3Hex.Core/ViewModels/ViewPort.cs index 49609351..5e832c31 100644 --- a/src/Gen3Hex.Core/ViewModels/ViewPort.cs +++ b/src/Gen3Hex.Core/ViewModels/ViewPort.cs @@ -344,23 +344,40 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { public IReadOnlyList Find(string rawSearch) { var results = new List(); - var cleanedSearchString = rawSearch.Replace(" ", string.Empty).ToUpper(); - var searchBytes = new List(); + var cleanedSearchString = rawSearch.ToUpper(); + var searchBytes = new List(); var hex = "0123456789ABCDEF"; + // precheck: it might be a string with no quotes, we should check for matches for that. + if (cleanedSearchString.Length > 3 && !cleanedSearchString.Contains('"')) { + var pcsBytes = PCSString.Convert(cleanedSearchString); + searchBytes.AddRange(pcsBytes.Select(b => new PCSSearchByte(b))); + for (int i = 0; i < Model.Count - searchBytes.Count; i++) { + for (int j = 0; j < searchBytes.Count; j++) { + if (!searchBytes[j].Match(Model[i + j])) break; + if (j == searchBytes.Count - 1) results.Add(i); + } + } + searchBytes.Clear(); + } + for (int i = 0; i < cleanedSearchString.Length;) { + if (cleanedSearchString[i] == ' ') { + i++; + continue; + } if (cleanedSearchString[i] == '<') { var pointerEnd = cleanedSearchString.IndexOf('>', i); if (pointerEnd == -1) { OnError(this, "Search mismatch: no closing >"); return results; } var pointerContents = cleanedSearchString.Substring(i + 1, pointerEnd - i - 2); var address = Model.GetAddressFromAnchor(-1, pointerContents); if (address != Pointer.NULL) { - searchBytes.Add((byte)(address >> 0)); - searchBytes.Add((byte)(address >> 8)); - searchBytes.Add((byte)(address >> 16)); - searchBytes.Add(0x08); + searchBytes.Add((SearchByte)(address >> 0)); + searchBytes.Add((SearchByte)(address >> 8)); + searchBytes.Add((SearchByte)(address >> 16)); + searchBytes.Add((SearchByte)0x08); } else if (pointerContents.All(hex.Contains) && pointerContents.Length <= 6) { - searchBytes.AddRange(Parse(pointerContents).Reverse().Append((byte)0x08)); + searchBytes.AddRange(Parse(pointerContents).Reverse().Append((byte)0x08).Select(b => (SearchByte)b)); } else { OnError(this, $"Could not parse pointer <{pointerContents}>"); return results; @@ -368,18 +385,31 @@ namespace HavenSoft.Gen3Hex.Core.ViewModels { i = pointerEnd + 1; continue; } + if (cleanedSearchString[i] == '"') { + var endIndex = cleanedSearchString.IndexOf('"', i + 1); + while (endIndex > i && cleanedSearchString[endIndex - 1] == '\\') endIndex = cleanedSearchString.IndexOf('"', endIndex + 1); + if (endIndex > i) { + var pcsBytes = PCSString.Convert(cleanedSearchString.Substring(i, endIndex + 1 - i)); + i = endIndex + 1; + if (i == cleanedSearchString.Length) pcsBytes.RemoveAt(pcsBytes.Count - 1); + searchBytes.AddRange(pcsBytes.Select(b => new PCSSearchByte(b))); + continue; + } + } if (cleanedSearchString.Length >= i + 2 && cleanedSearchString.Substring(i, 2).All(hex.Contains)) { - searchBytes.AddRange(Parse(cleanedSearchString.Substring(i, 2))); + searchBytes.AddRange(Parse(cleanedSearchString.Substring(i, 2)).Select(b => (SearchByte)b)); i += 2; continue; } - OnError(this, $"Could not parse search term {cleanedSearchString.Substring(i)}"); + if (results.Count == 0) { + OnError(this, $"Could not parse search term {cleanedSearchString.Substring(i)}"); + } return results; } for (int i = 0; i < Model.Count - searchBytes.Count; i++) { for (int j = 0; j < searchBytes.Count; j++) { - if (Model[i + j] != searchBytes[j]) break; + if (!searchBytes[j].Match(Model[i + j])) break; if (j == searchBytes.Count - 1) results.Add(i); } } diff --git a/src/Gen3Hex.Tests/StringModelTests.cs b/src/Gen3Hex.Tests/StringModelTests.cs index ad21b8cf..8030d6e5 100644 --- a/src/Gen3Hex.Tests/StringModelTests.cs +++ b/src/Gen3Hex.Tests/StringModelTests.cs @@ -231,7 +231,31 @@ namespace HavenSoft.Gen3Hex.Tests { Assert.Equal("^bob\"\" \"Hello World!\"", fileSystem.CopyText); } - // TODO Find + [Fact] + public void FindForStringsIsNotCaseSensitive() { + var buffer = Enumerable.Repeat((byte)0xFF, 0x200).ToArray(); + for (int i = 0; i < 0x10; i++) buffer[i] = 0x00; + var model = new PointerAndStringModel(buffer); + var viewPort = new ViewPort(new LoadedFile("test.txt", buffer), model) { Width = 0x10, Height = 0x10 }; + viewPort.Edit("^bob\"\" \"Text and BULBASAUR!\""); + + var results = viewPort.Find("\"bulbasaur\""); + Assert.Single(results); + Assert.Equal(9, results[0]); + } + + [Fact] + public void FindForStringsWorksWithoutQuotes() { + var buffer = Enumerable.Repeat((byte)0xFF, 0x200).ToArray(); + for (int i = 0; i < 0x10; i++) buffer[i] = 0x00; + var model = new PointerAndStringModel(buffer); + var viewPort = new ViewPort(new LoadedFile("test.txt", buffer), model) { Width = 0x10, Height = 0x10 }; + viewPort.Edit("^bob\"\" \"Text and BULBASAUR!\""); + + var results = viewPort.Find("bulbasaur"); + Assert.Single(results); + Assert.Equal(9, results[0]); + } // TODO undo/redo