using System.Collections.Concurrent; using Newtonsoft.Json; namespace Campus_live_rewrite.api.server { /// /// The NORMAL PLAYER whitelist: every Steam id that has been through the Campus launcher. /// /// The launcher proves a player's Steam account with a ticket from Steam itself /// (LauncherController /api/launcher/v1/steam, SteamTicket checks Steam's signature), and that /// Steam id is added here automatically. With Required on, a PC (Steam) game login is let in /// only when its - Steam-verified - id is on this list or on the hand-made allowlist /// (SteamAllowlist, Data/steam_allowlist.txt: staff / special entries). So playing on PC means /// running Campus through our launcher at least once. Quest/Meta is never affected. /// /// Stored in Data/steam_launcher_players.json; managed from the admin panel (Settings). /// public static class LauncherPlayers { public class Entry { public string SteamId { get; set; } = ""; public ulong AccountId { get; set; } public string Username { get; set; } = ""; public DateTime AddedAt { get; set; } public DateTime LastSeen { get; set; } } private class FileData { public bool Required { get; set; } = true; public List Players { get; set; } = new(); } private static string Path_ => Path.Combine(Environment.CurrentDirectory, "Data", "steam_launcher_players.json"); private static readonly object _lock = new(); private static FileData? _data; private static ConcurrentDictionary _byId = new(); private static FileData Data() { if (_data != null) return _data; lock (_lock) { if (_data != null) return _data; FileData d; try { d = File.Exists(Path_) ? JsonConvert.DeserializeObject(File.ReadAllText(Path_)) ?? new FileData() : new FileData(); } catch (Exception ex) { // Unreadable: keep the gate on (Required defaults true) but start empty - the // players re-add themselves on their next launcher start. Not saved over the // broken file, so it can still be recovered by hand. Console.WriteLine($"[launcher-players] could not read {Path_}: {ex.Message}"); d = new FileData(); } var map = new ConcurrentDictionary(); foreach (var e in d.Players) if (ulong.TryParse(e.SteamId, out ulong id) && id != 0) map[id] = e; _byId = map; _data = d; Console.WriteLine($"[launcher-players] {map.Count} Steam id(s) whitelisted through the launcher; " + $"launcher required for Steam logins: {(d.Required ? "YES" : "no")}"); return d; } } private static void Save() { try { var d = Data(); d.Players = _byId.Values.OrderBy(e => e.AddedAt).ToList(); Directory.CreateDirectory(Path.GetDirectoryName(Path_)!); string tmp = Path_ + ".tmp"; File.WriteAllText(tmp, JsonConvert.SerializeObject(d, Formatting.Indented)); File.Move(tmp, Path_, true); } catch (Exception ex) { Console.WriteLine($"[launcher-players] could not write {Path_}: {ex.Message}"); } } /// Must a Steam login come through the launcher (this list or the hand-made allowlist)? public static bool Required => !ConfigCampus.LocalDev && Data().Required; public static void SetRequired(bool on) { lock (_lock) { Data().Required = on; Save(); } Console.WriteLine($"[launcher-players] launcher required for Steam logins: {(on ? "YES" : "no")}"); } public static bool Contains(ulong steamId) { Data(); return steamId != 0 && _byId.ContainsKey(steamId); } /// Add (or refresh) a Steam id the launcher just proved with a Steam ticket. public static bool Add(ulong steamId, ulong accountId, string username) { if (steamId == 0) return false; Data(); lock (_lock) { bool added = !_byId.TryGetValue(steamId, out var e); e ??= new Entry { SteamId = steamId.ToString(), AddedAt = DateTime.UtcNow }; e.AccountId = accountId; e.Username = username ?? ""; e.LastSeen = DateTime.UtcNow; _byId[steamId] = e; Save(); if (added) Console.WriteLine($"[launcher-players] whitelisted Steam {steamId} (@{username}, {accountId}) - {_byId.Count} total"); return added; } } public static bool Remove(ulong steamId) { Data(); lock (_lock) { if (!_byId.TryRemove(steamId, out _)) return false; Save(); } Console.WriteLine($"[launcher-players] removed Steam {steamId} - {_byId.Count} left"); return true; } public static List All() { Data(); return _byId.Values.OrderByDescending(e => e.AddedAt).ToList(); } /// The whitelisted Steam ids an account added through the launcher (the launcher's /me). public static List ForAccount(ulong accountId) { Data(); return _byId.Values.Where(e => e.AccountId == accountId).Select(e => e.SteamId).ToList(); } } /// /// The one question the login asks about a Steam id: may it play? With the launcher required, /// only ids on the normal-player whitelist (LauncherPlayers) or the hand-made allowlist /// (SteamAllowlist) - an empty hand-made list does NOT open the gate then. Without it, the old /// allowlist rule (empty = everyone). /// public static class SteamGate { public static bool Active => LauncherPlayers.Required || SteamAllowlist.IsActive; public static bool Allowed(ulong steamId) => LauncherPlayers.Required ? steamId != 0 && (SteamAllowlist.All().Contains(steamId) || LauncherPlayers.Contains(steamId)) : SteamAllowlist.IsAllowed(steamId); } }