mirror of
https://github.com/space-wizards/space-station-14.git
synced 2026-02-14 19:29:53 +01:00
Merge remote-tracking branch 'upstream/stable' into stable-hotfix-merge-26-02-09
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.Value);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user