Skip to content

Commit 2c66d25

Browse files
HandyS11claude
andcommitted
feat(2a): add MarkerEventClassifier mapping deltas to domain events
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent b2f4927 commit 2c66d25

4 files changed

Lines changed: 172 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace RustPlusBot.Features.Events.Classifying;
2+
3+
/// <summary>A classified live map event.</summary>
4+
public enum MapEventKind
5+
{
6+
/// <summary>A cargo ship entered the map.</summary>
7+
CargoEntered = 0,
8+
9+
/// <summary>A cargo ship left the map.</summary>
10+
CargoLeft = 1,
11+
12+
/// <summary>A patrol helicopter entered the map.</summary>
13+
HeliEntered = 2,
14+
15+
/// <summary>A patrol helicopter left the map.</summary>
16+
HeliLeft = 3,
17+
18+
/// <summary>A Chinook spawned.</summary>
19+
ChinookSpawned = 4,
20+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using RustPlusBot.Abstractions.Events;
2+
using RustPlusBot.Abstractions.Time;
3+
using RustPlusBot.Features.Connections.Listening;
4+
5+
namespace RustPlusBot.Features.Events.Classifying;
6+
7+
/// <summary>Maps raw marker add/remove deltas to domain map events.</summary>
8+
/// <param name="clock">Stamps each produced event.</param>
9+
internal sealed class MarkerEventClassifier(IClock clock)
10+
{
11+
/// <summary>Classifies one marker-change delta into zero or more domain events.</summary>
12+
/// <param name="evt">The marker-change delta.</param>
13+
/// <returns>The classified events, in delta order (added first, then removed).</returns>
14+
public IReadOnlyList<RustMapEvent> Classify(MapMarkersChangedEvent evt)
15+
{
16+
ArgumentNullException.ThrowIfNull(evt);
17+
var now = clock.UtcNow;
18+
var events = new List<RustMapEvent>();
19+
20+
foreach (var m in evt.Added)
21+
{
22+
MapEventKind? kind = m.Kind switch
23+
{
24+
MarkerKind.CargoShip => MapEventKind.CargoEntered,
25+
MarkerKind.PatrolHelicopter => MapEventKind.HeliEntered,
26+
MarkerKind.Chinook => MapEventKind.ChinookSpawned,
27+
_ => null, // Crate/Other → no event (the game no longer sends crate markers).
28+
};
29+
if (kind is { } k)
30+
{
31+
events.Add(new RustMapEvent(k, m.X, m.Y, evt.Dimensions, now));
32+
}
33+
}
34+
35+
foreach (var m in evt.Removed)
36+
{
37+
MapEventKind? kind = m.Kind switch
38+
{
39+
MarkerKind.CargoShip => MapEventKind.CargoLeft,
40+
MarkerKind.PatrolHelicopter => MapEventKind.HeliLeft,
41+
_ => null, // Chinook/Crate removal is silent.
42+
};
43+
if (kind is { } k)
44+
{
45+
events.Add(new RustMapEvent(k, m.X, m.Y, evt.Dimensions, now));
46+
}
47+
}
48+
49+
return events;
50+
}
51+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using RustPlusBot.Features.Connections.Listening;
2+
3+
namespace RustPlusBot.Features.Events.Classifying;
4+
5+
/// <summary>A classified live map event with its location and time.</summary>
6+
/// <param name="Kind">The event kind.</param>
7+
/// <param name="X">World X coordinate.</param>
8+
/// <param name="Y">World Y coordinate.</param>
9+
/// <param name="Dimensions">Map dimensions for grid rendering, or null.</param>
10+
/// <param name="AtUtc">When the event was observed.</param>
11+
public sealed record RustMapEvent(MapEventKind Kind, float X, float Y, MapDimensions? Dimensions, DateTimeOffset AtUtc);
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
using NSubstitute;
2+
using RustPlusBot.Abstractions.Events;
3+
using RustPlusBot.Abstractions.Time;
4+
using RustPlusBot.Features.Connections.Listening;
5+
using RustPlusBot.Features.Events.Classifying;
6+
7+
namespace RustPlusBot.Features.Events.Tests.Classifying;
8+
9+
public sealed class MarkerEventClassifierTests
10+
{
11+
private static readonly Guid Server = Guid.NewGuid();
12+
private static readonly DateTimeOffset Now = new(2026, 6, 17, 12, 0, 0, TimeSpan.Zero);
13+
14+
private static MarkerEventClassifier Build()
15+
{
16+
var clock = Substitute.For<IClock>();
17+
clock.UtcNow.Returns(Now);
18+
return new MarkerEventClassifier(clock);
19+
}
20+
21+
private static MapMarkersChangedEvent Evt(
22+
IReadOnlyList<MapMarkerSnapshot> added,
23+
IReadOnlyList<MapMarkerSnapshot> removed) =>
24+
new(1UL, Server, new MapDimensions(4000u, 4000u, 500), added, removed);
25+
26+
[Fact]
27+
public void Cargo_added_is_CargoEntered()
28+
{
29+
var result = Build().Classify(Evt(
30+
[new MapMarkerSnapshot(1, MarkerKind.CargoShip, 10f, 20f, null)], []));
31+
32+
var e = Assert.Single(result);
33+
Assert.Equal(MapEventKind.CargoEntered, e.Kind);
34+
Assert.Equal(Now, e.AtUtc);
35+
Assert.NotNull(e.Dimensions);
36+
}
37+
38+
[Fact]
39+
public void Cargo_removed_is_CargoLeft()
40+
{
41+
var result = Build().Classify(Evt([],
42+
[new MapMarkerSnapshot(1, MarkerKind.CargoShip, 10f, 20f, null)]));
43+
Assert.Equal(MapEventKind.CargoLeft, Assert.Single(result).Kind);
44+
}
45+
46+
[Fact]
47+
public void Heli_added_and_removed_map_to_entered_and_left()
48+
{
49+
Assert.Equal(MapEventKind.HeliEntered, Assert.Single(Build().Classify(
50+
Evt([new MapMarkerSnapshot(2, MarkerKind.PatrolHelicopter, 0f, 0f, null)], []))).Kind);
51+
Assert.Equal(MapEventKind.HeliLeft, Assert.Single(Build().Classify(
52+
Evt([], [new MapMarkerSnapshot(2, MarkerKind.PatrolHelicopter, 0f, 0f, null)]))).Kind);
53+
}
54+
55+
[Fact]
56+
public void Chinook_added_is_spawned_but_removal_is_silent()
57+
{
58+
Assert.Equal(MapEventKind.ChinookSpawned, Assert.Single(Build().Classify(
59+
Evt([new MapMarkerSnapshot(3, MarkerKind.Chinook, 0f, 0f, null)], []))).Kind);
60+
Assert.Empty(Build().Classify(
61+
Evt([], [new MapMarkerSnapshot(3, MarkerKind.Chinook, 0f, 0f, null)])));
62+
}
63+
64+
[Fact]
65+
public void Crate_and_other_markers_produce_nothing()
66+
{
67+
// The game no longer sends crate markers; MarkerKind.Crate (and Other) classify to nothing.
68+
Assert.Empty(Build().Classify(Evt(
69+
[
70+
new MapMarkerSnapshot(4, MarkerKind.Crate, 0f, 0f, null),
71+
new MapMarkerSnapshot(5, MarkerKind.Other, 0f, 0f, null)
72+
],
73+
[
74+
new MapMarkerSnapshot(6, MarkerKind.Crate, 0f, 0f, null),
75+
new MapMarkerSnapshot(7, MarkerKind.Other, 0f, 0f, null)
76+
])));
77+
}
78+
79+
[Fact]
80+
public void Multiple_deltas_produce_multiple_events()
81+
{
82+
var result = Build().Classify(Evt(
83+
[
84+
new MapMarkerSnapshot(1, MarkerKind.CargoShip, 0f, 0f, null),
85+
new MapMarkerSnapshot(3, MarkerKind.Chinook, 0f, 0f, null)
86+
],
87+
[new MapMarkerSnapshot(2, MarkerKind.PatrolHelicopter, 0f, 0f, null)]));
88+
Assert.Equal(3, result.Count);
89+
}
90+
}

0 commit comments

Comments
 (0)