using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Text.Json; using RadicadeServer.DB; using RadicadeServer.Extensions; using RadicadeServer.RecNet.Accounts.Local; namespace RadicadeServer.API.Config { [Route("api/settings")] [Route("playersettings")] public class PlayerSettingsAPI : Controller { private readonly RecDBContext db; public PlayerSettingsAPI(RecDBContext context) { db = context; } [HttpGet(""), Authorize] [HttpGet("v2"), Authorize] [HttpGet("playersettings"), Authorize] public List GetSettings() { var userId = User.GetUserId(); if (userId == null) return new List(); var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); var list = settings?.DeserializeSettings() ?? GetDefaultSettings(); ApplyDefaultSettings(list); var player = db.Players.AsNoTracking().FirstOrDefault(x => x.AccountId == userId.Value); if (player != null && string.IsNullOrWhiteSpace(player.Hashword)) { SetOrReplace(list, "Recroom.AccountCreation.HasCreatedPassword", "False"); SetOrReplace(list, "Recroom.AccountCreation.HasFinished", "False"); SetOrReplace(list, "Recroom.OOBE", "0"); } ApplyResponseMetadata(list, userId.Value); if (settings == null) { db.PlayerSettings.Add(new PlayerSettings { AccountId = userId.Value, Settings = list.ToJson() }); db.SaveChanges(); } return list; } private static void SetOrReplace(List list, string key, string value) { var existing = list.FirstOrDefault(x => string.Equals(x.Key, key, StringComparison.OrdinalIgnoreCase)); if (existing != null) existing.Value = value; else list.Add(new Setting { Key = key, Value = value }); } private static void ApplyDefaultSettings(List list) { bool needsOrientation = list.Any(setting => string.Equals(setting.Key, "Radicade.NeedsOrientation", StringComparison.OrdinalIgnoreCase) && (setting.Value == "1" || string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase))); foreach (var setting in GetDefaultSettings()) { if (needsOrientation && (string.Equals(setting.Key, "HAS_COMPLETED_ORIENTATION", StringComparison.OrdinalIgnoreCase) || string.Equals(setting.Key, "OrientationCompletionTime", StringComparison.OrdinalIgnoreCase))) continue; if (list.All(existing => !string.Equals(existing.Key, setting.Key, StringComparison.OrdinalIgnoreCase))) list.Add(setting); } SetOrReplace(list, "SplitTestAssignedSegments", "1|{}"); } private static void ApplyResponseMetadata(List list, long accountId) { for (var i = 0; i < list.Count; i++) { list[i].Id = i + 1; list[i].AccountId = accountId; } } public const int InviteLinkLifetimeInMinutes = 60; [HttpGet("partyinvite")] [HttpGet("~/chat/settings/partyinvite")] public IActionResult GetPartyInviteSettings() => Ok(new { InviteLinkLifetimeInMinutes }); [HttpPost("v2/set"), Authorize] [HttpPost(""), Authorize] [HttpPost("playersettings/set"), Authorize] public async Task SetSettingRequest() { if (User.GetUserId() == null) return Unauthorized(); var body = await ReadJsonBodyAsync(); var updates = body.HasValue ? ReadSettings(body.Value) : new List(); if (updates.Count != 1) return BadRequest("Expected one setting with Key and Value."); return await SetSetting(updates[0]); } [NonAction] public async Task SetSetting(Setting val) { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); if (val == null || string.IsNullOrWhiteSpace(val.Key) || val.Value == null) return BadRequest("Expected a setting with Key and Value."); var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); if (settings == null) { settings = new PlayerSettings { AccountId = userId.Value, Settings = DBConfig.DefaultSettings2023 }; await db.PlayerSettings.AddAsync(settings); } var actualSettings = settings.DeserializeSettings(); if (!await CanSaveOnboardingMarkerAsync(userId.Value, val, actualSettings, new[] { val })) return Ok(Array.Empty()); SetOrReplace(actualSettings, val.Key, val.Value); if (IsOnboardingFinishedMarker(val)) SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1"); settings.Settings = actualSettings.ToJson(); await db.SaveChangesAsync(); await RecordCodeOfConductAsync(userId.Value, new[] { val }, actualSettings); return Ok(Array.Empty()); } [HttpPut(""), Authorize] [HttpPut("v2"), Authorize] [HttpPut("playersettings"), Authorize] public async Task PutSettings() { var userId = User.GetUserId(); if (userId == null) return Unauthorized(); var body = await ReadJsonBodyAsync(); var updates = body.HasValue ? ReadSettings(body.Value) : new List(); if (updates.Count == 0) return BadRequest("No valid settings were supplied."); var settings = db.PlayerSettings.FirstOrDefault(x => x.AccountId == userId.Value); if (settings == null) { settings = new PlayerSettings { AccountId = userId.Value, Settings = DBConfig.DefaultSettings2023 }; await db.PlayerSettings.AddAsync(settings); } var actualSettings = settings.DeserializeSettings(); foreach (var update in updates) { if (string.IsNullOrWhiteSpace(update.Key) || update.Value == null || !await CanSaveOnboardingMarkerAsync(userId.Value, update, actualSettings, updates)) continue; SetOrReplace(actualSettings, update.Key, update.Value); if (IsOnboardingFinishedMarker(update)) SetOrReplace(actualSettings, "Radicade.NeedsOrientation", "1"); } settings.Settings = actualSettings.ToJson(); await db.SaveChangesAsync(); await RecordCodeOfConductAsync(userId.Value, updates, actualSettings); ApplyResponseMetadata(actualSettings, userId.Value); return Ok(actualSettings); } private async Task RecordCodeOfConductAsync( long accountId, IEnumerable incomingSettings, IEnumerable savedSettings) { bool accepted = incomingSettings.Concat(savedSettings).Any(setting => string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) && string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); if (!accepted) return; var player = await db.Players.FirstOrDefaultAsync(x => x.AccountId == accountId); if (player == null || player.CodeOfConductAccepted) return; player.CodeOfConductAccepted = true; await db.SaveChangesAsync(); } private static bool IsOnboardingFinishedMarker(Setting setting) => (string.Equals(setting.Key, "Recroom.OOBE", StringComparison.OrdinalIgnoreCase) && setting.Value == "100") || (string.Equals(setting.Key, "Recroom.AccountCreation.HasFinished", StringComparison.OrdinalIgnoreCase) && string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); private async Task CanSaveOnboardingMarkerAsync( long accountId, Setting update, IReadOnlyCollection savedSettings, IReadOnlyCollection incomingSettings) { bool marksPasswordCreated = string.Equals(update.Key, "Recroom.AccountCreation.HasCreatedPassword", StringComparison.OrdinalIgnoreCase) && string.Equals(update.Value, "true", StringComparison.OrdinalIgnoreCase); if (!marksPasswordCreated && !IsOnboardingFinishedMarker(update)) return true; var player = await db.Players.AsNoTracking().FirstOrDefaultAsync(x => x.AccountId == accountId); if (player == null || string.IsNullOrWhiteSpace(player.Hashword)) { return false; } if (marksPasswordCreated) return true; var allSettings = incomingSettings.Concat(savedSettings).ToList(); var profile = await db.Profiles.AsNoTracking().FirstOrDefaultAsync(x => x.accountId == accountId); bool hasBirthday = player.Birthday.HasValue && player.Birthday.Value.Year > 1900; bool hasChosenUsername = allSettings.Any(setting => string.Equals(setting.Key, "Recroom.AccountCreation.HasChosenUsername", StringComparison.OrdinalIgnoreCase) && string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); bool hasAcceptedCodeOfConduct = player.CodeOfConductAccepted || allSettings.Any(setting => string.Equals(setting.Key, "Recroom.AccountCreation.AcceptedCodeOfConduct", StringComparison.OrdinalIgnoreCase) && string.Equals(setting.Value, "true", StringComparison.OrdinalIgnoreCase)); if (profile == null || string.IsNullOrWhiteSpace(profile.username)) { return false; } if (!hasChosenUsername) { return false; } if (!hasAcceptedCodeOfConduct) { return false; } return true; } private static List GetDefaultSettings() { try { return DBConfig.DefaultSettings2023.FromJson>() ?? new List(); } catch (Exception) { return new List(); } } private async Task ReadJsonBodyAsync() { if (Request.Body == null) return null; if (Request.ContentLength == 0) return null; try { if (Request.HasFormContentType) { var form = await Request.ReadFormAsync(); return JsonSerializer.SerializeToElement(form.ToDictionary(pair => pair.Key, pair => pair.Value.ToString())); } using var doc = await JsonDocument.ParseAsync(Request.Body); return doc.RootElement.Clone(); } catch (JsonException) { return null; } } private static List ReadSettings(JsonElement body) { if (body.ValueKind == JsonValueKind.Array) return ReadSettingsArray(body); if (body.ValueKind != JsonValueKind.Object) return new List(); foreach (var key in new[] { "settings", "Settings", "playerSettings", "PlayerSettings" }) { if (body.TryGetProperty(key, out var nested) && nested.ValueKind == JsonValueKind.Array) return ReadSettingsArray(nested); } var setting = ReadSettingObject(body); return setting == null ? new List() : new List { setting }; } private static List ReadSettingsArray(JsonElement array) { var settings = new List(); foreach (var item in array.EnumerateArray()) { var setting = ReadSettingObject(item); if (setting != null) settings.Add(setting); } return settings; } private static Setting? ReadSettingObject(JsonElement item) { if (item.ValueKind != JsonValueKind.Object) return null; var key = ReadString(item, "Key", "key", "Name", "name"); var value = ReadString(item, "Value", "value"); if (string.IsNullOrWhiteSpace(key) || value == null) return null; return new Setting { Key = key, Value = value }; } private static string? ReadString(JsonElement item, params string[] names) { foreach (var name in names) { if (!item.TryGetProperty(name, out var value)) continue; return value.ValueKind switch { JsonValueKind.String => value.GetString(), JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => value.ToString(), _ => null, }; } return null; } } }