diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fc867..05ec364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ This library uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 0.6.0 + +Replays can now be edited. This API is still a work in progress and currently has a couple problems which will be fixed later. See the remarks in the `ReplayEventsData` class for more information. + +### Added + +- Added `ReplayEventsData.InsertEvent` and `ReplayEventsData.RemoveEvent` methods. +- Added `ReplayEventsData.ChangeEntityType` method. This is a temporary method that will be removed in the future. + +### Changed + +- Spawn events now have a public setter for `EntityId`. This should never be used and will be removed in the future. +- `ReplayEventsData.AddEvent` now overwrites the `EntityId` to be correct. This will change in the future in a way that doesn't let you pass an `EntityId` to this method at all. + ## 0.5.0 ### Changed diff --git a/src/DevilDaggersInfo.Core.Replay/Events/BoidSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/BoidSpawnEvent.cs index 3f3f7df..02c5324 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/BoidSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/BoidSpawnEvent.cs @@ -13,6 +13,8 @@ public record BoidSpawnEvent(int EntityId, int SpawnerEntityId, BoidType BoidTyp public Vector3 Velocity = Velocity; public float Speed = Speed; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => BoidType switch { BoidType.Skull1 => EntityType.Skull1, diff --git a/src/DevilDaggersInfo.Core.Replay/Events/DaggerSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/DaggerSpawnEvent.cs index 609c392..4540d88 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/DaggerSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/DaggerSpawnEvent.cs @@ -12,6 +12,8 @@ public record DaggerSpawnEvent(int EntityId, int A, Int16Vec3 Position, Int16Mat public bool IsShot = IsShot; public DaggerType DaggerType = DaggerType; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => DaggerType switch { DaggerType.Level1 => EntityType.Level1Dagger, diff --git a/src/DevilDaggersInfo.Core.Replay/Events/Interfaces/IEntitySpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/Interfaces/IEntitySpawnEvent.cs index eb25782..7f21e8e 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/Interfaces/IEntitySpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/Interfaces/IEntitySpawnEvent.cs @@ -4,7 +4,7 @@ namespace DevilDaggersInfo.Core.Replay.Events.Interfaces; public interface IEntitySpawnEvent : IEvent { - int EntityId { get; } + int EntityId { get; internal set; } EntityType EntityType { get; } } diff --git a/src/DevilDaggersInfo.Core.Replay/Events/LeviathanSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/LeviathanSpawnEvent.cs index 78bbe15..1bf3819 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/LeviathanSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/LeviathanSpawnEvent.cs @@ -7,6 +7,8 @@ public record LeviathanSpawnEvent(int EntityId, int A) : IEntitySpawnEvent { public int A = A; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => EntityType.Leviathan; public void Write(BinaryWriter bw) diff --git a/src/DevilDaggersInfo.Core.Replay/Events/PedeSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/PedeSpawnEvent.cs index acd61ac..40df994 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/PedeSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/PedeSpawnEvent.cs @@ -12,6 +12,8 @@ public record PedeSpawnEvent(int EntityId, PedeType PedeType, int A, Vector3 Pos public Vector3 B = B; public Matrix3x3 Orientation = Orientation; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => PedeType switch { PedeType.Centipede => EntityType.Centipede, diff --git a/src/DevilDaggersInfo.Core.Replay/Events/SpiderEggSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/SpiderEggSpawnEvent.cs index 0441dbf..992edb6 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/SpiderEggSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/SpiderEggSpawnEvent.cs @@ -9,6 +9,8 @@ public record SpiderEggSpawnEvent(int EntityId, int SpawnerEntityId, Vector3 Pos public Vector3 Position = Position; public Vector3 TargetPosition = TargetPosition; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => EntityType.SpiderEgg; public void Write(BinaryWriter bw) diff --git a/src/DevilDaggersInfo.Core.Replay/Events/SpiderSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/SpiderSpawnEvent.cs index f0aeedc..558cd40 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/SpiderSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/SpiderSpawnEvent.cs @@ -10,6 +10,8 @@ public record SpiderSpawnEvent(int EntityId, SpiderType SpiderType, int A, Vecto public int A = A; public Vector3 Position = Position; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => SpiderType switch { SpiderType.Spider1 => EntityType.Spider1, diff --git a/src/DevilDaggersInfo.Core.Replay/Events/SquidSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/SquidSpawnEvent.cs index 4695749..efe1b4e 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/SquidSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/SquidSpawnEvent.cs @@ -12,6 +12,8 @@ public record SquidSpawnEvent(int EntityId, SquidType SquidType, int A, Vector3 public Vector3 Direction = Direction; public float RotationInRadians = RotationInRadians; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => SquidType switch { SquidType.Squid1 => EntityType.Squid1, diff --git a/src/DevilDaggersInfo.Core.Replay/Events/ThornSpawnEvent.cs b/src/DevilDaggersInfo.Core.Replay/Events/ThornSpawnEvent.cs index d77958e..40fc9c0 100644 --- a/src/DevilDaggersInfo.Core.Replay/Events/ThornSpawnEvent.cs +++ b/src/DevilDaggersInfo.Core.Replay/Events/ThornSpawnEvent.cs @@ -9,6 +9,8 @@ public record ThornSpawnEvent(int EntityId, int A, Vector3 Position, float Rotat public Vector3 Position = Position; public float RotationInRadians = RotationInRadians; + public int EntityId { get; set; } = EntityId; + public EntityType EntityType => EntityType.Thorn; public void Write(BinaryWriter bw) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 99a7542..b23b663 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -3,6 +3,21 @@ namespace DevilDaggersInfo.Core.Replay; +/// +/// Represents all the events in a replay. +/// +/// IMPORTANT: This API is unfinished and will change in the future. Right now, the class generally lets you corrupt its state for the sake of performance and ease of use. This will be solved in the future. +/// +/// When changing the internal type of a spawn event, be sure to also update the list of entity types using . +/// When adding or inserting a spawn event, the entity ID is re-calculated and overwritten. This will be changed in the future. +/// The event types currently let you change their ID, but this should only ever be done by the class itself. This will be removed in the future. +/// +/// +/// +// TODO: Rewrite: +// We should rewrite the event classes to be mutable structs and exclude EntityId and EntityType from them, then add a new wrapper class containing EntityId, EntityType, and TEventStruct as properties instead. This fixes point 2 above. +// The wrapper class should have an internal set for EntityId. This fixes point 3 above. +// Point 1 above could be solved by referencing to all Spawn events (instances of the wrapper class) from the _events list, instead of keeping a list of EntityType enums. public class ReplayEventsData { private readonly List _events = new(); @@ -31,6 +46,9 @@ public void Clear() public void AddEvent(IEvent e) { + if (e is IEntitySpawnEvent spawnEvent) + spawnEvent.EntityId = _entityTypes.Count; + _events.Add(e); _eventOffsetsPerTick[^1]++; @@ -39,4 +57,110 @@ public void AddEvent(IEvent e) else if (e is IEntitySpawnEvent ese) _entityTypes.Add(ese.EntityType); } + + public void RemoveEvent(int index) + { + if (index < 0 || index >= _events.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + IEvent e = _events[index]; + if (e is IEntitySpawnEvent spawnEvent) + { + _entityTypes.RemoveAt(spawnEvent.EntityId); + + // Decrement all entity IDs that are higher than the removed entity ID. + for (int i = 0; i < _events.Count; i++) + { + if (i == index) + continue; + + if (_events[i] is IEntitySpawnEvent otherSpawnEvent && otherSpawnEvent.EntityId > spawnEvent.EntityId) + otherSpawnEvent.EntityId--; + } + } + + _events.Remove(e); + + int? containingTick = null; + bool isInputsEvent = e is InputsEvent; + for (int i = 0; i < _eventOffsetsPerTick.Count; i++) + { + if (index >= _eventOffsetsPerTick[i]) + continue; // Skip ticks that are before the event. + + // Remove the tick offset when removing an inputs event. + if (!containingTick.HasValue && isInputsEvent) + { + _eventOffsetsPerTick.RemoveAt(i); + containingTick = i; + } + + // For every tick that is after the event, decrement the offset by 1. + if (_eventOffsetsPerTick.Count > i && _eventOffsetsPerTick[i] > 0) + _eventOffsetsPerTick[i]--; + + // If the tick offset is the same as the previous one, remove it. + if (i > 0 && _eventOffsetsPerTick.Count > i && _eventOffsetsPerTick[i] == _eventOffsetsPerTick[i - 1]) + _eventOffsetsPerTick.RemoveAt(i); + } + } + + public void InsertEvent(int index, IEvent e) + { + if (index < 0 || index > _events.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + if (e is IEntitySpawnEvent spawnEvent) + { + // Increment all entity IDs that are higher than the added entity ID. + int entityId = 1; // Skip 0 as it is always reserved. + for (int i = 0; i < _events.Count; i++) + { + if (i == index) + { + spawnEvent.EntityId = entityId; + _events.Insert(index, e); + _entityTypes.Insert(entityId, spawnEvent.EntityType); + } + else if (_events[i] is IEntitySpawnEvent otherSpawnEvent) + { + if (i >= index) + otherSpawnEvent.EntityId++; + else + entityId++; + } + } + } + else + { + _events.Insert(index, e); + } + + int? containingTick = null; + for (int i = 0; i < _eventOffsetsPerTick.Count; i++) + { + if (index >= _eventOffsetsPerTick[i]) + continue; // Skip ticks that are before the event. + + // The first tick that does not lie before the event is the tick that contains the event. + // Add new tick if needed. This always means an input event was added. + if (!containingTick.HasValue && e is InputsEvent) + { + int previousOffset = i > 0 ? _eventOffsetsPerTick[i - 1] : 0; + _eventOffsetsPerTick.Insert(i, previousOffset); + containingTick = i; + } + + // For every tick that is after the event, increment the offset by 1. + _eventOffsetsPerTick[i]++; + } + } + + public void ChangeEntityType(int entityId, EntityType entityType) + { + if (entityId < 0 || entityId >= _entityTypes.Count) + throw new ArgumentOutOfRangeException(nameof(entityId)); + + _entityTypes[entityId] = entityType; + } } diff --git a/src/DevilDaggersInfo.Core/DevilDaggersInfo.Core.csproj b/src/DevilDaggersInfo.Core/DevilDaggersInfo.Core.csproj index 63fc32d..d13c882 100644 --- a/src/DevilDaggersInfo.Core/DevilDaggersInfo.Core.csproj +++ b/src/DevilDaggersInfo.Core/DevilDaggersInfo.Core.csproj @@ -6,7 +6,7 @@ Copyright © Noah Stolk git https://github.com/NoahStolk/ddinfo-core - 0.5.0 + 0.6.0 diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayBinaryTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayBinaryTests.cs index 66ea95b..b387bb5 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayBinaryTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayBinaryTests.cs @@ -50,7 +50,7 @@ public void ParseAndCompileEvents() } [TestMethod] - public void ParseAndEditAndCompileEvents() + public void EditEventData() { string replayFilePath = Path.Combine("Resources", "SkullTest.ddreplay"); byte[] replayBuffer = File.ReadAllBytes(replayFilePath); diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs new file mode 100644 index 0000000..853d9b2 --- /dev/null +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -0,0 +1,584 @@ +using DevilDaggersInfo.Core.Replay.Events.Enums; +using DevilDaggersInfo.Core.Replay.Events.Interfaces; +using System.Diagnostics.CodeAnalysis; + +namespace DevilDaggersInfo.Core.Replay.Test; + +[TestClass] +[SuppressMessage("Major Bug", "S2583:Conditionally executed code should be reachable", Justification = "False positive. This analyzer doesn't always work correctly.")] +public class ReplayEventsEditingTests +{ + private const int _eventCount = 72; + private const int _entityCount = 6; + private const int _tickCount = 67; + + private readonly ReplayBinary _replay; + + public ReplayEventsEditingTests() + { + string replayFilePath = Path.Combine("Resources", "SkullTest.ddreplay"); + byte[] replayBuffer = File.ReadAllBytes(replayFilePath); + _replay = new(replayBuffer); + + // Check initial events. + Assert.AreEqual(_eventCount, _replay.EventsData.Events.Count); + + // Check initial entity types. + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + ValidateOriginalEntityTypes(); + + // Check initial event offsets per tick. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + ValidateOriginalTicks(_tickCount); + + // Check initial entity IDs. + ValidateOriginalEntityIds(); + } + + private static void AssertEntityId(IEvent e, int expectedEntityId) + where TEvent : IEntitySpawnEvent + { + Assert.IsInstanceOfType(e); + Assert.AreEqual(expectedEntityId, ((TEvent)e).EntityId); + } + + private void ValidateOriginalEntityTypes() + { + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + Assert.AreEqual(EntityType.Squid1, _replay.EventsData.EntityTypes[1]); + for (int i = 2; i < _entityCount; i++) + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[i]); + } + + private void ValidateOriginalTicks(int count) + { + int expectedOffset = 0; + for (int i = 0; i < count; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + private void ValidateOriginalEntityIds() + { + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[24], 3); + AssertEntityId(_replay.EventsData.Events[45], 4); + AssertEntityId(_replay.EventsData.Events[66], 5); + } + + [TestMethod] + public void AddGemEvent() + { + _replay.EventsData.AddEvent(new GemEvent()); + + // There should be one new event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + + // There shouldn't be any new ticks or entities. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + ValidateOriginalEntityIds(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + [DataRow(5)] + [DataRow(6)] + public void AddSpawnEvent(int ignoredEntityId) + { + // The entity ID should be changed to 6 regardless of the value of ignoredEntityId. + _replay.EventsData.AddEvent(new ThornSpawnEvent(ignoredEntityId, -1, default, 0)); + + // There should be one new event and one new entity. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_entityCount + 1, _replay.EventsData.EntityTypes.Count); + + // There shouldn't be any new ticks. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + + // The new entity should be a Thorn. + Assert.AreEqual(EntityType.Thorn, _replay.EventsData.EntityTypes[6]); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + ValidateOriginalEntityIds(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); + } + + [TestMethod] + public void AddInputsEvent() + { + _replay.EventsData.AddEvent(new InputsEvent(true, false, false, false, JumpType.None, ShootType.None, ShootType.None, 0, 0)); + + // There should be one new event and one new tick. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_tickCount + 1, _replay.EventsData.EventOffsetsPerTick.Count); + + // There shouldn't be any new entities. + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + ValidateOriginalEntityIds(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); + } + + [TestMethod] + public void RemoveHitEvent() + { + _replay.EventsData.RemoveEvent(0); + + // There should be one less event. + Assert.AreEqual(_eventCount - 1, _replay.EventsData.Events.Count); + + // There shouldn't be any new ticks or entities. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be decremented. + AssertEntityId(_replay.EventsData.Events[1], 1); + AssertEntityId(_replay.EventsData.Events[2], 2); + AssertEntityId(_replay.EventsData.Events[23], 3); + AssertEntityId(_replay.EventsData.Events[44], 4); + AssertEntityId(_replay.EventsData.Events[65], 5); + + // Offsets should be changed. + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void RemoveSpawnEvent() + { + _replay.EventsData.RemoveEvent(3); // Remove the first Skull spawn. + + // There should be one less event and one less entity. + Assert.AreEqual(_eventCount - 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_entityCount - 1, _replay.EventsData.EntityTypes.Count); + + // There shouldn't be any new ticks. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + + // There should be one less entity. + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + Assert.AreEqual(EntityType.Squid1, _replay.EventsData.EntityTypes[1]); + for (int i = 2; i < _entityCount - 1; i++) + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[i]); + + // Entity IDs should be changed. + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[23], 2); + AssertEntityId(_replay.EventsData.Events[44], 3); + AssertEntityId(_replay.EventsData.Events[65], 4); + + // Offsets should be changed. + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset++; // Squid spawn event. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [DataTestMethod] + [DataRow(4)] // Inputs event after the Squid and Skull spawns. + [DataRow(5)] // Inputs event without any additional events. + [DataRow(6)] // Inputs event without any additional events. + public void RemoveInputsEvent(int eventIndex) + { + _replay.EventsData.RemoveEvent(eventIndex); + + // There should be one less event and one less tick. + Assert.AreEqual(_eventCount - 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_tickCount - 1, _replay.EventsData.EventOffsetsPerTick.Count); + + // There shouldn't be any new entities. + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be decremented. + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[23], 3); + AssertEntityId(_replay.EventsData.Events[44], 4); + AssertEntityId(_replay.EventsData.Events[65], 5); + + // Offsets should be changed. + int expectedOffset = 0; + for (int i = 0; i < _tickCount - 1; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 20 or 40 or 60) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void RemoveAllEvents() + { + for (int i = 0; i < _eventCount - 2; i++) + _replay.EventsData.RemoveEvent(2); // Do not remove initial hits and initial inputs event. + + Assert.AreEqual(2, _replay.EventsData.Events.Count); + Assert.AreEqual(2, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(1, _replay.EventsData.EntityTypes.Count); + + Assert.IsInstanceOfType(_replay.EventsData.Events[0]); + Assert.IsInstanceOfType(_replay.EventsData.Events[1]); + + Assert.AreEqual(0, _replay.EventsData.EventOffsetsPerTick[0]); + Assert.AreEqual(2, _replay.EventsData.EventOffsetsPerTick[1]); + + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + } + + [TestMethod] + public void RemoveAllEventsReverse() + { + for (int i = _eventCount - 1; i >= 2; i--) + _replay.EventsData.RemoveEvent(i); // Do not remove initial hits and initial inputs event. + + Assert.AreEqual(2, _replay.EventsData.Events.Count); + Assert.AreEqual(2, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(1, _replay.EventsData.EntityTypes.Count); + + Assert.IsInstanceOfType(_replay.EventsData.Events[0]); + Assert.IsInstanceOfType(_replay.EventsData.Events[1]); + + Assert.AreEqual(0, _replay.EventsData.EventOffsetsPerTick[0]); + Assert.AreEqual(2, _replay.EventsData.EventOffsetsPerTick[1]); + + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + } + + [TestMethod] + public void InsertGemEventAtStart() + { + _replay.EventsData.InsertEvent(0, new GemEvent()); + + // There should be one new event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + + // There shouldn't be any new ticks or entities. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be incremented. + AssertEntityId(_replay.EventsData.Events[3], 1); + AssertEntityId(_replay.EventsData.Events[4], 2); + AssertEntityId(_replay.EventsData.Events[25], 3); + AssertEntityId(_replay.EventsData.Events[46], 4); + AssertEntityId(_replay.EventsData.Events[67], 5); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset += 2; // Hit event 53333... and Gem event. + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void InsertGemEvent() + { + _replay.EventsData.InsertEvent(10, new GemEvent()); + + // There should be one new event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + + // There shouldn't be any new ticks or entities. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be incremented. + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[25], 3); + AssertEntityId(_replay.EventsData.Events[46], 4); + AssertEntityId(_replay.EventsData.Events[67], 5); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i == 7) + expectedOffset++; // Gem event. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + public void InsertSpawnEventAtStart(int ignoredEntityId) + { + // The entity ID should be changed to 1 regardless of the value of ignoredEntityId. + _replay.EventsData.InsertEvent(0, new ThornSpawnEvent(ignoredEntityId, -1, default, 0)); + + // There should be one new event and one new entity. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_entityCount + 1, _replay.EventsData.EntityTypes.Count); + + // There shouldn't be any new ticks. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + + // The new entity should be a Thorn. + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + Assert.AreEqual(EntityType.Thorn, _replay.EventsData.EntityTypes[1]); + Assert.AreEqual(EntityType.Squid1, _replay.EventsData.EntityTypes[2]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[3]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[4]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[5]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[6]); + + AssertEntityId(_replay.EventsData.Events[0], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[4], 3); + AssertEntityId(_replay.EventsData.Events[25], 4); + AssertEntityId(_replay.EventsData.Events[46], 5); + AssertEntityId(_replay.EventsData.Events[67], 6); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset += 2; // Hit event 53333... and Thorn spawn event. + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + public void InsertSpawnEvent(int ignoredEntityId) + { + // The entity ID should be changed to 3 regardless of the value of ignoredEntityId. + _replay.EventsData.InsertEvent(10, new ThornSpawnEvent(ignoredEntityId, -1, default, 0)); + + // There should be one new event and one new entity. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_entityCount + 1, _replay.EventsData.EntityTypes.Count); + + // There shouldn't be any new ticks. + Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); + + // The new entity should be a Thorn. + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + Assert.AreEqual(EntityType.Squid1, _replay.EventsData.EntityTypes[1]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[2]); + Assert.AreEqual(EntityType.Thorn, _replay.EventsData.EntityTypes[3]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[4]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[5]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[6]); + + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[10], 3); + AssertEntityId(_replay.EventsData.Events[25], 4); + AssertEntityId(_replay.EventsData.Events[46], 5); + AssertEntityId(_replay.EventsData.Events[67], 6); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i == 7) + expectedOffset++; // Thorn spawn event. + else if (i is 21 or 41 or 61) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void InsertInputsEventAtStart() + { + _replay.EventsData.InsertEvent(0, new InputsEvent(true, false, false, false, JumpType.None, ShootType.None, ShootType.None, 0, 0)); + + // There should be one new event and one new tick. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_tickCount + 1, _replay.EventsData.EventOffsetsPerTick.Count); + + // There shouldn't be any new entities. + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be incremented. + AssertEntityId(_replay.EventsData.Events[3], 1); + AssertEntityId(_replay.EventsData.Events[4], 2); + AssertEntityId(_replay.EventsData.Events[25], 3); + AssertEntityId(_replay.EventsData.Events[46], 4); + AssertEntityId(_replay.EventsData.Events[67], 5); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount + 1; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 1) + expectedOffset++; // Hit event 53333... + else if (i == 2) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 22 or 42 or 62) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void InsertInputsEvent() + { + _replay.EventsData.InsertEvent(10, new InputsEvent(true, false, false, false, JumpType.None, ShootType.None, ShootType.None, 0, 0)); + + // There should be one new event and one new tick. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.Events.Count); + Assert.AreEqual(_tickCount + 1, _replay.EventsData.EventOffsetsPerTick.Count); + + // There shouldn't be any new entities. + Assert.AreEqual(_entityCount, _replay.EventsData.EntityTypes.Count); + ValidateOriginalEntityTypes(); + + // Entity IDs should be unchanged, but their indexes should be incremented. + AssertEntityId(_replay.EventsData.Events[2], 1); + AssertEntityId(_replay.EventsData.Events[3], 2); + AssertEntityId(_replay.EventsData.Events[25], 3); + AssertEntityId(_replay.EventsData.Events[46], 4); + AssertEntityId(_replay.EventsData.Events[67], 5); + + int expectedOffset = 0; + for (int i = 0; i < _tickCount + 1; i++) + { + int offset = _replay.EventsData.EventOffsetsPerTick[i]; + Assert.AreEqual(expectedOffset, offset); + + expectedOffset++; // Inputs event. + + if (i == 0) + expectedOffset++; // Hit event 53333... + else if (i == 1) + expectedOffset += 2; // Squid and Skull spawn events. + else if (i is 22 or 42 or 62) + expectedOffset++; // Skull spawn event. + } + } + + [TestMethod] + public void ChangeEntityType() + { + Assert.IsInstanceOfType(_replay.EventsData.Events[3]); + BoidSpawnEvent boidSpawnEvent = (BoidSpawnEvent)_replay.EventsData.Events[3]; + boidSpawnEvent.BoidType = BoidType.Skull2; + + _replay.EventsData.ChangeEntityType(2, EntityType.Skull2); + + Assert.AreEqual(EntityType.Zero, _replay.EventsData.EntityTypes[0]); + Assert.AreEqual(EntityType.Squid1, _replay.EventsData.EntityTypes[1]); + Assert.AreEqual(EntityType.Skull2, _replay.EventsData.EntityTypes[2]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[3]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[4]); + Assert.AreEqual(EntityType.Skull1, _replay.EventsData.EntityTypes[5]); + } +}