From 1941c6d9fc15df97b9e8869c3a4634147dd3ec51 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sat, 14 Oct 2023 23:26:52 +0200 Subject: [PATCH 01/12] Implement inserting and deleting replay events --- .../ReplayEventsData.cs | 61 +++++++++ .../ReplayBinaryTests.cs | 2 +- .../ReplayEventsEditingTests.cs | 116 ++++++++++++++++++ 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 99a7542..a24d425 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -39,4 +39,65 @@ 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)); + + _events.RemoveAt(index); + + 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. + containingTick ??= i; + + // For every tick that is after the event, decrement the offset by 1. + _eventOffsetsPerTick[i]--; + } + + // Remove empty tick if needed. This always means an input event was removed. + if (containingTick.HasValue && _eventOffsetsPerTick[containingTick.Value] == 0) + { + _eventOffsetsPerTick.RemoveAt(containingTick.Value); + } + } + + public void InsertEvent(int index, IEvent e) + { + if (index < 0 || index > _events.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _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) + { + _eventOffsetsPerTick.Insert(index, 0); + containingTick = i; + } + + // For every tick that is after the event, increment the offset by 1. + _eventOffsetsPerTick[i]++; + } + } + + public void ChangeEntityType(int index, EntityType entityType) + { + if (index < 0 || index >= _entityTypes.Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + _entityTypes[index] = entityType; + } } 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..08417e2 --- /dev/null +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -0,0 +1,116 @@ +using DevilDaggersInfo.Core.Replay.Events.Enums; +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); + } + + 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. + } + } + + [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(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + } + + [TestMethod] + public void AddSpawnEvent() + { + // TODO: The entity ID should be calculated automatically. + const int entityId = 6; + _replay.EventsData.AddEvent(new ThornSpawnEvent(entityId, -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[entityId]); + + // Original data should be unchanged. + ValidateOriginalEntityTypes(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + } + + [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(); + ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + } +} From c9c12a53990f50e24a61f0844f1576a148bb25ad Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sat, 14 Oct 2023 23:55:39 +0200 Subject: [PATCH 02/12] Fix off-by-one error --- .../ReplayEventsData.cs | 4 +-- .../ReplayEventsEditingTests.cs | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index a24d425..65ff298 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -50,7 +50,7 @@ public void RemoveEvent(int index) int? containingTick = null; for (int i = 0; i < _eventOffsetsPerTick.Count; i++) { - if (index > _eventOffsetsPerTick[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. @@ -77,7 +77,7 @@ public void InsertEvent(int index, IEvent e) int? containingTick = null; for (int i = 0; i < _eventOffsetsPerTick.Count; i++) { - if (index > _eventOffsetsPerTick[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. diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index 08417e2..b50ee60 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -73,6 +73,7 @@ public void AddGemEvent() // Original data should be unchanged. ValidateOriginalEntityTypes(); ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); } [TestMethod] @@ -95,6 +96,7 @@ public void AddSpawnEvent() // Original data should be unchanged. ValidateOriginalEntityTypes(); ValidateOriginalTicks(_tickCount - 1); // Except for the last tick, which now has an extra event. + Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); } [TestMethod] @@ -112,5 +114,37 @@ public void AddInputsEvent() // Original data should be unchanged. ValidateOriginalEntityTypes(); 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(); + + // 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. + } } } From 8f1e1a1251605f5446b76547a1c0ec9e42a79c71 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:02:10 +0200 Subject: [PATCH 03/12] Fix not removing entity type when removing spawn event --- .../ReplayEventsData.cs | 6 +++- .../ReplayEventsEditingTests.cs | 36 +++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 65ff298..e75565d 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -45,7 +45,11 @@ public void RemoveEvent(int index) if (index < 0 || index >= _events.Count) throw new ArgumentOutOfRangeException(nameof(index)); - _events.RemoveAt(index); + IEvent e = _events[index]; + if (e is IEntitySpawnEvent spawnEvent) + _entityTypes.RemoveAt(spawnEvent.EntityId); + + _events.Remove(e); int? containingTick = null; for (int i = 0; i < _eventOffsetsPerTick.Count; i++) diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index b50ee60..e095150 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -147,4 +147,40 @@ public void RemoveHitEvent() 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]); + + // 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. + } + } } From 1994d35881b652a83c6b56db78697bd3529e990a Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:10:31 +0200 Subject: [PATCH 04/12] Fix not always removing tick offsets correctly --- .../ReplayEventsData.cs | 15 ++++---- .../ReplayEventsEditingTests.cs | 36 +++++++++++++++++++ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index e75565d..745e70c 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -52,23 +52,22 @@ public void RemoveEvent(int index) _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. - // The first tick that does not lie before the event is the tick that contains the event. - containingTick ??= i; + // 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. _eventOffsetsPerTick[i]--; } - - // Remove empty tick if needed. This always means an input event was removed. - if (containingTick.HasValue && _eventOffsetsPerTick[containingTick.Value] == 0) - { - _eventOffsetsPerTick.RemoveAt(containingTick.Value); - } } public void InsertEvent(int index, IEvent e) diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index e095150..1fe9588 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -183,4 +183,40 @@ public void RemoveSpawnEvent() 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(); + + // 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. + } + } } From 316a5830327da6792ff79aa7314ba2d41ed2163c Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:35:35 +0200 Subject: [PATCH 05/12] Fix not decrementing entity IDs when removing spawn event --- .../Events/BoidSpawnEvent.cs | 2 + .../Events/DaggerSpawnEvent.cs | 2 + .../Events/Interfaces/IEntitySpawnEvent.cs | 2 +- .../Events/LeviathanSpawnEvent.cs | 2 + .../Events/PedeSpawnEvent.cs | 2 + .../Events/SpiderEggSpawnEvent.cs | 2 + .../Events/SpiderSpawnEvent.cs | 2 + .../Events/SquidSpawnEvent.cs | 2 + .../Events/ThornSpawnEvent.cs | 2 + .../ReplayEventsData.cs | 12 ++++++ .../ReplayEventsEditingTests.cs | 43 +++++++++++++++++++ 11 files changed, 72 insertions(+), 1 deletion(-) 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 745e70c..d3ad661 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -47,8 +47,20 @@ public void RemoveEvent(int 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; diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index 1fe9588..d343852 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -1,4 +1,5 @@ using DevilDaggersInfo.Core.Replay.Events.Enums; +using DevilDaggersInfo.Core.Replay.Events.Interfaces; using System.Diagnostics.CodeAnalysis; namespace DevilDaggersInfo.Core.Replay.Test; @@ -29,6 +30,16 @@ public ReplayEventsEditingTests() // 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() @@ -58,6 +69,15 @@ private void ValidateOriginalTicks(int count) } } + 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() { @@ -72,6 +92,7 @@ public void AddGemEvent() // 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]); } @@ -95,6 +116,7 @@ public void AddSpawnEvent() // 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]); } @@ -113,6 +135,7 @@ public void AddInputsEvent() // 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]); } @@ -132,6 +155,13 @@ public void RemoveHitEvent() // 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++) @@ -166,6 +196,12 @@ public void RemoveSpawnEvent() 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++) @@ -202,6 +238,13 @@ public void RemoveInputsEvent(int eventIndex) // 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++) From 5f5184cd46341e5a7e809ba1dbbfd4e04d752c43 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 00:56:22 +0200 Subject: [PATCH 06/12] Fix not removing useless tick offsets --- .../ReplayEventsData.cs | 7 +++- .../ReplayEventsEditingTests.cs | 38 +++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index d3ad661..0a2f112 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -78,7 +78,12 @@ public void RemoveEvent(int index) } // For every tick that is after the event, decrement the offset by 1. - _eventOffsetsPerTick[i]--; + 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); } } diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index d343852..1e951f4 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -262,4 +262,42 @@ public void RemoveInputsEvent(int eventIndex) 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]); + } } From e0b49a0c19b3c4d1852ba85c420ee226c1efc7c6 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 01:58:28 +0200 Subject: [PATCH 07/12] Fix not adding new entity type and not incrementing next entity IDs on event insert --- .../ReplayEventsData.cs | 15 ++ .../ReplayEventsEditingTests.cs | 129 ++++++++++++++++++ 2 files changed, 144 insertions(+) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 0a2f112..3b69f05 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -93,6 +93,21 @@ public void InsertEvent(int index, IEvent e) throw new ArgumentOutOfRangeException(nameof(index)); _events.Insert(index, e); + if (e is IEntitySpawnEvent spawnEvent) + { + // TODO: EntityId should be automatically calculated and not taken from the event. + _entityTypes.Insert(spawnEvent.EntityId, spawnEvent.EntityType); + + // Increment all entity IDs that are higher than the added 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++; + } + } int? containingTick = null; for (int i = 0; i < _eventOffsetsPerTick.Count; i++) diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index 1e951f4..c135c5a 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -300,4 +300,133 @@ public void RemoveAllEventsReverse() 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. + } + } + + // TODO: Add tests: InsertSpawnEventAtStart, InsertInputsEventAtStart, InsertInputsEvent. + + [TestMethod] + public void InsertSpawnEvent() + { + _replay.EventsData.InsertEvent(10, new ThornSpawnEvent(3, -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. + } + } } From fd5b870f5ee9e4cbe6c061f7334f2312e54096d2 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:13:37 +0200 Subject: [PATCH 08/12] Fix not inserting new tick offset correctly --- .../ReplayEventsData.cs | 3 +- .../ReplayEventsEditingTests.cs | 121 +++++++++++++++++- 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 3b69f05..85ab00c 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -119,7 +119,8 @@ public void InsertEvent(int index, IEvent e) // Add new tick if needed. This always means an input event was added. if (!containingTick.HasValue && e is InputsEvent) { - _eventOffsetsPerTick.Insert(index, 0); + int previousOffset = i > 0 ? _eventOffsetsPerTick[i - 1] : 0; + _eventOffsetsPerTick.Insert(i, previousOffset); containingTick = i; } diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index c135c5a..8726200 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -381,7 +381,50 @@ public void InsertGemEvent() } } - // TODO: Add tests: InsertSpawnEventAtStart, InsertInputsEventAtStart, InsertInputsEvent. + [TestMethod] + public void InsertSpawnEventAtStart() + { + _replay.EventsData.InsertEvent(0, new ThornSpawnEvent(1, -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. + } + } [TestMethod] public void InsertSpawnEvent() @@ -429,4 +472,80 @@ public void InsertSpawnEvent() 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. + } + } + + // TODO: Add tests: ChangeEntityType. } From 77f90e1ec3bf47e8a0ef6b8e937fc4bb1a7e7af1 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:20:28 +0200 Subject: [PATCH 09/12] Add ChangeEntityType test --- .../ReplayEventsData.cs | 8 ++++---- .../ReplayEventsEditingTests.cs | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 85ab00c..10e4a72 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -129,11 +129,11 @@ public void InsertEvent(int index, IEvent e) } } - public void ChangeEntityType(int index, EntityType entityType) + public void ChangeEntityType(int entityId, EntityType entityType) { - if (index < 0 || index >= _entityTypes.Count) - throw new ArgumentOutOfRangeException(nameof(index)); + if (entityId < 0 || entityId >= _entityTypes.Count) + throw new ArgumentOutOfRangeException(nameof(entityId)); - _entityTypes[index] = entityType; + _entityTypes[entityId] = entityType; } } diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index 8726200..7f78b41 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -547,5 +547,20 @@ public void InsertInputsEvent() } } - // TODO: Add tests: ChangeEntityType. + [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]); + } } From cb890512a530777a0a25958c317441f91f2df945 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 15:45:25 +0200 Subject: [PATCH 10/12] Automatically overwrite entity ID for new spawn events --- .../ReplayEventsData.cs | 38 +++++++++++++---- .../ReplayEventsEditingTests.cs | 42 +++++++++++++------ 2 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index 10e4a72..d8189e9 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -3,6 +3,16 @@ namespace DevilDaggersInfo.Core.Replay; +/// +/// Represents all the events in a replay. +/// +/// IMPORTANT: This class generally lets you corrupt its state for the sake of performance and ease of use. +/// +/// 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. TODO: We should probably 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. +/// +/// +/// public class ReplayEventsData { private readonly List _events = new(); @@ -31,6 +41,9 @@ public void Clear() public void AddEvent(IEvent e) { + if (e is IEntitySpawnEvent spawnEvent) + spawnEvent.EntityId = _entityTypes.Count; + _events.Add(e); _eventOffsetsPerTick[^1]++; @@ -92,22 +105,31 @@ public void InsertEvent(int index, IEvent e) if (index < 0 || index > _events.Count) throw new ArgumentOutOfRangeException(nameof(index)); - _events.Insert(index, e); if (e is IEntitySpawnEvent spawnEvent) { - // TODO: EntityId should be automatically calculated and not taken from the event. - _entityTypes.Insert(spawnEvent.EntityId, spawnEvent.EntityType); - // 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) - continue; - - if (_events[i] is IEntitySpawnEvent otherSpawnEvent && otherSpawnEvent.EntityId >= spawnEvent.EntityId) - otherSpawnEvent.EntityId++; + { + 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++) diff --git a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs index 7f78b41..853d9b2 100644 --- a/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs +++ b/src/test/DevilDaggersInfo.Core.Replay.Test/ReplayEventsEditingTests.cs @@ -97,12 +97,18 @@ public void AddGemEvent() Assert.AreEqual(_eventCount + 1, _replay.EventsData.EventOffsetsPerTick[^1]); } - [TestMethod] - public void AddSpawnEvent() + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + [DataRow(5)] + [DataRow(6)] + public void AddSpawnEvent(int ignoredEntityId) { - // TODO: The entity ID should be calculated automatically. - const int entityId = 6; - _replay.EventsData.AddEvent(new ThornSpawnEvent(entityId, -1, default, 0)); + // 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); @@ -112,7 +118,7 @@ public void AddSpawnEvent() Assert.AreEqual(_tickCount, _replay.EventsData.EventOffsetsPerTick.Count); // The new entity should be a Thorn. - Assert.AreEqual(EntityType.Thorn, _replay.EventsData.EntityTypes[entityId]); + Assert.AreEqual(EntityType.Thorn, _replay.EventsData.EntityTypes[6]); // Original data should be unchanged. ValidateOriginalEntityTypes(); @@ -381,10 +387,16 @@ public void InsertGemEvent() } } - [TestMethod] - public void InsertSpawnEventAtStart() + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + public void InsertSpawnEventAtStart(int ignoredEntityId) { - _replay.EventsData.InsertEvent(0, new ThornSpawnEvent(1, -1, default, 0)); + // 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); @@ -426,10 +438,16 @@ public void InsertSpawnEventAtStart() } } - [TestMethod] - public void InsertSpawnEvent() + [DataTestMethod] + [DataRow(0)] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + public void InsertSpawnEvent(int ignoredEntityId) { - _replay.EventsData.InsertEvent(10, new ThornSpawnEvent(3, -1, default, 0)); + // 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); From 65d31af377db754e571f403b1319dd011836aa58 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:00:40 +0200 Subject: [PATCH 11/12] Add comments --- src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs index d8189e9..b23b663 100644 --- a/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs +++ b/src/DevilDaggersInfo.Core.Replay/ReplayEventsData.cs @@ -6,13 +6,18 @@ namespace DevilDaggersInfo.Core.Replay; /// /// Represents all the events in a replay. /// -/// IMPORTANT: This class generally lets you corrupt its state for the sake of performance and ease of use. +/// 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. TODO: We should probably 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. +/// 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(); From f518ec73875228a86e45ba3569dfa934d67bdc52 Mon Sep 17 00:00:00 2001 From: Noah Stolk <31079637+NoahStolk@users.noreply.github.com> Date: Sun, 15 Oct 2023 16:04:44 +0200 Subject: [PATCH 12/12] Update changelog and version --- CHANGELOG.md | 14 ++++++++++++++ .../DevilDaggersInfo.Core.csproj | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) 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/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