Match/SeatDirectory.cs

The SeatDirectory class manages the binding between authenticated accounts and active engine connections, acting as a session directory for users.

using System;
using System.Collections.Generic;
using System.Linq;

namespace CitySim.Match;

internal sealed record SeatAccount(ulong Account,Guid Player);

/// <summary>Host-only account-to-seat ownership. Account identity must come from the engine, never an RPC argument.</summary>
internal sealed class SeatDirectory
{
	readonly Dictionary<ulong,Guid> accounts=new();
	readonly Dictionary<Guid,Guid> connections=new();
	public Guid Player(Guid connection) => connections.GetValueOrDefault(connection);
	public Guid AccountPlayer(ulong account) => accounts.GetValueOrDefault(account);
	public Guid Bind(Guid connection,ulong authenticatedAccount)
	{
		if (connection==Guid.Empty || authenticatedAccount==0) return Guid.Empty;
		if (connections.TryGetValue(connection,out var bound) && AccountPlayer(authenticatedAccount)!=bound) return Guid.Empty;
		if (!accounts.ContainsKey(authenticatedAccount) && accounts.ContainsValue(connection)) return Guid.Empty;
		if (!accounts.TryGetValue(authenticatedAccount,out var player)) accounts[authenticatedAccount]=player=connection;
		// A new connection from the same authenticated account replaces its old session.
		foreach (var old in connections.Where(p=>p.Value==player && p.Key!=connection).Select(p=>p.Key).ToArray()) connections.Remove(old);
		connections[connection]=player; return player;
	}
	public Guid Unbind(Guid connection)
	{
		var player=Player(connection); connections.Remove(connection); return player;
	}
	public SeatAccount[] Capture() => accounts.Select(p=>new SeatAccount(p.Key,p.Value)).ToArray();
	public void Restore(IEnumerable<SeatAccount> saved)
	{
		accounts.Clear(); connections.Clear();
		foreach (var seat in saved)
		{
			if (seat.Account==0 || seat.Player==Guid.Empty || accounts.ContainsKey(seat.Account) || accounts.ContainsValue(seat.Player)) throw new ArgumentException("Invalid seat ownership checkpoint.");
			accounts.Add(seat.Account,seat.Player);
		}
	}
}