mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-26 07:28:45 -07:00
4edf25b2d5
Targets Rec Room 2020.12.18 client.
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 the dorknet-clientpatch-{key}.zip
Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
on v1-december-* tag push.
Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.
111 lines
4.1 KiB
C#
111 lines
4.1 KiB
C#
using DorkNet.Models.GameSessions;
|
|
using DorkNet.Server.Data;
|
|
using DorkNet.Server.Data.Entities;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
namespace DorkNet.Server.Services;
|
|
|
|
/// <summary>
|
|
/// Cross-replica matchmaking session registry. Each session is a Photon
|
|
/// match with a player count; <see cref="JoinOrCreateAsync"/> finds an
|
|
/// existing non-full session in the requested room+region or creates
|
|
/// a new one.
|
|
///
|
|
/// Pre-PR-3 used a process-local <c>ConcurrentDictionary</c> + atomic
|
|
/// counter. With horizontal scale that was unsafe two ways:
|
|
/// <list type="bullet">
|
|
/// <item>Each replica's counter started at 1, so two replicas would
|
|
/// hand out colliding session ids — clients on different replicas
|
|
/// could end up with the same SessionId pointing at different
|
|
/// Photon rooms.</item>
|
|
/// <item>"Find a non-full session" only saw replica-local sessions, so
|
|
/// two players matchmaking via different replicas got their own
|
|
/// fresh session each and were isolated.</item>
|
|
/// </list>
|
|
///
|
|
/// Both are fixed by moving to Postgres: bigserial allocates from a
|
|
/// shared sequence; the SELECT sees every replica's sessions.
|
|
/// Service is now <b>scoped</b> because it holds a DbContext.
|
|
/// </summary>
|
|
public class GameSessionService(DorkNetDbContext db)
|
|
{
|
|
// Dorm room is always available
|
|
private const string DormRoomId = "00000000-0000-0000-0000-000000000001";
|
|
private const string DormActivityId = "76d98498-60a1-430c-ab76-b54a29b7a163";
|
|
|
|
public async Task<GameSession> JoinOrCreateAsync(string? roomId, string? activityId, string region)
|
|
{
|
|
var effectiveRoom = roomId ?? DormRoomId;
|
|
|
|
// Find an existing non-full session for this room+region. ORDER BY
|
|
// PlayerCount DESC + LIMIT 1 packs joiners into the fullest matching
|
|
// session first, so we don't fragment players across half-empty
|
|
// rooms when the catalog is light.
|
|
var match = await db.GameSessions
|
|
.Where(s => s.RoomId == effectiveRoom
|
|
&& s.Region == region
|
|
&& s.PlayerCount < s.MaxCapacity)
|
|
.OrderByDescending(s => s.PlayerCount)
|
|
.FirstOrDefaultAsync();
|
|
|
|
if (match is not null)
|
|
{
|
|
match.PlayerCount += 1;
|
|
await db.SaveChangesAsync();
|
|
return ToDto(match);
|
|
}
|
|
|
|
var session = new GameSessionEntity
|
|
{
|
|
RoomId = effectiveRoom,
|
|
ActivityLevelId = activityId ?? DormActivityId,
|
|
Region = region,
|
|
// PhotonRoomName needs the eventual server-allocated Id baked
|
|
// in, so we save once with a placeholder, then patch and save
|
|
// again. Two SaveChangesAsync calls is cheap at our QPS.
|
|
PhotonRoomName = "pending",
|
|
MaxCapacity = 8,
|
|
PlayerCount = 1,
|
|
CreatedAt = DateTime.UtcNow,
|
|
};
|
|
db.GameSessions.Add(session);
|
|
await db.SaveChangesAsync();
|
|
session.PhotonRoomName = $"recroom_{session.Id}_{Guid.NewGuid():N}";
|
|
await db.SaveChangesAsync();
|
|
return ToDto(session);
|
|
}
|
|
|
|
public async Task<GameSession?> GetByIdAsync(long id)
|
|
{
|
|
var s = await db.GameSessions.AsNoTracking()
|
|
.FirstOrDefaultAsync(s => s.Id == id);
|
|
return s is null ? null : ToDto(s);
|
|
}
|
|
|
|
public async Task PlayerLeftAsync(long sessionId)
|
|
{
|
|
var s = await db.GameSessions.FirstOrDefaultAsync(s => s.Id == sessionId);
|
|
if (s is null) return;
|
|
s.PlayerCount = Math.Max(0, s.PlayerCount - 1);
|
|
if (s.PlayerCount == 0)
|
|
{
|
|
db.GameSessions.Remove(s);
|
|
}
|
|
await db.SaveChangesAsync();
|
|
}
|
|
|
|
private static GameSession ToDto(GameSessionEntity s) => new()
|
|
{
|
|
GameSessionId = s.Id,
|
|
RoomId = s.RoomId,
|
|
ActivityLevelId = s.ActivityLevelId,
|
|
RegionId = s.Region,
|
|
PhotonRoomName = s.PhotonRoomName,
|
|
Private = false,
|
|
MaxCapacity = s.MaxCapacity,
|
|
PlayerCount = s.PlayerCount,
|
|
IsFull = s.PlayerCount >= s.MaxCapacity,
|
|
GameInProgress = s.PlayerCount > 0,
|
|
};
|
|
}
|