Matchmade sessions respect room's subroom Max Player Count (#35)

* Avoid login kick broadcast during reconnect

* feat: Matchmade sessions respect room's subroom Max Player Count

GameSessionService previously hardcoded MaxCapacity=8 for new sessions,
fragmenting matchmade joiners 8-per-instance regardless of the host's
'Max Player Count in This Subroom' slider. New sessions now resolve the
room's entry subroom (lowest OrderIndex) RoomSceneEntity.MaxPlayers,
clamped 1-80, falling back to 8 for system rooms (dorm GUID roomIds).
This commit is contained in:
Alexa
2026-06-30 13:35:14 +03:00
committed by GitHub
parent 97f6522497
commit 193d9b343c
2 changed files with 37 additions and 13 deletions
@@ -20,7 +20,6 @@ namespace DorkNet.Server.Controllers.Match;
[ApiController] [ApiController]
public class MatchPlayerController( public class MatchPlayerController(
PlayerPresenceService presence, PlayerPresenceService presence,
NotificationService notifications,
RoomService rooms, RoomService rooms,
PrivateInstanceService privateInstances, PrivateInstanceService privateInstances,
IConfiguration config, IConfiguration config,
@@ -37,7 +36,7 @@ public class MatchPlayerController(
/// </summary> /// </summary>
[HttpPost("/player/login")] [HttpPost("/player/login")]
[Consumes("application/x-www-form-urlencoded", "multipart/form-data")] [Consumes("application/x-www-form-urlencoded", "multipart/form-data")]
public async Task<IActionResult> PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock) public IActionResult PlayerLogin([FromForm(Name = "LoginLock")] string? loginLock)
{ {
var playerId = TryGetCurrentPlayerId(); var playerId = TryGetCurrentPlayerId();
if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock)) if (playerId != 0 && !string.IsNullOrWhiteSpace(loginLock))
@@ -46,17 +45,15 @@ public class MatchPlayerController(
if (replaced) if (replaced)
{ {
log.LogWarning( log.LogWarning(
"[player-login] concurrent session for player={PlayerId}; kicking old session(s) via ModerationKick push", "[player-login] concurrent session marker replaced for player={PlayerId}; stale SignalR connections will be evicted on hub connect",
playerId); playerId);
// The new session hasn't opened its SignalR socket yet // Do not broadcast ModerationKick to the whole player group
// (negotiate fires AFTER /player/login on the watch's // here. During reconnects the active watch can already have a
// boot sequence — verified in the live trace), so the // SignalR connection in that group, and the 2020 client crashes
// ModerationKick push hits only the OLDER session's // hard if it receives a malformed/stale moderation payload.
// existing connections. They'll show the "you've been // NotifyHub.OnConnectedAsync snapshots existing connection ids
// kicked" dialog and gracefully disconnect via the // before adding the new connection and sends a targeted kick to
// watch's LocalModerationKickSelf path. // those ids only.
await notifications.KickStaleSession(playerId,
"Your account was signed in on another device.");
} }
} }
return Ok(Array.Empty<object>()); return Ok(Array.Empty<object>());
+28 -1
View File
@@ -64,7 +64,12 @@ public class GameSessionService(DorkNetDbContext db)
// in, so we save once with a placeholder, then patch and save // in, so we save once with a placeholder, then patch and save
// again. Two SaveChangesAsync calls is cheap at our QPS. // again. Two SaveChangesAsync calls is cheap at our QPS.
PhotonRoomName = "pending", PhotonRoomName = "pending",
MaxCapacity = 8, // Track the room's "Max Player Count in This Subroom" slider
// instead of a hardcoded 8, so matchmade joiners fill the room to
// the host's chosen limit rather than fragmenting into a new
// instance every 8 players. (Existing sessions keep whatever cap
// they were created with.)
MaxCapacity = await ResolveRoomMaxCapacityAsync(effectiveRoom),
PlayerCount = 1, PlayerCount = 1,
CreatedAt = DateTime.UtcNow, CreatedAt = DateTime.UtcNow,
}; };
@@ -75,6 +80,28 @@ public class GameSessionService(DorkNetDbContext db)
return ToDto(session); return ToDto(session);
} }
// Default session cap when we can't resolve a room-specific value (system
// rooms like the dorm use a GUID roomId that doesn't map to a RoomEntity).
private const int DefaultMaxCapacity = 8;
/// <summary>Resolve a new session's capacity from the room's entry
/// subroom (lowest <c>OrderIndex</c>) <see cref="RoomSceneEntity.MaxPlayers"/>
/// — the value the in-game "Max Player Count in This Subroom" slider
/// writes. Matchmade sessions land in the entry subroom, so its cap is the
/// right ceiling. GameSession roomIds are the room's numeric Id as a
/// string for user rooms; non-numeric (dorm GUID, etc.) → default. Clamped
/// to a sane Photon ceiling.</summary>
private async Task<int> ResolveRoomMaxCapacityAsync(string roomId)
{
if (!long.TryParse(roomId, out var roomLongId)) return DefaultMaxCapacity;
var max = await db.RoomScenes
.Where(s => s.RoomId == roomLongId)
.OrderBy(s => s.OrderIndex)
.Select(s => (int?)s.MaxPlayers)
.FirstOrDefaultAsync();
return max is int v && v > 0 ? Math.Clamp(v, 1, 80) : DefaultMaxCapacity;
}
public async Task<GameSession?> GetByIdAsync(long id) public async Task<GameSession?> GetByIdAsync(long id)
{ {
var s = await db.GameSessions.AsNoTracking() var s = await db.GameSessions.AsNoTracking()