Skip to content

Commit 5b95505

Browse files
HandyS11claude
andcommitted
fix(connections): guard TeamStateTracker with a lock for concurrent !afk reads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 98e812f commit 5b95505

1 file changed

Lines changed: 32 additions & 26 deletions

File tree

src/RustPlusBot.Features.Connections/Listening/TeamStateTracker.cs

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
namespace RustPlusBot.Features.Connections.Listening;
44

55
/// <summary>Diffs successive team snapshots into presence transitions. One instance per connected window.</summary>
6-
/// <remarks>Not thread-safe: the supervisor calls <see cref="Diff"/> from a single poll loop.</remarks>
76
internal sealed class TeamStateTracker
87
{
8+
private readonly object _gate = new();
99
private Dictionary<ulong, TeamMemberSnapshot>? _baseline;
1010
private readonly Dictionary<ulong, DateTimeOffset> _stillSince = new();
1111
private readonly HashSet<ulong> _afk = new();
@@ -26,32 +26,35 @@ public IReadOnlyList<PlayerTransition> Diff(
2626

2727
var current = snapshot.Members.ToDictionary(m => m.SteamId);
2828

29-
if (_baseline is null)
29+
lock (_gate)
3030
{
31-
_baseline = current;
32-
foreach (var m in current.Values)
31+
if (_baseline is null)
3332
{
34-
_stillSince[m.SteamId] = now;
35-
}
33+
_baseline = current;
34+
foreach (var m in current.Values)
35+
{
36+
_stillSince[m.SteamId] = now;
37+
}
3638

37-
return [];
38-
}
39+
return [];
40+
}
3941

40-
var transitions = new List<PlayerTransition>();
41-
foreach (var (id, nowMember) in current)
42-
{
43-
if (!_baseline.TryGetValue(id, out var was))
42+
var transitions = new List<PlayerTransition>();
43+
foreach (var (id, nowMember) in current)
4444
{
45-
_stillSince[id] = now; // prime new member's stillness clock
46-
continue;
45+
if (!_baseline.TryGetValue(id, out var was))
46+
{
47+
_stillSince[id] = now; // prime new member's stillness clock
48+
continue;
49+
}
50+
51+
AddPresenceTransitions(transitions, id, was, nowMember, snapshot);
52+
UpdateAfk(transitions, id, was, nowMember, now, afkThreshold, afkEpsilon);
4753
}
4854

49-
AddPresenceTransitions(transitions, id, was, nowMember, snapshot);
50-
UpdateAfk(transitions, id, was, nowMember, now, afkThreshold, afkEpsilon);
55+
_baseline = current;
56+
return transitions;
5157
}
52-
53-
_baseline = current;
54-
return transitions;
5558
}
5659

5760
private static void AddPresenceTransitions(
@@ -119,17 +122,20 @@ private void UpdateAfk(
119122
/// <param name="now">The current wall-clock time used to compute each member's still duration.</param>
120123
public IReadOnlyList<AfkMember> CurrentAfk(DateTimeOffset now)
121124
{
122-
var result = new List<AfkMember>();
123-
foreach (var id in _afk)
125+
lock (_gate)
124126
{
125-
if (_baseline is not null && _baseline.TryGetValue(id, out var m))
127+
var result = new List<AfkMember>();
128+
foreach (var id in _afk)
126129
{
127-
var since = _stillSince.TryGetValue(id, out var s) ? s : now;
128-
result.Add(new AfkMember(id, m.Name, now - since));
130+
if (_baseline is not null && _baseline.TryGetValue(id, out var m))
131+
{
132+
var since = _stillSince.TryGetValue(id, out var s) ? s : now;
133+
result.Add(new AfkMember(id, m.Name, now - since));
134+
}
129135
}
130-
}
131136

132-
return result;
137+
return result;
138+
}
133139
}
134140

135141
private static (float X, float Y)? ResolveDeathLocation(

0 commit comments

Comments
 (0)