From 4abdcf7a733a1331bd69a6760592166489c48bc2 Mon Sep 17 00:00:00 2001 From: pathetic meowmeow Date: Mon, 9 Feb 2026 15:30:20 -0500 Subject: [PATCH] Ensure profile loading only returns valid species (#42842) * Ensure profile loading only returns valid species * punt conversion logic outside of the database --- .../Tests/Preferences/ServerDbSqliteTests.cs | 57 +++++- Content.Server/Database/ServerDbBase.cs | 182 +++--------------- Content.Server/Database/ServerDbManager.cs | 22 ++- Content.Server/Database/ServerDbPostgres.cs | 4 +- Content.Server/Database/ServerDbSqlite.cs | 4 +- .../Managers/ServerPreferencesManager.cs | 145 +++++++++++++- 6 files changed, 241 insertions(+), 173 deletions(-) diff --git a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs index 9e90919ef41..9d237ef7f3c 100644 --- a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs +++ b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; using System.Linq; +using System.Threading; using Content.Server.Database; +using Content.Server.Preferences.Managers; using Content.Shared.GameTicking; using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; using Content.Shared.Preferences.Loadouts; using Content.Shared.Preferences.Loadouts.Effects; @@ -14,6 +17,7 @@ using Robust.Shared.Enums; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Network; +using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; using Robust.UnitTesting; @@ -58,13 +62,12 @@ namespace Content.IntegrationTests.Tests.Preferences { var cfg = server.ResolveDependency(); var serialization = server.ResolveDependency(); - var task = server.ResolveDependency(); var opsLog = server.ResolveDependency().GetSawmill("db.ops"); var builder = new DbContextOptionsBuilder(); var conn = new SqliteConnection("Data Source=:memory:"); conn.Open(); builder.UseSqlite(conn); - return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog, task, serialization); + return new ServerDbSqlite(() => builder.Options, true, cfg, true, opsLog, serialization); } [Test] @@ -83,12 +86,14 @@ namespace Content.IntegrationTests.Tests.Preferences { var pair = await PoolManager.GetServerClient(); var db = GetDb(pair.Server); + var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); const int slot = 0; var originalProfile = CharlieCharlieson(); await db.InitPrefsAsync(username, originalProfile); var prefs = await db.GetPlayerPreferencesAsync(username); - Assert.That(prefs.Characters.Single(p => p.Key == slot).Value.MemberwiseEquals(originalProfile)); + var profile = preferences.ConvertProfiles(prefs!.Profiles.Find(p => p.Slot == slot)); + Assert.That(profile.MemberwiseEquals(originalProfile)); await pair.CleanReturnAsync(); } @@ -104,7 +109,7 @@ namespace Content.IntegrationTests.Tests.Preferences await db.SaveSelectedCharacterIndexAsync(username, 1); await db.SaveCharacterSlotAsync(username, null, 1); var prefs = await db.GetPlayerPreferencesAsync(username); - Assert.That(!prefs.Characters.Any(p => p.Key != 0)); + Assert.That(prefs!.Profiles, Has.Count.EqualTo(1)); await pair.CleanReturnAsync(); } @@ -123,5 +128,49 @@ namespace Content.IntegrationTests.Tests.Preferences { return new(Guid.NewGuid()); } + + private const string InvalidSpecies = "WingusDingus"; + + private static bool[] _trueFalse = [true, false]; + + [Test] + [TestCaseSource(nameof(_trueFalse))] + public async Task InvalidSpeciesConversion(bool legacy) + { + var pair = await PoolManager.GetServerClient(); + var server = pair.Server; + var db = GetDb(pair.Server); + var preferences = (ServerPreferencesManager)pair.Server.ResolveDependency(); + + var proto = server.ResolveDependency(); + Assert.That(!proto.HasIndex(InvalidSpecies), "You should not have added a species called WingusDingus, but change it in this test to something else I guess"); + + var bogus = new HumanoidCharacterProfile() + { + Species = InvalidSpecies, + }; + + var username = new NetUserId(new Guid("640bd619-fc8d-4fe2-bf3c-4a5fb17d6ddd")); + await db.InitPrefsAsync(username, new HumanoidCharacterProfile()); + await db.SaveCharacterSlotAsync(username, bogus, 0); + await db.SaveSelectedCharacterIndexAsync(username, 0); + + if (legacy) + await db.MakeCharacterSlotLegacyAsync(username, 0); + + var prefs = await db.GetPlayerPreferencesAsync(username, CancellationToken.None); + + Assert.That(prefs, Is.Not.Null); + await server.WaitAssertion(() => + { + var converted = preferences.ConvertPreferences(prefs); + + Assert.That(converted.Characters, Has.Count.EqualTo(1)); + Assert.That(converted.Characters[0].Species, Is.Not.EqualTo(InvalidSpecies)); + Assert.That(converted.Characters[0].Species, Is.EqualTo(HumanoidCharacterProfile.DefaultSpecies)); + }); + + await pair.CleanReturnAsync(); + } } } diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index eee17bdef87..f86c29e7d2a 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -9,18 +9,12 @@ using System.Threading; using System.Threading.Tasks; using Content.Server.Administration.Logs; using Content.Shared.Administration.Logs; -using Content.Shared.Body; using Content.Shared.Construction.Prototypes; using Content.Shared.Database; using Content.Shared.Humanoid; -using Content.Shared.Humanoid.Markings; using Content.Shared.Preferences; -using Content.Shared.Preferences.Loadouts; using Content.Shared.Roles; -using Content.Shared.Traits; using Microsoft.EntityFrameworkCore; -using Robust.Shared.Asynchronous; -using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Serialization.Manager; @@ -32,25 +26,23 @@ namespace Content.Server.Database { private readonly ISawmill _opsLog; public event Action? OnNotificationReceived; - private readonly ITaskManager _task; private readonly ISerializationManager _serialization; /// Sawmill to trace log database operations to. - public ServerDbBase(ISawmill opsLog, ITaskManager taskManager, ISerializationManager serialization) + public ServerDbBase(ISawmill opsLog, ISerializationManager serialization) { - _task = taskManager; _serialization = serialization; _opsLog = opsLog; } #region Preferences - public async Task GetPlayerPreferencesAsync( + public async Task GetPlayerPreferencesAsync( NetUserId userId, CancellationToken cancel = default) { await using var db = await GetDb(cancel); - var prefs = await db.DbContext + return await db.DbContext .Preference .Include(p => p.Profiles).ThenInclude(h => h.Jobs) .Include(p => p.Profiles).ThenInclude(h => h.Antags) @@ -61,22 +53,6 @@ namespace Content.Server.Database .ThenInclude(group => group.Loadouts) .AsSplitQuery() .SingleOrDefaultAsync(p => p.UserId == userId.UserId, cancel); - - if (prefs is null) - return null; - - var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1; - var profiles = new Dictionary(maxSlot); - foreach (var profile in prefs.Profiles) - { - profiles[profile.Slot] = await ConvertProfiles(profile); - } - - var constructionFavorites = new List>(prefs.ConstructionFavorites.Count); - foreach (var favorite in prefs.ConstructionFavorites) - constructionFavorites.Add(new ProtoId(favorite)); - - return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor), constructionFavorites); } public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index) @@ -88,6 +64,30 @@ namespace Content.Server.Database await db.DbContext.SaveChangesAsync(); } + /// + /// Only intended for use in unit tests - drops the organ marking data from a profile in the given slot + /// + /// The user whose profile to modify + /// The slot index to modify + public async Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot) + { + await using var db = await GetDb(); + + var oldProfile = await db.DbContext.Profile + .Include(p => p.Preference) + .Where(p => p.Preference.UserId == userId.UserId) + .AsSplitQuery() + .SingleOrDefaultAsync(h => h.Slot == slot); + + if (oldProfile == null) + return; + + oldProfile.OrganMarkings = null; + oldProfile.Markings = JsonSerializer.SerializeToDocument(new List()); + + await db.DbContext.SaveChangesAsync(); + } + public async Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? humanoid, int slot) { await using var db = await GetDb(); @@ -139,7 +139,7 @@ namespace Content.Server.Database db.Profile.Remove(profile); } - public async Task InitPrefsAsync(NetUserId userId, HumanoidCharacterProfile defaultProfile) + public async Task InitPrefsAsync(NetUserId userId, HumanoidCharacterProfile defaultProfile) { await using var db = await GetDb(); @@ -158,7 +158,7 @@ namespace Content.Server.Database await db.DbContext.SaveChangesAsync(); - return new PlayerPreferences(new[] { new KeyValuePair(0, defaultProfile) }, 0, Color.FromHex(prefs.AdminOOCColor), []); + return prefs; } public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot) @@ -203,130 +203,6 @@ namespace Content.Server.Database prefs.SelectedCharacterSlot = newSlot; } - private static TValue? TryDeserialize(JsonDocument document) where TValue : class - { - try - { - return document.Deserialize(); - } - catch (JsonException) - { - return null; - } - } - - private async Task ConvertProfiles(Profile profile) - { - - var jobs = profile.Jobs.ToDictionary(j => new ProtoId(j.JobName), j => (JobPriority) j.Priority); - var antags = profile.Antags.Select(a => new ProtoId(a.AntagName)); - var traits = profile.Traits.Select(t => new ProtoId(t.TraitName)); - - var sex = Sex.Male; - if (Enum.TryParse(profile.Sex, true, out var sexVal)) - sex = sexVal; - - var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority; - - var gender = sex == Sex.Male ? Gender.Male : Gender.Female; - if (Enum.TryParse(profile.Gender, true, out var genderVal)) - gender = genderVal; - - - var markings = - new Dictionary, Dictionary>>(); - - if (profile.OrganMarkings?.RootElement is { } element) - { - var data = element.ToDataNode(); - markings = _serialization - .Read, Dictionary>>>( - data, - notNullableOverride: true); - } - else if (profile.Markings is { } profileMarkings && TryDeserialize>(profileMarkings) is { } markingsRaw) - { - List markingsList = new(); - - foreach (var marking in markingsRaw) - { - var parsed = Marking.ParseFromDbString(marking); - - if (parsed is null) continue; - - markingsList.Add(parsed); - } - - if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } facialMarking) - markingsList.Add(facialMarking); - - if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } hairMarking) - markingsList.Add(hairMarking); - - var completion = new TaskCompletionSource(); - _task.RunOnMainThread(() => - { - var markingManager = IoCManager.Resolve(); - - try - { - markings = markingManager.ConvertMarkings(markingsList, profile.Species); - completion.SetResult(); - } - catch (Exception ex) - { - completion.TrySetException(ex); - } - }); - await completion.Task; - } - - var loadouts = new Dictionary(); - - foreach (var role in profile.Loadouts) - { - var loadout = new RoleLoadout(role.RoleName) - { - EntityName = role.EntityName, - }; - - foreach (var group in role.Groups) - { - var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName); - foreach (var profLoadout in group.Loadouts) - { - groupLoadouts.Add(new Loadout() - { - Prototype = profLoadout.LoadoutName, - }); - } - } - - loadouts[role.RoleName] = loadout; - } - - return new HumanoidCharacterProfile( - profile.CharacterName, - profile.FlavorText, - profile.Species, - profile.Age, - sex, - gender, - new HumanoidCharacterAppearance - ( - Color.FromHex(profile.EyeColor), - Color.FromHex(profile.SkinColor), - markings - ), - spawnPriority, - jobs, - (PreferenceUnavailableMode) profile.PreferenceUnavailable, - antags.ToHashSet(), - traits.ToHashSet(), - loadouts - ); - } - private Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int slot, Profile? profile = null) { profile ??= new Profile(); diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs index 44479b133c0..96a83ce630d 100644 --- a/Content.Server/Database/ServerDbManager.cs +++ b/Content.Server/Database/ServerDbManager.cs @@ -16,7 +16,6 @@ using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Npgsql; using Prometheus; -using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.ContentPack; using Robust.Shared.Network; @@ -36,13 +35,15 @@ namespace Content.Server.Database Task HasPendingModelChanges(); #region Preferences - Task InitPrefsAsync( + Task InitPrefsAsync( NetUserId userId, HumanoidCharacterProfile defaultProfile, CancellationToken cancel); Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index); + Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot); + Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? profile, int slot); Task SaveAdminOOCColorAsync(NetUserId userId, Color color); @@ -51,7 +52,7 @@ namespace Content.Server.Database // Single method for two operations for transaction. Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); - Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel); + Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel); #endregion #region User Ids @@ -372,7 +373,6 @@ namespace Content.Server.Database [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IResourceManager _res = default!; [Dependency] private readonly ILogManager _logMgr = default!; - [Dependency] private readonly ITaskManager _task = default!; [Dependency] private readonly ISerializationManager _serialization = default!; private ServerDbBase _db = default!; @@ -405,11 +405,11 @@ namespace Content.Server.Database { case "sqlite": SetupSqlite(out var contextFunc, out var inMemory); - _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog, _task, _serialization); + _db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog, _serialization); break; case "postgres": var (pgOptions, conString) = CreatePostgresOptions(); - _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog, _task, _serialization); + _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog, _serialization); break; default: throw new InvalidDataException($"Unknown database engine {engine}."); @@ -426,7 +426,7 @@ namespace Content.Server.Database _db.Shutdown(); } - public Task InitPrefsAsync( + public Task InitPrefsAsync( NetUserId userId, HumanoidCharacterProfile defaultProfile, CancellationToken cancel) @@ -441,6 +441,12 @@ namespace Content.Server.Database return RunDbCommand(() => _db.SaveSelectedCharacterIndexAsync(userId, index)); } + public Task MakeCharacterSlotLegacyAsync(NetUserId userId, int slot) + { + DbWriteOpsMetric.Inc(); + return RunDbCommand(() => _db.MakeCharacterSlotLegacyAsync(userId, slot)); + } + public Task SaveCharacterSlotAsync(NetUserId userId, HumanoidCharacterProfile? profile, int slot) { DbWriteOpsMetric.Inc(); @@ -465,7 +471,7 @@ namespace Content.Server.Database return RunDbCommand(() => _db.SaveConstructionFavoritesAsync(userId, constructionFavorites)); } - public Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel) + public Task GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel) { DbReadOpsMetric.Inc(); return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel)); diff --git a/Content.Server/Database/ServerDbPostgres.cs b/Content.Server/Database/ServerDbPostgres.cs index f0c56b72951..bea75fe941b 100644 --- a/Content.Server/Database/ServerDbPostgres.cs +++ b/Content.Server/Database/ServerDbPostgres.cs @@ -11,7 +11,6 @@ using Content.Server.IP; using Content.Shared.CCVar; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; -using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Serialization.Manager; @@ -33,9 +32,8 @@ namespace Content.Server.Database IConfigurationManager cfg, ISawmill opsLog, ISawmill notifyLog, - ITaskManager taskManager, ISerializationManager serialization) - : base(opsLog, taskManager, serialization) + : base(opsLog, serialization) { var concurrency = cfg.GetCVar(CCVars.DatabasePgConcurrency); diff --git a/Content.Server/Database/ServerDbSqlite.cs b/Content.Server/Database/ServerDbSqlite.cs index 6bb1bea45fb..97e265dbff0 100644 --- a/Content.Server/Database/ServerDbSqlite.cs +++ b/Content.Server/Database/ServerDbSqlite.cs @@ -11,7 +11,6 @@ using Content.Server.Preferences.Managers; using Content.Shared.CCVar; using Content.Shared.Database; using Microsoft.EntityFrameworkCore; -using Robust.Shared.Asynchronous; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Serialization.Manager; @@ -39,9 +38,8 @@ namespace Content.Server.Database IConfigurationManager cfg, bool synchronous, ISawmill opsLog, - ITaskManager taskManager, ISerializationManager serialization) - : base(opsLog, taskManager, serialization) + : base(opsLog, serialization) { _options = options; diff --git a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs index 4cde984254f..228b820e25a 100644 --- a/Content.Server/Preferences/Managers/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/Managers/ServerPreferencesManager.cs @@ -1,16 +1,26 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Content.Server.Database; +using Content.Shared.Body; using Content.Shared.CCVar; using Content.Shared.Construction.Prototypes; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; using Content.Shared.Preferences; +using Content.Shared.Preferences.Loadouts; +using Content.Shared.Roles; +using Content.Shared.Traits; using Robust.Server.Player; using Robust.Shared.Configuration; +using Robust.Shared.Enums; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Prototypes; +using Robust.Shared.Serialization.Manager; using Robust.Shared.Utility; namespace Content.Server.Preferences.Managers @@ -29,6 +39,8 @@ namespace Content.Server.Preferences.Managers [Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly UserDbDataManager _userDb = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; + [Dependency] private readonly MarkingManager _marking = default!; + [Dependency] private readonly ISerializationManager _serialization = default!; // Cache player prefs on the server so we don't need as much async hell related to them. private readonly Dictionary _cachedPlayerPrefs = @@ -48,6 +60,135 @@ namespace Content.Server.Preferences.Managers _sawmill = _log.GetSawmill("prefs"); } + private static TValue? TryDeserialize(JsonDocument document) where TValue : class + { + try + { + return document.Deserialize(); + } + catch (JsonException) + { + return null; + } + } + + internal PlayerPreferences ConvertPreferences(Preference prefs) + { + var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1; + var profiles = new Dictionary(maxSlot); + foreach (var profile in prefs.Profiles) + { + profiles[profile.Slot] = ConvertProfiles(profile); + } + + var constructionFavorites = new List>(prefs.ConstructionFavorites.Count); + foreach (var favorite in prefs.ConstructionFavorites) + constructionFavorites.Add(new ProtoId(favorite)); + + return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor), constructionFavorites); + } + + internal HumanoidCharacterProfile ConvertProfiles(Profile profile) + { + + var jobs = profile.Jobs.ToDictionary(j => new ProtoId(j.JobName), j => (JobPriority) j.Priority); + var antags = profile.Antags.Select(a => new ProtoId(a.AntagName)); + var traits = profile.Traits.Select(t => new ProtoId(t.TraitName)); + + var sex = Sex.Male; + if (Enum.TryParse(profile.Sex, true, out var sexVal)) + sex = sexVal; + + var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority; + + var gender = sex == Sex.Male ? Gender.Male : Gender.Female; + if (Enum.TryParse(profile.Gender, true, out var genderVal)) + gender = genderVal; + + + var markings = + new Dictionary, Dictionary>>(); + + var species = profile.Species; + if (!_prototypeManager.HasIndex(species)) + species = HumanoidCharacterProfile.DefaultSpecies; + + if (profile.OrganMarkings?.RootElement is { } element) + { + var data = element.ToDataNode(); + markings = _serialization + .Read, Dictionary>>>( + data, + notNullableOverride: true); + } + else if (profile.Markings is { } profileMarkings && TryDeserialize>(profileMarkings) is { } markingsRaw) + { + List markingsList = new(); + + foreach (var marking in markingsRaw) + { + var parsed = Marking.ParseFromDbString(marking); + + if (parsed is null) continue; + + markingsList.Add(parsed); + } + + if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } facialMarking) + markingsList.Add(facialMarking); + + if (Marking.ParseFromDbString($"{profile.HairName}@{profile.HairColor}") is { } hairMarking) + markingsList.Add(hairMarking); + + markings = _marking.ConvertMarkings(markingsList, species); + } + + var loadouts = new Dictionary(); + + foreach (var role in profile.Loadouts) + { + var loadout = new RoleLoadout(role.RoleName) + { + EntityName = role.EntityName, + }; + + foreach (var group in role.Groups) + { + var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName); + foreach (var profLoadout in group.Loadouts) + { + groupLoadouts.Add(new Loadout() + { + Prototype = profLoadout.LoadoutName, + }); + } + } + + loadouts[role.RoleName] = loadout; + } + + return new HumanoidCharacterProfile( + profile.CharacterName, + profile.FlavorText, + species, + profile.Age, + sex, + gender, + new HumanoidCharacterAppearance + ( + Color.FromHex(profile.EyeColor), + Color.FromHex(profile.SkinColor), + markings + ), + spawnPriority, + jobs, + (PreferenceUnavailableMode) profile.PreferenceUnavailable, + antags.ToHashSet(), + traits.ToHashSet(), + loadouts + ); + } + private async void HandleSelectCharacterMessage(MsgSelectCharacter message) { var index = message.SelectedCharacterIndex; @@ -247,7 +388,7 @@ namespace Content.Server.Preferences.Managers async Task LoadPrefs() { var prefs = await GetOrCreatePreferencesAsync(session.UserId, cancel); - prefsData.Prefs = prefs; + prefsData.Prefs = ConvertPreferences(prefs); } } } @@ -329,7 +470,7 @@ namespace Content.Server.Preferences.Managers return null; } - private async Task GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel) + private async Task GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel) { var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel); if (prefs is null)