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:
YotaXP
2025-05-17 13:37:19 -04:00
committed by GitHub
parent 1141dcb868
commit e404e45ffc
16 changed files with 4424 additions and 25 deletions

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Content.Client.Lobby;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Client.UserInterface.Systems.MenuBar.Widgets; using Content.Client.UserInterface.Systems.MenuBar.Widgets;
using Content.Shared.Construction.Prototypes; using Content.Shared.Construction.Prototypes;
@@ -28,6 +29,7 @@ namespace Content.Client.Construction.UI
[Dependency] private readonly IPlacementManager _placementManager = default!; [Dependency] private readonly IPlacementManager _placementManager = default!;
[Dependency] private readonly IUserInterfaceManager _uiManager = default!; [Dependency] private readonly IUserInterfaceManager _uiManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
private readonly SpriteSystem _spriteSystem; private readonly SpriteSystem _spriteSystem;
private readonly IConstructionMenuView _constructionView; private readonly IConstructionMenuView _constructionView;
@@ -116,7 +118,7 @@ namespace Content.Client.Construction.UI
_constructionView.RecipeFavorited += (_, _) => OnViewFavoriteRecipe(); _constructionView.RecipeFavorited += (_, _) => OnViewFavoriteRecipe();
PopulateCategories(); SetFavorites(_preferencesManager.Preferences?.ConstructionFavorites ?? []);
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty)); OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
} }
@@ -493,10 +495,34 @@ namespace Content.Client.Construction.UI
_favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty)); _favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty));
} }
var newFavorites = new List<ProtoId<ConstructionPrototype>>(_favoritedRecipes.Count);
foreach (var recipe in _favoritedRecipes)
newFavorites.Add(recipe.ID);
_preferencesManager.UpdateConstructionFavorites(newFavorites);
PopulateInfo(_selected); PopulateInfo(_selected);
PopulateCategories(_selectedCategory); PopulateCategories(_selectedCategory);
} }
public void SetFavorites(IReadOnlyList<ProtoId<ConstructionPrototype>> favorites)
{
_favoritedRecipes.Clear();
foreach (var id in favorites)
{
if (_prototypeManager.TryIndex(id, out ConstructionPrototype? recipe, logError: false))
_favoritedRecipes.Add(recipe);
}
if (_selectedCategory == FavoriteCatName)
{
OnViewPopulateRecipes(_constructionView,
_favoritedRecipes.Count > 0 ? (string.Empty, FavoriteCatName) : (string.Empty, string.Empty));
}
PopulateCategories(_selectedCategory);
}
private void SystemBindingChanged(ConstructionSystem? newSystem) private void SystemBindingChanged(ConstructionSystem? newSystem)
{ {
if (newSystem is null) if (newSystem is null)

View File

@@ -1,8 +1,10 @@
using System.Linq; using System.Linq;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Client; using Robust.Client;
using Robust.Client.Player; using Robust.Client.Player;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Client.Lobby namespace Content.Client.Lobby
@@ -49,7 +51,7 @@ namespace Content.Client.Lobby
public void SelectCharacter(int slot) public void SelectCharacter(int slot)
{ {
Preferences = new PlayerPreferences(Preferences.Characters, slot, Preferences.AdminOOCColor); Preferences = new PlayerPreferences(Preferences.Characters, slot, Preferences.AdminOOCColor, Preferences.ConstructionFavorites);
var msg = new MsgSelectCharacter var msg = new MsgSelectCharacter
{ {
SelectedCharacterIndex = slot SelectedCharacterIndex = slot
@@ -62,7 +64,7 @@ namespace Content.Client.Lobby
var collection = IoCManager.Instance!; var collection = IoCManager.Instance!;
profile.EnsureValid(_playerManager.LocalSession!, collection); profile.EnsureValid(_playerManager.LocalSession!, collection);
var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile}; var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile};
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor); Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor, Preferences.ConstructionFavorites);
var msg = new MsgUpdateCharacter var msg = new MsgUpdateCharacter
{ {
Profile = profile, Profile = profile,
@@ -85,7 +87,7 @@ namespace Content.Client.Lobby
var l = lowest.Value; var l = lowest.Value;
characters.Add(l, profile); characters.Add(l, profile);
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor); Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor, Preferences.ConstructionFavorites);
UpdateCharacter(profile, l); UpdateCharacter(profile, l);
} }
@@ -98,7 +100,7 @@ namespace Content.Client.Lobby
public void DeleteCharacter(int slot) public void DeleteCharacter(int slot)
{ {
var characters = Preferences.Characters.Where(p => p.Key != slot); var characters = Preferences.Characters.Where(p => p.Key != slot);
Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor); Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor, Preferences.ConstructionFavorites);
var msg = new MsgDeleteCharacter var msg = new MsgDeleteCharacter
{ {
Slot = slot Slot = slot
@@ -106,6 +108,16 @@ namespace Content.Client.Lobby
_netManager.ClientSendMessage(msg); _netManager.ClientSendMessage(msg);
} }
public void UpdateConstructionFavorites(List<ProtoId<ConstructionPrototype>> favorites)
{
Preferences = new PlayerPreferences(Preferences.Characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor, favorites);
var msg = new MsgUpdateConstructionFavorites
{
Favorites = favorites
};
_netManager.ClientSendMessage(msg);
}
private void HandlePreferencesAndSettings(MsgPreferencesAndSettings message) private void HandlePreferencesAndSettings(MsgPreferencesAndSettings message)
{ {
Preferences = message.Preferences; Preferences = message.Preferences;

View File

@@ -1,4 +1,6 @@
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Shared.Prototypes;
namespace Content.Client.Lobby namespace Content.Client.Lobby
{ {
@@ -17,5 +19,6 @@ namespace Content.Client.Lobby
void CreateCharacter(ICharacterProfile profile); void CreateCharacter(ICharacterProfile profile);
void DeleteCharacter(ICharacterProfile profile); void DeleteCharacter(ICharacterProfile profile);
void DeleteCharacter(int slot); void DeleteCharacter(int slot);
void UpdateConstructionFavorites(List<ProtoId<ConstructionPrototype>> favorites);
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ConstructionFavorites : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string[]>(
name: "construction_favorites",
table: "preference",
type: "text[]",
nullable: false,
defaultValue: new string[0]);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "construction_favorites",
table: "preference");
}
}
}

View File

@@ -20,7 +20,7 @@ namespace Content.Server.Database.Migrations.Postgres
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder modelBuilder
.HasAnnotation("ProductVersion", "8.0.0") .HasAnnotation("ProductVersion", "9.0.1")
.HasAnnotation("Relational:MaxIdentifierLength", 63); .HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
@@ -283,8 +283,7 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time"); .HasColumnName("expiration_time");
b.Property<DateTime?>("LastEditedAt") b.Property<DateTime>("LastEditedAt")
.IsRequired()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at"); .HasColumnName("last_edited_at");
@@ -418,8 +417,7 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("expiration_time"); .HasColumnName("expiration_time");
b.Property<DateTime?>("LastEditedAt") b.Property<DateTime>("LastEditedAt")
.IsRequired()
.HasColumnType("timestamp with time zone") .HasColumnType("timestamp with time zone")
.HasColumnName("last_edited_at"); .HasColumnName("last_edited_at");
@@ -797,6 +795,11 @@ namespace Content.Server.Database.Migrations.Postgres
.HasColumnType("text") .HasColumnType("text")
.HasColumnName("admin_ooc_color"); .HasColumnName("admin_ooc_color");
b.PrimitiveCollection<string[]>("ConstructionFavorites")
.IsRequired()
.HasColumnType("text[]")
.HasColumnName("construction_favorites");
b.Property<int>("SelectedCharacterSlot") b.Property<int>("SelectedCharacterSlot")
.HasColumnType("integer") .HasColumnType("integer")
.HasColumnName("selected_character_slot"); .HasColumnName("selected_character_slot");

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,29 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ConstructionFavorites : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "construction_favorites",
table: "preference",
type: "TEXT",
nullable: false,
defaultValue: "[]");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "construction_favorites",
table: "preference");
}
}
}

View File

@@ -15,7 +15,7 @@ namespace Content.Server.Database.Migrations.Sqlite
protected override void BuildModel(ModelBuilder modelBuilder) protected override void BuildModel(ModelBuilder modelBuilder)
{ {
#pragma warning disable 612, 618 #pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "8.0.0"); modelBuilder.HasAnnotation("ProductVersion", "9.0.1");
modelBuilder.Entity("Content.Server.Database.Admin", b => modelBuilder.Entity("Content.Server.Database.Admin", b =>
{ {
@@ -264,8 +264,7 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("expiration_time"); .HasColumnName("expiration_time");
b.Property<DateTime?>("LastEditedAt") b.Property<DateTime>("LastEditedAt")
.IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("last_edited_at"); .HasColumnName("last_edited_at");
@@ -393,8 +392,7 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("expiration_time"); .HasColumnName("expiration_time");
b.Property<DateTime?>("LastEditedAt") b.Property<DateTime>("LastEditedAt")
.IsRequired()
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("last_edited_at"); .HasColumnName("last_edited_at");
@@ -751,6 +749,11 @@ namespace Content.Server.Database.Migrations.Sqlite
.HasColumnType("TEXT") .HasColumnType("TEXT")
.HasColumnName("admin_ooc_color"); .HasColumnName("admin_ooc_color");
b.PrimitiveCollection<string>("ConstructionFavorites")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("construction_favorites");
b.Property<int>("SelectedCharacterSlot") b.Property<int>("SelectedCharacterSlot")
.HasColumnType("INTEGER") .HasColumnType("INTEGER")
.HasColumnName("selected_character_slot"); .HasColumnName("selected_character_slot");

View File

@@ -392,6 +392,7 @@ namespace Content.Server.Database
public Guid UserId { get; set; } public Guid UserId { get; set; }
public int SelectedCharacterSlot { get; set; } public int SelectedCharacterSlot { get; set; }
public string AdminOOCColor { get; set; } = null!; public string AdminOOCColor { get; set; } = null!;
public List<string> ConstructionFavorites { get; set; } = new();
public List<Profile> Profiles { get; } = new(); public List<Profile> Profiles { get; } = new();
} }

View File

@@ -9,6 +9,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Humanoid; using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings; using Content.Shared.Humanoid.Markings;
@@ -65,7 +66,11 @@ namespace Content.Server.Database
profiles[profile.Slot] = ConvertProfiles(profile); 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) public async Task SaveSelectedCharacterIndexAsync(NetUserId userId, int index)
@@ -143,7 +148,8 @@ namespace Content.Server.Database
{ {
UserId = userId.UserId, UserId = userId.UserId,
SelectedCharacterSlot = 0, SelectedCharacterSlot = 0,
AdminOOCColor = Color.Red.ToHex() AdminOOCColor = Color.Red.ToHex(),
ConstructionFavorites = [],
}; };
prefs.Profiles.Add(profile); prefs.Profiles.Add(profile);
@@ -152,7 +158,7 @@ namespace Content.Server.Database
await db.DbContext.SaveChangesAsync(); 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) 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) private static async Task SetSelectedCharacterSlotAsync(NetUserId userId, int newSlot, ServerDbContext db)
{ {
var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId); var prefs = await db.Preference.SingleAsync(p => p.UserId == userId.UserId);

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Shared.Administration.Logs; using Content.Shared.Administration.Logs;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Content.Shared.Roles; using Content.Shared.Roles;
@@ -42,6 +43,8 @@ namespace Content.Server.Database
Task SaveAdminOOCColorAsync(NetUserId userId, Color color); Task SaveAdminOOCColorAsync(NetUserId userId, Color color);
Task SaveConstructionFavoritesAsync(NetUserId userId, List<ProtoId<ConstructionPrototype>> constructionFavorites);
// Single method for two operations for transaction. // Single method for two operations for transaction.
Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot); Task DeleteSlotAndSetSelectedIndex(NetUserId userId, int deleteSlot, int newSlot);
Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel); Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel);
@@ -489,6 +492,12 @@ namespace Content.Server.Database
return RunDbCommand(() => _db.SaveAdminOOCColorAsync(userId, color)); 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) public Task<PlayerPreferences?> GetPlayerPreferencesAsync(NetUserId userId, CancellationToken cancel)
{ {
DbReadOpsMetric.Inc(); DbReadOpsMetric.Inc();

View File

@@ -1,9 +1,11 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Server.Preferences.Managers namespace Content.Server.Preferences.Managers
{ {
@@ -22,5 +24,6 @@ namespace Content.Server.Preferences.Managers
bool HavePreferencesLoaded(ICommonSession session); bool HavePreferencesLoaded(ICommonSession session);
Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile); Task SetProfile(NetUserId userId, int slot, ICharacterProfile profile);
Task SetConstructionFavorites(NetUserId userId, List<ProtoId<ConstructionPrototype>> favorites);
} }
} }

View File

@@ -4,11 +4,13 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Content.Server.Database; using Content.Server.Database;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Construction.Prototypes;
using Content.Shared.Preferences; using Content.Shared.Preferences;
using Robust.Server.Player; using Robust.Server.Player;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Network; using Robust.Shared.Network;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility; using Robust.Shared.Utility;
namespace Content.Server.Preferences.Managers namespace Content.Server.Preferences.Managers
@@ -26,6 +28,7 @@ namespace Content.Server.Preferences.Managers
[Dependency] private readonly IDependencyCollection _dependencies = default!; [Dependency] private readonly IDependencyCollection _dependencies = default!;
[Dependency] private readonly ILogManager _log = default!; [Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly UserDbDataManager _userDb = 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. // Cache player prefs on the server so we don't need as much async hell related to them.
private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs = private readonly Dictionary<NetUserId, PlayerPrefData> _cachedPlayerPrefs =
@@ -41,6 +44,7 @@ namespace Content.Server.Preferences.Managers
_netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage); _netManager.RegisterNetMessage<MsgSelectCharacter>(HandleSelectCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage); _netManager.RegisterNetMessage<MsgUpdateCharacter>(HandleUpdateCharacterMessage);
_netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage); _netManager.RegisterNetMessage<MsgDeleteCharacter>(HandleDeleteCharacterMessage);
_netManager.RegisterNetMessage<MsgUpdateConstructionFavorites>(HandleUpdateConstructionFavoritesMessage);
_sawmill = _log.GetSawmill("prefs"); _sawmill = _log.GetSawmill("prefs");
} }
@@ -68,7 +72,7 @@ namespace Content.Server.Preferences.Managers
return; 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)) if (ShouldStorePrefs(message.MsgChannel.AuthType))
{ {
@@ -108,12 +112,28 @@ namespace Content.Server.Preferences.Managers
[slot] = profile [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)) if (ShouldStorePrefs(session.Channel.AuthType))
await _db.SaveCharacterSlotAsync(userId, profile, slot); 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) private async void HandleDeleteCharacterMessage(MsgDeleteCharacter message)
{ {
var slot = message.Slot; var slot = message.Slot;
@@ -151,7 +171,7 @@ namespace Content.Server.Preferences.Managers
var arr = new Dictionary<int, ICharacterProfile>(curPrefs.Characters); var arr = new Dictionary<int, ICharacterProfile>(curPrefs.Characters);
arr.Remove(slot); 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)) 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. // Should only be called via UserDbDataManager.
public async Task LoadData(ICommonSession session, CancellationToken cancel) public async Task LoadData(ICommonSession session, CancellationToken cancel)
{ {
@@ -176,8 +230,8 @@ namespace Content.Server.Preferences.Managers
{ {
PrefsLoaded = true, PrefsLoaded = true,
Prefs = new PlayerPreferences( Prefs = new PlayerPreferences(
new[] {new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random())}, new[] { new KeyValuePair<int, ICharacterProfile>(0, HumanoidCharacterProfile.Random()) },
0, Color.Transparent) 0, Color.Transparent, [])
}; };
_cachedPlayerPrefs[session.UserId] = prefsData; _cachedPlayerPrefs[session.UserId] = prefsData;
@@ -294,7 +348,7 @@ namespace Content.Server.Preferences.Managers
return new PlayerPreferences(prefs.Characters.Select(p => return new PlayerPreferences(prefs.Characters.Select(p =>
{ {
return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection)); 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( public IEnumerable<KeyValuePair<NetUserId, ICharacterProfile>> GetSelectedProfilesForPlayers(

View File

@@ -0,0 +1,36 @@
using Content.Shared.Construction.Prototypes;
using Lidgren.Network;
using Robust.Shared.Network;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Preferences;
/// <summary>
/// The client sends this to update their construction favorites.
/// </summary>
public sealed class MsgUpdateConstructionFavorites : NetMessage
{
public override MsgGroups MsgGroup => MsgGroups.Command;
public List<ProtoId<ConstructionPrototype>> Favorites = [];
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
var length = buffer.ReadVariableInt32();
Favorites.Clear();
for (var i = 0; i < length; i++)
{
Favorites.Add(new ProtoId<ConstructionPrototype>(buffer.ReadString()));
}
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.WriteVariableInt32(Favorites.Count);
foreach (var favorite in Favorites)
{
buffer.Write(favorite);
}
}
}

View File

@@ -1,3 +1,5 @@
using Content.Shared.Construction.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization; using Robust.Shared.Serialization;
using Robust.Shared.Utility; using Robust.Shared.Utility;
@@ -13,11 +15,12 @@ namespace Content.Shared.Preferences
{ {
private Dictionary<int, ICharacterProfile> _characters; private Dictionary<int, ICharacterProfile> _characters;
public PlayerPreferences(IEnumerable<KeyValuePair<int, ICharacterProfile>> characters, int selectedCharacterIndex, Color adminOOCColor) public PlayerPreferences(IEnumerable<KeyValuePair<int, ICharacterProfile>> characters, int selectedCharacterIndex, Color adminOOCColor, List<ProtoId<ConstructionPrototype>> constructionFavorites)
{ {
_characters = new Dictionary<int, ICharacterProfile>(characters); _characters = new Dictionary<int, ICharacterProfile>(characters);
SelectedCharacterIndex = selectedCharacterIndex; SelectedCharacterIndex = selectedCharacterIndex;
AdminOOCColor = adminOOCColor; AdminOOCColor = adminOOCColor;
ConstructionFavorites = constructionFavorites;
} }
/// <summary> /// <summary>
@@ -42,6 +45,11 @@ namespace Content.Shared.Preferences
public Color AdminOOCColor { get; set; } public Color AdminOOCColor { get; set; }
/// <summary>
/// List of favorite items in the construction menu.
/// </summary>
public List<ProtoId<ConstructionPrototype>> ConstructionFavorites { get; set; } = [];
public int IndexOfCharacter(ICharacterProfile profile) public int IndexOfCharacter(ICharacterProfile profile)
{ {
return _characters.FirstOrNull(p => p.Value == profile)?.Key ?? -1; return _characters.FirstOrNull(p => p.Value == profile)?.Key ?? -1;