diff --git a/NHSE.Core/Structures/Villager/IVillagerOrigin.cs b/NHSE.Core/Structures/Villager/IVillagerOrigin.cs index 8dd90ef..cc1a67e 100644 --- a/NHSE.Core/Structures/Villager/IVillagerOrigin.cs +++ b/NHSE.Core/Structures/Villager/IVillagerOrigin.cs @@ -41,8 +41,6 @@ private static void ChangeToHostTown(this IVillagerOrigin visit, IVillagerOrigin { var hostTown = host.GetTownIdentity(); var visitTown = visit.GetTownIdentity(); - if (hostTown.SequenceEqual(visitTown)) - return; visitData.ReplaceOccurrences(visitTown, hostTown); } @@ -50,8 +48,6 @@ private static void ChangeToHostPlayer(this IVillagerOrigin visit, IVillagerOrig { var hostPlayer = host.GetPlayerIdentity(); var visitPlayer = visit.GetPlayerIdentity(); - if (hostPlayer.SequenceEqual(visitPlayer)) - return; visitData.ReplaceOccurrences(visitPlayer, hostPlayer); } } \ No newline at end of file diff --git a/NHSE.Core/Util/ArrayUtil.cs b/NHSE.Core/Util/ArrayUtil.cs index 87c8fb5..76f5475 100644 --- a/NHSE.Core/Util/ArrayUtil.cs +++ b/NHSE.Core/Util/ArrayUtil.cs @@ -7,67 +7,25 @@ namespace NHSE.Core; /// public static class ArrayUtil { - //public static int ReplaceOccurrences(this Span array, ReadOnlySpan pattern, ReadOnlySpan swap) - //{ - // int count = 0; - // int ofs = 0; - // while (true) - // { - // var index = array[ofs..].IndexOf(pattern); - // if (index == -1) - // return count; - // ofs += index; - // - // swap.CopyTo(array[ofs..]); - // ofs += swap.Length; // skip past swapped data - // ++count; - // } - //} - public static int ReplaceOccurrences(this Span array, ReadOnlySpan pattern, ReadOnlySpan swap) { + if (pattern.Length != swap.Length) + return -1; + if (pattern.SequenceEqual(swap)) + return 0; + int count = 0; + int ofs = 0; while (true) { - int ofs = IndexOfBytes(array, pattern); - if (ofs == -1) + var index = array[ofs..].IndexOf(pattern); + if (index == -1) return count; + ofs += index; + swap.CopyTo(array[ofs..]); + ofs += swap.Length; // skip past swapped data ++count; } } - - /// - /// Finds a provided within the supplied . - /// - /// Array to look in - /// Pattern to look for - /// Starting offset to look from - /// Amount of entries to look through - /// Index the pattern occurs at; if not found, returns -1. - public static int IndexOfBytes(ReadOnlySpan array, ReadOnlySpan pattern, int startIndex = 0, int length = -1) - { - int len = pattern.Length; - int endIndex = length > 0 - ? startIndex + length - : array.Length - len - startIndex; - - endIndex = Math.Min(array.Length - pattern.Length, endIndex); - - int i = startIndex; - int j = 0; - while (true) - { - if (pattern[j] != array[i + j]) - { - if (++i == endIndex) - return -1; - j = 0; - } - else if (++j == len) - { - return i; - } - } - } } \ No newline at end of file diff --git a/NHSE.Tests/ArrayUtilTests.cs b/NHSE.Tests/ArrayUtilTests.cs new file mode 100644 index 0000000..f56588b --- /dev/null +++ b/NHSE.Tests/ArrayUtilTests.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using FluentAssertions; +using NHSE.Core; +using Xunit; + +namespace NHSE.Tests; + +public class ArrayUtilTests +{ + [Fact] + public void ReplaceOccurrences_WhenPatternNotFound_ReturnsZero() + { + byte[] array = [0x01, 0x02, 0x03, 0x04, 0x05]; + byte[] pattern = [0xAA, 0xBB]; + byte[] swap = [0xCC, 0xDD]; + + var count = array.ReplaceOccurrences(pattern, swap); + + count.Should().Be(0); + array.Should().BeEquivalentTo([0x01, 0x02, 0x03, 0x04, 0x05]); + } + + [Fact] + public void ReplaceOccurrences_WhenSingleOccurrence_ReplacesAndReturnsOne() + { + byte[] array = [0x01, 0xAA, 0xBB, 0x04, 0x05]; + byte[] pattern = [0xAA, 0xBB]; + byte[] swap = [0xCC, 0xDD]; + + var count = array.ReplaceOccurrences(pattern, swap); + + count.Should().Be(1); + array.Should().BeEquivalentTo([0x01, 0xCC, 0xDD, 0x04, 0x05]); + } + + [Fact] + public void ReplaceOccurrences_WhenMultipleOccurrences_ReplacesAllAndReturnsCount() + { + byte[] array = [0xAA, 0xBB, 0x03, 0xAA, 0xBB, 0x06, 0xAA, 0xBB]; + byte[] pattern = [0xAA, 0xBB]; + byte[] swap = [0xCC, 0xDD]; + + var count = array.ReplaceOccurrences(pattern, swap); + + count.Should().Be(3); + array.Should().BeEquivalentTo([0xCC, 0xDD, 0x03, 0xCC, 0xDD, 0x06, 0xCC, 0xDD]); + } + + [Fact] + public void ReplaceOccurrences_WhenConsecutiveOccurrences_ReplacesAll() + { + byte[] array = [0xAA, 0xBB, 0xAA, 0xBB, 0xAA, 0xBB]; + byte[] pattern = [0xAA, 0xBB]; + byte[] swap = [0xCC, 0xDD]; + + var count = array.ReplaceOccurrences(pattern, swap); + + count.Should().Be(3); + array.Should().BeEquivalentTo([0xCC, 0xDD, 0xCC, 0xDD, 0xCC, 0xDD]); + } + + [Fact] + public void ReplaceOccurrences_WhenSwapContainsPattern_DoesNotCauseInfiniteLoop() + { + // Swap contains the original pattern - must skip past swapped data + byte[] array = [0x01, 0xAA, 0xBB, 0x04]; + byte[] pattern = [0xAA, 0xBB]; + byte[] swap = [0xAA, 0xBB]; // Same as pattern + + var count = array.ReplaceOccurrences(pattern, swap); + + count.Should().Be(0); + array.Should().BeEquivalentTo([0x01, 0xAA, 0xBB, 0x04]); + } + + [Fact] + public void ReplaceOccurrences_WhenLargeFileWithRandomPlacements_ReplacesAllOccurrences() + { + const int fileSize = 1024 * 1024; // 1 MB + const int sequenceLength = 0x13; // 19 bytes + const int seed = 42; + + var random = new Random(seed); + Span file = new byte[fileSize]; + random.NextBytes(file); + + // Create two distinct random sequences + Span patternA = stackalloc byte[sequenceLength]; + Span patternB = stackalloc byte[sequenceLength]; + random.NextBytes(patternA); + random.NextBytes(patternB); + + // Ensure patterns are different + patternB[0] = (byte)(patternA[0] ^ 0xFF); + + // Insert pattern A at random non-overlapping offsets + var insertedOffsets = new List(); + int currentPosition = 0; + + while (currentPosition + sequenceLength <= fileSize) + { + // Random gap between 0 and 1000 bytes, then add sequence length to avoid overlap + int gap = random.Next(0, 1000); + int nextOffset = currentPosition + gap; + + if (nextOffset + sequenceLength > fileSize) + break; + + // Insert pattern A at this offset + patternA.CopyTo(file[nextOffset..]); + insertedOffsets.Add(nextOffset); + + // Move past the inserted sequence + currentPosition = nextOffset + sequenceLength; + } + + // Run the replacement + var count = file.ReplaceOccurrences(patternA, patternB); + + // Verify count matches + count.Should().Be(insertedOffsets.Count); + + // Verify each offset now contains pattern B + foreach (var offset in insertedOffsets) + { + var actual = file.Slice(offset, sequenceLength); + actual.SequenceEqual(patternB).Should().BeTrue(because: $"offset {offset} should have been replaced"); + } + } +}