Refactor room data blob handling and permissions

- Updated the RoomsController to utilize synthetic CDN keys for data blobs.
- Introduced methods for determining current or synthetic data blob names.
- Adjusted room role permissions to align with new game role definitions.
- Enhanced the SanitizeController with a new endpoint for purity checks.
- Modified Match and PlayerPresence services to handle deferred presence commits.
- Improved RoomDataBlobService to support new game role and tool tag settings.
- Added support for boot dorm grace period in PlayerPresenceService.
- Updated protobuf definitions to reflect new room role and game role structures.

You are now able to go into dorm and the rec center!
This commit is contained in:
Alexa
2026-06-24 12:02:13 +03:00
parent 5274da03b4
commit 323c17bedb
11 changed files with 733 additions and 81 deletions
@@ -47,6 +47,7 @@ public class PlayerPresenceService
private readonly ConcurrentDictionary<long, RoomInstanceDto> _local = new();
private readonly ConcurrentDictionary<long, DateTimeOffset> _localRoomExpires = new();
private readonly ConcurrentDictionary<long, DateTimeOffset> _localActiveExpires = new();
private readonly ConcurrentDictionary<long, DateTimeOffset> _localBootDormGraceExpires = new();
private readonly IConnectionMultiplexer? _redis;
private readonly ILogger<PlayerPresenceService>? _log;
@@ -68,6 +69,15 @@ public class PlayerPresenceService
/// </summary>
public static TimeSpan ActivityTtl { get; } = TimeSpan.FromSeconds(90);
/// <summary>
/// Right after login the 2023 watch may boot into its cached offline
/// DormRoom before the live Photon join finishes. During that window
/// a null roomInstance is safer than returning stale/live presence:
/// the client treats the offline placeholder as invalid and accepts
/// null, but a live private dorm forces a resync.
/// </summary>
public static TimeSpan BootDormGraceTtl { get; } = TimeSpan.FromSeconds(45);
private static readonly JsonSerializerOptions JsonOpts = new()
{
// Match the wire format used by the matchmaking response so
@@ -150,6 +160,63 @@ public class PlayerPresenceService
_localActiveExpires[playerId] = DateTimeOffset.UtcNow.Add(ActivityTtl);
}
public void BeginBootDormGrace(long playerId)
{
if (_redis is { } mux)
{
try
{
mux.GetDatabase().StringSet(BootDormGraceKey(playerId), "1", BootDormGraceTtl);
}
catch (Exception ex)
{
_log?.LogWarning(ex, "[presence] redis boot grace SET failed for player={PlayerId}", playerId);
}
return;
}
_localBootDormGraceExpires[playerId] = DateTimeOffset.UtcNow.Add(BootDormGraceTtl);
}
public bool IsBootDormGraceActive(long playerId)
{
if (_redis is { } mux)
{
try
{
return mux.GetDatabase().KeyExists(BootDormGraceKey(playerId));
}
catch (Exception ex)
{
_log?.LogWarning(ex, "[presence] redis boot grace GET failed for player={PlayerId}", playerId);
return false;
}
}
if (!_localBootDormGraceExpires.TryGetValue(playerId, out var expires)) return false;
if (expires > DateTimeOffset.UtcNow) return true;
_localBootDormGraceExpires.TryRemove(playerId, out _);
return false;
}
public void EndBootDormGrace(long playerId)
{
if (_redis is { } mux)
{
try
{
mux.GetDatabase().KeyDelete(BootDormGraceKey(playerId));
}
catch (Exception ex)
{
_log?.LogWarning(ex, "[presence] redis boot grace DEL failed for player={PlayerId}", playerId);
}
return;
}
_localBootDormGraceExpires.TryRemove(playerId, out _);
}
public IReadOnlyCollection<long> RecentlyActivePlayerIds()
{
if (_redis is { } mux)
@@ -265,6 +332,7 @@ public class PlayerPresenceService
var db = mux.GetDatabase();
db.KeyDelete(Key(playerId));
db.KeyDelete(ActiveKey(playerId));
db.KeyDelete(BootDormGraceKey(playerId));
db.SetRemove(ActiveSetKey, playerId);
}
else
@@ -272,6 +340,7 @@ public class PlayerPresenceService
_local.TryRemove(playerId, out _);
_localRoomExpires.TryRemove(playerId, out _);
_localActiveExpires.TryRemove(playerId, out _);
_localBootDormGraceExpires.TryRemove(playerId, out _);
}
}
@@ -316,6 +385,7 @@ public class PlayerPresenceService
private static string Key(long playerId) => $"presence:{playerId}";
private static string ActiveKey(long playerId) => $"presence-active:{playerId}";
private static string BootDormGraceKey(long playerId) => $"presence-boot-dorm-grace:{playerId}";
private const string ActiveSetKey = "presence-active";
// ── LoginLock single-session enforcement ─────────────────────────