Persist construction menu favorites server-side (#35867)
* Persist construction menu favorites to player profile * Use `ProtoId`s for construction favorites * Validate construction favorites updates from the client * Actually await the async database call
This commit is contained in:
@@ -9,6 +9,7 @@ using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
@@ -65,7 +66,11 @@ namespace Content.Server.Database
|
||||
profiles[profile.Slot] = ConvertProfiles(profile);
|
||||
}
|
||||
|
||||
return new PlayerPreferences(profiles, prefs.SelectedCharacterSlot, Color.FromHex(prefs.AdminOOCColor));
|
||||
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)
|
||||
@@ -143,7 +148,8 @@ namespace Content.Server.Database
|
||||
{
|
||||
UserId = userId.UserId,
|
||||
SelectedCharacterSlot = 0,
|
||||
AdminOOCColor = Color.Red.ToHex()
|
||||
AdminOOCColor = Color.Red.ToHex(),
|
||||
ConstructionFavorites = [],
|
||||
};
|
||||
|
||||
prefs.Profiles.Add(profile);
|
||||
@@ -152,7 +158,7 @@ namespace Content.Server.Database
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
|
||||
return new PlayerPreferences(new[] {new KeyValuePair<int, ICharacterProfile>(0, defaultProfile)}, 0, Color.FromHex(prefs.AdminOOCColor));
|
||||
return new PlayerPreferences(new[] { new KeyValuePair<int, ICharacterProfile>(0, defaultProfile) }, 0, Color.FromHex(prefs.AdminOOCColor), []);
|
||||
}
|
||||
|
||||
public async Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot)
|
||||
@@ -178,6 +184,19 @@ namespace Content.Server.Database
|
||||
|
||||
}
|
||||
|
||||
public async Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
var prefs = await db.DbContext.Preference.SingleAsync(p => p.UserId == userId.UserId);
|
||||
|
||||
var favorites = new List<string>(constructionFavorites.Count);
|
||||
foreach (var favorite in constructionFavorites)
|
||||
favorites.Add(favorite.Id);
|
||||
prefs.ConstructionFavorites = favorites;
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db)
|
||||
{
|
||||
var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId);
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared.Administration.Logs;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Roles;
|
||||
@@ -42,6 +43,8 @@ namespace Content.Server.Database
|
||||
|
||||
Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
|
||||
|
||||
Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites);
|
||||
|
||||
// Single method for two operations for transaction.
|
||||
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
|
||||
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
|
||||
@@ -489,6 +492,12 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color));
|
||||
}
|
||||
|
||||
public Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.SaveConstructionFavoritesAsync(userId, constructionFavorites));
|
||||
}
|
||||
|
||||
public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
|
||||
{
|
||||
DbReadOpsMetric.Inc();
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
@@ -22,5 +24,6 @@ namespace Content.Server.Preferences.Managers
|
||||
bool HavePreferencesLoaded(ICommonSession session);
|
||||
|
||||
Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile);
|
||||
Task SetConstructionFavorites(NetUserId userId, List<ProtoId<ConstructionPrototype>> favorites);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Construction.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Preferences.Managers
|
||||
@@ -26,6 +28,7 @@ namespace Content.Server.Preferences.Managers
|
||||
[Dependency] private readonly IDependencyCollection _dependencies = default!;
|
||||
[Dependency] private readonly ILogManager _log = default!;
|
||||
[Dependency] private readonly UserDbDataManager _userDb = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = 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 =
|
||||
@@ -41,6 +44,7 @@ namespace Content.Server.Preferences.Managers
|
||||
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
|
||||
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
|
||||
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
|
||||
_netManager.RegisterNetMessage<MsgUpdateConstructionFavorites>(HandleUpdateConstructionFavoritesMessage);
|
||||
_sawmill = _log.GetSawmill("prefs");
|
||||
}
|
||||
|
||||
@@ -68,7 +72,7 @@ namespace Content.Server.Preferences.Managers
|
||||
return;
|
||||
}
|
||||
|
||||
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, index, curPrefs.AdminOOCColor);
|
||||
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, index, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
|
||||
|
||||
if (ShouldStorePrefs(message.MsgChannel.AuthType))
|
||||
{
|
||||
@@ -108,12 +112,28 @@ namespace Content.Server.Preferences.Managers
|
||||
[slot] = profile
|
||||
};
|
||||
|
||||
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor);
|
||||
prefsData.Prefs = new PlayerPreferences(profiles, slot, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
|
||||
|
||||
if (ShouldStorePrefs(session.Channel.AuthType))
|
||||
await _db.SaveCharacterSlotAsync(userId, profile, slot);
|
||||
}
|
||||
|
||||
public async Task SetConstructionFavorites(NetUserId userId, List<ProtoId<ConstructionPrototype>> favorites)
|
||||
{
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
|
||||
{
|
||||
_sawmill.Error($"Tried to modify user {userId} preferences before they loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
var curPrefs = prefsData.Prefs!;
|
||||
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, favorites);
|
||||
|
||||
var session = _playerManager.GetSessionById(userId);
|
||||
if (ShouldStorePrefs(session.Channel.AuthType))
|
||||
await _db.SaveConstructionFavoritesAsync(userId, favorites);
|
||||
}
|
||||
|
||||
private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)
|
||||
{
|
||||
var slot = message.Slot;
|
||||
@@ -151,7 +171,7 @@ namespace Content.Server.Preferences.Managers
|
||||
var arr = new Dictionary<int, ICharacterProfile>(curPrefs.Characters);
|
||||
arr.Remove(slot);
|
||||
|
||||
prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor);
|
||||
prefsData.Prefs = new PlayerPreferences(arr, nextSlot ?? curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, curPrefs.ConstructionFavorites);
|
||||
|
||||
if (ShouldStorePrefs(message.MsgChannel.AuthType))
|
||||
{
|
||||
@@ -166,6 +186,40 @@ namespace Content.Server.Preferences.Managers
|
||||
}
|
||||
}
|
||||
|
||||
private async void HandleUpdateConstructionFavoritesMessage(MsgUpdateConstructionFavorites message)
|
||||
{
|
||||
var userId = message.MsgChannel.UserId;
|
||||
if (!_cachedPlayerPrefs.TryGetValue(userId, out var prefsData) || !prefsData.PrefsLoaded)
|
||||
{
|
||||
_sawmill.Warning($"User {userId} tried to modify preferences before they loaded.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate items in the message so that a modified client cannot freely store a gigabyte of arbitrary data.
|
||||
var validatedSet = new HashSet<ProtoId<ConstructionPrototype>>();
|
||||
foreach (var favorite in message.Favorites)
|
||||
{
|
||||
if (_prototypeManager.HasIndex(favorite))
|
||||
validatedSet.Add(favorite);
|
||||
}
|
||||
|
||||
var validatedList = message.Favorites;
|
||||
if (validatedSet.Count != message.Favorites.Count)
|
||||
{
|
||||
// A difference in counts indicates that unrecognized or duplicate IDs are present.
|
||||
_sawmill.Warning($"User {userId} sent invalid construction favorites.");
|
||||
validatedList = validatedSet.ToList();
|
||||
}
|
||||
|
||||
var curPrefs = prefsData.Prefs!;
|
||||
prefsData.Prefs = new PlayerPreferences(curPrefs.Characters, curPrefs.SelectedCharacterIndex, curPrefs.AdminOOCColor, validatedList);
|
||||
|
||||
if (ShouldStorePrefs(message.MsgChannel.AuthType))
|
||||
{
|
||||
await _db.SaveConstructionFavoritesAsync(userId, validatedList);
|
||||
}
|
||||
}
|
||||
|
||||
// Should only be called via UserDbDataManager.
|
||||
public async Task LoadData(ICommonSession session, CancellationToken cancel)
|
||||
{
|
||||
@@ -176,8 +230,8 @@ namespace Content.Server.Preferences.Managers
|
||||
{
|
||||
PrefsLoaded = true,
|
||||
Prefs = new PlayerPreferences(
|
||||
new[] {new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random())},
|
||||
0, Color.Transparent)
|
||||
new[] { new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random()) },
|
||||
0, Color.Transparent, [])
|
||||
};
|
||||
|
||||
_cachedPlayerPrefs[session.UserId] = prefsData;
|
||||
@@ -294,7 +348,7 @@ namespace Content.Server.Preferences.Managers
|
||||
return new PlayerPreferences(prefs.Characters.Select(p =>
|
||||
{
|
||||
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection));
|
||||
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor);
|
||||
}), prefs.SelectedCharacterIndex, prefs.AdminOOCColor, prefs.ConstructionFavorites);
|
||||
}
|
||||
|
||||
public IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(
|
||||
|
||||
Reference in New Issue
Block a user