Ensure profile loading only returns valid species (#42842)

* Ensure profile loading only returns valid species

* punt conversion logic outside of the database
This commit is contained in:
pathetic meowmeow
2026-02-09 15:30:20 -05:00
committed by GitHub
parent 2320d257d5
commit 4abdcf7a73
6 changed files with 241 additions and 173 deletions

View File

@@ -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<IConfigurationManager>();
var serialization = server.ResolveDependency<ISerializationManager>();
var task = server.ResolveDependency<ITaskManager>();
var opsLog = server.ResolveDependency<ILogManager>().GetSawmill("db.ops");
var builder = new DbContextOptionsBuilder<SqliteServerDbContext>();
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<IServerPreferencesManager>();
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<IServerPreferencesManager>();
var proto = server.ResolveDependency<IPrototypeManager>();
Assert.That(!proto.HasIndex<SpeciesPrototype>(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();
}
}
}

View File

@@ -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<DatabaseNotification>? OnNotificationReceived;
private readonly ITaskManager _task;
private readonly ISerializationManager _serialization;
/// <param name="opsLog">Sawmill to trace log database operations to.</param>
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<PlayerPreferences?> GetPlayerPreferencesAsync(
public async Task<Preference?> 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<int, HumanoidCharacterProfile>(maxSlot);
foreach (var profile in prefs.Profiles)
{
profiles[profile.Slot] = await ConvertProfiles(profile);
}
var constructionFavorites = new List<ProtoId<ConstructionPrototype>>(prefs.ConstructionFavorites.Count);
foreach (var favorite in prefs.ConstructionFavorites)
constructionFavorites.Add(new ProtoId<ConstructionPrototype>(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();
}
/// <summary>
/// Only intended for use in unit tests - drops the organ marking data from a profile in the given slot
/// </summary>
/// <param name="userId">The user whose profile to modify</param>
/// <param name="slot">The slot index to modify</param>
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<string>());
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<PlayerPreferences> InitPrefsAsync(NetUserId userId, HumanoidCharacterProfile defaultProfile)
public async Task<Preference> 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<int, HumanoidCharacterProfile>(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<TValue>(JsonDocument document) where TValue : class
{
try
{
return document.Deserialize<TValue>();
}
catch (JsonException)
{
return null;
}
}
private async Task<HumanoidCharacterProfile> ConvertProfiles(Profile profile)
{
var jobs = profile.Jobs.ToDictionary(j => new ProtoId<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
var sex = Sex.Male;
if (Enum.TryParse<Sex>(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<Gender>(profile.Gender, true, out var genderVal))
gender = genderVal;
var markings =
new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
if (profile.OrganMarkings?.RootElement is { } element)
{
var data = element.ToDataNode();
markings = _serialization
.Read<Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>>(
data,
notNullableOverride: true);
}
else if (profile.Markings is { } profileMarkings && TryDeserialize<List<string>>(profileMarkings) is { } markingsRaw)
{
List<Marking> 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<MarkingManager>();
try
{
markings = markingManager.ConvertMarkings(markingsList, profile.Species);
completion.SetResult();
}
catch (Exception ex)
{
completion.TrySetException(ex);
}
});
await completion.Task;
}
var loadouts = new Dictionary<string, RoleLoadout>();
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();

View File

@@ -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<bool> HasPendingModelChanges();
#region Preferences
Task<PlayerPreferences> InitPrefsAsync(
Task<Preference> 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<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
Task<Preference?> 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<PlayerPreferences> InitPrefsAsync(
public Task<Preference> 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<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
public Task<Preference?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetPlayerPreferencesAsync(userId, cancel));

View File

@@ -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);

View File

@@ -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;

View File

@@ -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<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
@@ -48,6 +60,135 @@ namespace Content.Server.Preferences.Managers
_sawmill = _log.GetSawmill("prefs");
}
private static TValue? TryDeserialize<TValue>(JsonDocument document) where TValue : class
{
try
{
return document.Deserialize<TValue>();
}
catch (JsonException)
{
return null;
}
}
internal PlayerPreferences ConvertPreferences(Preference prefs)
{
var maxSlot = prefs.Profiles.Max(p => p.Slot) + 1;
var profiles = new Dictionary<int, HumanoidCharacterProfile>(maxSlot);
foreach (var profile in prefs.Profiles)
{
profiles[profile.Slot] = ConvertProfiles(profile);
}
var constructionFavorites = new List<ProtoId<ConstructionPrototype>>(prefs.ConstructionFavorites.Count);
foreach (var favorite in prefs.ConstructionFavorites)
constructionFavorites.Add(new ProtoId<ConstructionPrototype>(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<JobPrototype>(j.JobName), j => (JobPriority) j.Priority);
var antags = profile.Antags.Select(a => new ProtoId<AntagPrototype>(a.AntagName));
var traits = profile.Traits.Select(t => new ProtoId<TraitPrototype>(t.TraitName));
var sex = Sex.Male;
if (Enum.TryParse<Sex>(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<Gender>(profile.Gender, true, out var genderVal))
gender = genderVal;
var markings =
new Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>();
var species = profile.Species;
if (!_prototypeManager.HasIndex<SpeciesPrototype>(species))
species = HumanoidCharacterProfile.DefaultSpecies;
if (profile.OrganMarkings?.RootElement is { } element)
{
var data = element.ToDataNode();
markings = _serialization
.Read<Dictionary<ProtoId<OrganCategoryPrototype>, Dictionary<HumanoidVisualLayers, List<Marking>>>>(
data,
notNullableOverride: true);
}
else if (profile.Markings is { } profileMarkings && TryDeserialize<List<string>>(profileMarkings) is { } markingsRaw)
{
List<Marking> 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<string, RoleLoadout>();
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<PlayerPreferences> GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel)
private async Task<Preference> GetOrCreatePreferencesAsync(NetUserId userId, CancellationToken cancel)
{
var prefs = await _db.GetPlayerPreferencesAsync(userId, cancel);
if (prefs is null)