mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 23:48:45 -07:00
Import march-2020-03-10 from private repo (scrubbed 2026-05-27)
Targets Rec Room 2020.03.10 client (also serves 2020.03.06).
Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces dorknet-clientpatch-{key}.zip
for v1-march-* Release uploads.
- .github/workflows/release.yml — on tag push publishes both
dorknet-server-march_2020_03_10-win-x64.zip and
dorknet-clientpatch-march_2020_03_10.zip as Release assets.
Same scrub rules as december; tools/build-patcher-zip.ps1 added to
the maintainer-side allow-list so it survives.
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
using System.Collections.Concurrent;
|
||||
using StackExchange.Redis;
|
||||
|
||||
namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Cross-replica tracker for the set of currently-online players (anyone
|
||||
/// with at least one active SignalR connection somewhere in the cluster).
|
||||
///
|
||||
/// **Replaces the static <c>NotifyHub._byPlayer</c> dictionary** which
|
||||
/// only tracked connections on the local replica. With horizontal scale,
|
||||
/// admin tools that asked "is player X online" got a per-replica answer
|
||||
/// — they'd see a player as offline if that player happened to be
|
||||
/// connected to a different replica. This service centralises the
|
||||
/// answer in Redis so all replicas agree.
|
||||
///
|
||||
/// **Two Redis keys per player**:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>notify:conns:{playerId}</c> — Set of active connection ids
|
||||
/// for that player on any replica. Connections add themselves on
|
||||
/// <see cref="Hubs.NotifyHub.OnConnectedAsync"/> and remove on
|
||||
/// <see cref="Hubs.NotifyHub.OnDisconnectedAsync"/>. The set has
|
||||
/// a 24h EXPIRE backstop so a replica that crashes mid-flight
|
||||
/// doesn't leak connection ids forever.</item>
|
||||
/// <item><c>notify:online</c> — Master set of player ids that have at
|
||||
/// least one active connection. Maintained explicitly: SADD when
|
||||
/// the conns set transitions empty→non-empty, SREM when it goes
|
||||
/// non-empty→empty. <see cref="OnlinePlayerIds"/> returns SMEMBERS
|
||||
/// in one round-trip.</item>
|
||||
/// </list>
|
||||
///
|
||||
/// Falls back to in-process state when no Redis is configured (local dev).
|
||||
/// Same semantics — just per-replica visibility.
|
||||
/// </summary>
|
||||
public class OnlinePresenceService
|
||||
{
|
||||
private readonly IConnectionMultiplexer? _redis;
|
||||
// Local fallback only. The outer dict maps playerId → connection-id set.
|
||||
private readonly ConcurrentDictionary<long, HashSet<string>> _localConns = new();
|
||||
|
||||
/// <summary>Aggressive backstop: if a replica vanishes ungracefully,
|
||||
/// the conns set ages out instead of leaking forever. Real disconnect
|
||||
/// events should remove specific connection ids well before this.</summary>
|
||||
public static TimeSpan ConnectionsKeyTtl { get; } = TimeSpan.FromHours(24);
|
||||
|
||||
public OnlinePresenceService(IConnectionMultiplexer? redis = null) => _redis = redis;
|
||||
|
||||
public async Task AddConnectionAsync(long playerId, string connectionId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
var db = mux.GetDatabase();
|
||||
var connsKey = ConnsKey(playerId);
|
||||
var added = await db.SetAddAsync(connsKey, connectionId);
|
||||
await db.KeyExpireAsync(connsKey, ConnectionsKeyTtl);
|
||||
// Only flip the master `online` set when this is the FIRST
|
||||
// connection for the player — saves a SADD on every retry
|
||||
// reconnect and keeps the online set tight.
|
||||
if (added)
|
||||
{
|
||||
var size = await db.SetLengthAsync(connsKey);
|
||||
if (size == 1) await db.SetAddAsync(OnlineSetKey, playerId);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
var set = _localConns.GetOrAdd(playerId, _ => new HashSet<string>());
|
||||
lock (set) set.Add(connectionId);
|
||||
}
|
||||
|
||||
public async Task RemoveConnectionAsync(long playerId, string connectionId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
var db = mux.GetDatabase();
|
||||
var connsKey = ConnsKey(playerId);
|
||||
var removed = await db.SetRemoveAsync(connsKey, connectionId);
|
||||
if (removed)
|
||||
{
|
||||
var remaining = await db.SetLengthAsync(connsKey);
|
||||
if (remaining == 0)
|
||||
{
|
||||
// Last connection for this player gone — drop them
|
||||
// from the online set so admin queries see "offline".
|
||||
await db.SetRemoveAsync(OnlineSetKey, playerId);
|
||||
await db.KeyDeleteAsync(connsKey);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_localConns.TryGetValue(playerId, out var set))
|
||||
{
|
||||
lock (set) set.Remove(connectionId);
|
||||
if (set.Count == 0) _localConns.TryRemove(playerId, out _);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of SignalR connection ids that currently belong
|
||||
/// to <paramref name="playerId"/>. Used by <see cref="DorkNet.Server.Hubs.NotifyHub.OnConnectedAsync"/>
|
||||
/// to detect a second-session join and kick the existing
|
||||
/// connection(s) via a targeted ModerationKick push. Returns an
|
||||
/// empty collection if nobody (or only the current caller) is
|
||||
/// connected — never null.</summary>
|
||||
public IReadOnlyCollection<string> GetConnections(long playerId)
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
try
|
||||
{
|
||||
var members = mux.GetDatabase().SetMembers(ConnsKey(playerId));
|
||||
if (members.Length == 0) return Array.Empty<string>();
|
||||
var result = new string[members.Length];
|
||||
for (int i = 0; i < members.Length; i++) result[i] = members[i].ToString();
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Redis sick → fall through to local fallback rather
|
||||
// than pretend there are no existing connections.
|
||||
}
|
||||
}
|
||||
if (!_localConns.TryGetValue(playerId, out var set)) return Array.Empty<string>();
|
||||
lock (set) return set.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of currently-online player ids across the
|
||||
/// cluster. Returns an empty array when nobody's online; never null.
|
||||
/// Callers typically <c>.ToHashSet()</c> the result for membership
|
||||
/// queries inside a tight loop.</summary>
|
||||
public IReadOnlyCollection<long> OnlinePlayerIds()
|
||||
{
|
||||
if (_redis is { } mux)
|
||||
{
|
||||
var members = mux.GetDatabase().SetMembers(OnlineSetKey);
|
||||
var result = new long[members.Length];
|
||||
for (int i = 0; i < members.Length; i++)
|
||||
result[i] = (long)members[i];
|
||||
return result;
|
||||
}
|
||||
return _localConns.Where(kv => kv.Value.Count > 0).Select(kv => kv.Key).ToArray();
|
||||
}
|
||||
|
||||
private const string OnlineSetKey = "notify:online";
|
||||
private static string ConnsKey(long playerId) => $"notify:conns:{playerId}";
|
||||
}
|
||||
Reference in New Issue
Block a user