diff --git a/Content.Client/ClientPreferencesManager.cs b/Content.Client/ClientPreferencesManager.cs index 35b1032fdd..c7abc9b5f9 100644 --- a/Content.Client/ClientPreferencesManager.cs +++ b/Content.Client/ClientPreferencesManager.cs @@ -1,3 +1,4 @@ +using System; using System.Linq; using Content.Client.Interfaces; using Content.Shared.Preferences; @@ -17,6 +18,7 @@ namespace Content.Client [Dependency] private readonly IClientNetManager _netManager; #pragma warning restore 649 + public event Action OnServerDataLoaded; public GameSettings Settings { get; private set; } public PlayerPreferences Preferences { get; private set; } @@ -69,6 +71,8 @@ namespace Content.Client { Preferences = message.Preferences; Settings = message.Settings; + + OnServerDataLoaded?.Invoke(); } } } diff --git a/Content.Client/Interfaces/IClientPreferencesManager.cs b/Content.Client/Interfaces/IClientPreferencesManager.cs index d64f019e7e..06fced6678 100644 --- a/Content.Client/Interfaces/IClientPreferencesManager.cs +++ b/Content.Client/Interfaces/IClientPreferencesManager.cs @@ -1,9 +1,14 @@ +using System; using Content.Shared.Preferences; namespace Content.Client.Interfaces { public interface IClientPreferencesManager { + event Action OnServerDataLoaded; + + bool ServerDataLoaded => Settings != null; + GameSettings Settings { get; } PlayerPreferences Preferences { get; } void Initialize(); diff --git a/Content.Client/UserInterface/CharacterSetupGui.cs b/Content.Client/UserInterface/CharacterSetupGui.cs index be82a1668d..69afe49fb2 100644 --- a/Content.Client/UserInterface/CharacterSetupGui.cs +++ b/Content.Client/UserInterface/CharacterSetupGui.cs @@ -136,7 +136,6 @@ namespace Content.Client.UserInterface _createNewCharacterButton = new Button { Text = "Create new slot...", - ToolTip = $"A maximum of {preferencesManager.Settings.MaxCharacterSlots} characters are allowed." }; _createNewCharacterButton.OnPressed += args => { @@ -155,6 +154,8 @@ namespace Content.Client.UserInterface hBox.AddChild(_humanoidProfileEditor); UpdateUI(); + + preferencesManager.OnServerDataLoaded += UpdateUI; } public void Save() => _humanoidProfileEditor.Save(); @@ -164,6 +165,15 @@ namespace Content.Client.UserInterface var numberOfFullSlots = 0; var characterButtonsGroup = new ButtonGroup(); _charactersVBox.RemoveAllChildren(); + + if (!_preferencesManager.ServerDataLoaded) + { + return; + } + + _createNewCharacterButton.ToolTip = + $"A maximum of {_preferencesManager.Settings.MaxCharacterSlots} characters are allowed."; + var characterIndex = 0; foreach (var character in _preferencesManager.Preferences.Characters) { diff --git a/Content.Client/UserInterface/HumanoidProfileEditor.cs b/Content.Client/UserInterface/HumanoidProfileEditor.cs index 647c4180ad..83e0492885 100644 --- a/Content.Client/UserInterface/HumanoidProfileEditor.cs +++ b/Content.Client/UserInterface/HumanoidProfileEditor.cs @@ -51,8 +51,7 @@ namespace Content.Client.UserInterface public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager) { _random = IoCManager.Resolve(); - Profile = (HumanoidCharacterProfile) preferencesManager.Preferences.SelectedCharacter; - CharacterSlot = preferencesManager.Preferences.SelectedCharacterIndex; + _preferencesManager = preferencesManager; var margin = new MarginContainer @@ -365,11 +364,23 @@ namespace Content.Client.UserInterface #endregion Save - UpdateControls(); + if (preferencesManager.ServerDataLoaded) + { + LoadServerData(); + } + + preferencesManager.OnServerDataLoaded += LoadServerData; IsDirty = false; } + private void LoadServerData() + { + Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences.SelectedCharacter; + CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex; + UpdateControls(); + } + private void SetAge(int newAge) { Profile = Profile?.WithAge(newAge); diff --git a/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs b/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs index 98d800445e..c5df73e001 100644 --- a/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs +++ b/Content.Client/UserInterface/LobbyCharacterPreviewPanel.cs @@ -22,6 +22,8 @@ namespace Content.Client.UserInterface private readonly IClientPreferencesManager _preferencesManager; private IEntity _previewDummy; private readonly Label _summaryLabel; + private VBoxContainer _loaded; + private Label _unloaded; public LobbyCharacterPreviewPanel(IEntityManager entityManager, IClientPreferencesManager preferencesManager) @@ -50,9 +52,13 @@ namespace Content.Client.UserInterface var vBox = new VBoxContainer(); vBox.AddChild(header); - vBox.AddChild(CharacterSetupButton); - vBox.AddChild(_summaryLabel); + _unloaded = new Label {Text = "Your character preferences have not yet loaded, please stand by."}; + + _loaded = new VBoxContainer {Visible = false}; + + _loaded.AddChild(CharacterSetupButton); + _loaded.AddChild(_summaryLabel); var hBox = new HBoxContainer(); hBox.AddChild(viewSouth); @@ -60,11 +66,15 @@ namespace Content.Client.UserInterface hBox.AddChild(viewWest); hBox.AddChild(viewEast); - vBox.AddChild(hBox); + _loaded.AddChild(hBox); + vBox.AddChild(_loaded); + vBox.AddChild(_unloaded); AddChild(vBox); UpdateUI(); + + _preferencesManager.OnServerDataLoaded += UpdateUI; } public Button CharacterSetupButton { get; } @@ -89,17 +99,27 @@ namespace Content.Client.UserInterface public void UpdateUI() { - if (!(_preferencesManager.Preferences.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)) + if (!_preferencesManager.ServerDataLoaded) { - _summaryLabel.Text = string.Empty; + _loaded.Visible = false; + _unloaded.Visible = true; } else { - _summaryLabel.Text = selectedCharacter.Summary; - var component = _previewDummy.GetComponent(); - component.UpdateFromProfile(selectedCharacter); + _loaded.Visible = true; + _unloaded.Visible = false; + if (!(_preferencesManager.Preferences.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)) + { + _summaryLabel.Text = string.Empty; + } + else + { + _summaryLabel.Text = selectedCharacter.Summary; + var component = _previewDummy.GetComponent(); + component.UpdateFromProfile(selectedCharacter); - GiveDummyJobClothes(_previewDummy, selectedCharacter); + GiveDummyJobClothes(_previewDummy, selectedCharacter); + } } } diff --git a/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.Designer.cs b/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.Designer.cs new file mode 100644 index 0000000000..408755d35a --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.Designer.cs @@ -0,0 +1,154 @@ +// +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Content.Server.Database.Migrations.Postgres +{ + [DbContext(typeof(PostgresPreferencesDbContext))] + [Migration("20200625230829_AddSlotPrefsIdIndex")] + partial class AddSlotPrefsIdIndex + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) + .HasAnnotation("ProductVersion", "3.1.4") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => + { + b.Property("HumanoidProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("Age") + .HasColumnType("integer"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer"); + + b.Property("PrefsId") + .HasColumnType("integer"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("text"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("text"); + + b.Property("Slot") + .HasColumnType("integer"); + + b.Property("SlotName") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("HumanoidProfileId"); + + b.HasIndex("PrefsId"); + + b.HasIndex("Slot", "PrefsId") + .IsUnique(); + + b.ToTable("HumanoidProfile"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("JobId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("JobName") + .IsRequired() + .HasColumnType("text"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProfileHumanoidProfileId") + .HasColumnType("integer"); + + b.HasKey("JobId"); + + b.HasIndex("ProfileHumanoidProfileId"); + + b.ToTable("Job"); + }); + + modelBuilder.Entity("Content.Server.Database.Prefs", b => + { + b.Property("PrefsId") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("SelectedCharacterSlot") + .HasColumnType("integer"); + + b.Property("Username") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("PrefsId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => + { + b.HasOne("Content.Server.Database.Prefs", "Prefs") + .WithMany("HumanoidProfiles") + .HasForeignKey("PrefsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.HumanoidProfile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileHumanoidProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.cs b/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.cs new file mode 100644 index 0000000000..088a48bafa --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20200625230829_AddSlotPrefsIdIndex.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Postgres +{ + public partial class AddSlotPrefsIdIndex : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_HumanoidProfile_Slot_PrefsId", + table: "HumanoidProfile", + columns: new[] { "Slot", "PrefsId" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_HumanoidProfile_Slot_PrefsId", + table: "HumanoidProfile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresPreferencesDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresPreferencesDbContextModelSnapshot.cs index 5baa572a5c..bfd4851099 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresPreferencesDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresPreferencesDbContextModelSnapshot.cs @@ -1,7 +1,8 @@ // - +using Content.Server.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Content.Server.Database.Migrations.Postgres @@ -14,7 +15,7 @@ namespace Content.Server.Database.Migrations.Postgres #pragma warning disable 612, 618 modelBuilder .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn) - .HasAnnotation("ProductVersion", "3.1.0") + .HasAnnotation("ProductVersion", "3.1.4") .HasAnnotation("Relational:MaxIdentifierLength", 63); modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => @@ -76,6 +77,9 @@ namespace Content.Server.Database.Migrations.Postgres b.HasIndex("PrefsId"); + b.HasIndex("Slot", "PrefsId") + .IsUnique(); + b.ToTable("HumanoidProfile"); }); diff --git a/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.Designer.cs new file mode 100644 index 0000000000..62f527c4f4 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.Designer.cs @@ -0,0 +1,148 @@ +// +using Content.Server.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Content.Server.Database.Migrations.Sqlite +{ + [DbContext(typeof(SqlitePreferencesDbContext))] + [Migration("20200625230839_AddSlotPrefsIdIndex")] + partial class AddSlotPrefsIdIndex + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.4"); + + modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => + { + b.Property("HumanoidProfileId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Age") + .HasColumnType("INTEGER"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("EyeColor") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FacialHairColor") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("FacialHairName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER"); + + b.Property("PrefsId") + .HasColumnType("INTEGER"); + + b.Property("Sex") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SkinColor") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Slot") + .HasColumnType("INTEGER"); + + b.Property("SlotName") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("HumanoidProfileId"); + + b.HasIndex("PrefsId"); + + b.HasIndex("Slot", "PrefsId") + .IsUnique(); + + b.ToTable("HumanoidProfile"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("JobId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("JobName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("ProfileHumanoidProfileId") + .HasColumnType("INTEGER"); + + b.HasKey("JobId"); + + b.HasIndex("ProfileHumanoidProfileId"); + + b.ToTable("Job"); + }); + + modelBuilder.Entity("Content.Server.Database.Prefs", b => + { + b.Property("PrefsId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER"); + + b.Property("Username") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("PrefsId"); + + b.HasIndex("Username") + .IsUnique(); + + b.ToTable("Preferences"); + }); + + modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => + { + b.HasOne("Content.Server.Database.Prefs", "Prefs") + .WithMany("HumanoidProfiles") + .HasForeignKey("PrefsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.HumanoidProfile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileHumanoidProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.cs b/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.cs new file mode 100644 index 0000000000..53863e24b7 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20200625230839_AddSlotPrefsIdIndex.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Content.Server.Database.Migrations.Sqlite +{ + public partial class AddSlotPrefsIdIndex : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateIndex( + name: "IX_HumanoidProfile_Slot_PrefsId", + table: "HumanoidProfile", + columns: new[] { "Slot", "PrefsId" }, + unique: true); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_HumanoidProfile_Slot_PrefsId", + table: "HumanoidProfile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqlitePreferencesDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqlitePreferencesDbContextModelSnapshot.cs index 7f39daafa9..bf6e21f02a 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqlitePreferencesDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqlitePreferencesDbContextModelSnapshot.cs @@ -1,7 +1,8 @@ // - +using Content.Server.Database; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Content.Server.Database.Migrations { @@ -12,7 +13,7 @@ namespace Content.Server.Database.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "3.1.0"); + .HasAnnotation("ProductVersion", "3.1.4"); modelBuilder.Entity("Content.Server.Database.HumanoidProfile", b => { @@ -72,6 +73,9 @@ namespace Content.Server.Database.Migrations b.HasIndex("PrefsId"); + b.HasIndex("Slot", "PrefsId") + .IsUnique(); + b.ToTable("HumanoidProfile"); }); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index d971002adc..e3c62ffd13 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -53,12 +53,17 @@ namespace Content.Server.Database } public DbSet Preferences { get; set; } = null!; + public DbSet HumanoidProfile { get; set; } = null!; protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity() .HasIndex(p => p.Username) .IsUnique(); + + modelBuilder.Entity() + .HasIndex(p => new {p.Slot, p.PrefsId}) + .IsUnique(); } } diff --git a/Content.Server.Database/PrefsDb.cs b/Content.Server.Database/PrefsDb.cs index ea47678d93..a1c3de32cd 100644 --- a/Content.Server.Database/PrefsDb.cs +++ b/Content.Server.Database/PrefsDb.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace Content.Server.Database @@ -20,16 +22,16 @@ namespace Content.Server.Database _prefsCtx.Database.Migrate(); } - public Prefs GetPlayerPreferences(string username) + public async Task GetPlayerPreferences(string username) { - return _prefsCtx + return await _prefsCtx .Preferences .Include(p => p.HumanoidProfiles) .ThenInclude(h => h.Jobs) - .SingleOrDefault(p => p.Username == username); + .SingleOrDefaultAsync(p => p.Username == username); } - public void SaveSelectedCharacterIndex(string username, int slot) + public async Task SaveSelectedCharacterIndex(string username, int slot) { var prefs = _prefsCtx.Preferences.SingleOrDefault(p => p.Username == username); if (prefs is null) @@ -40,10 +42,10 @@ namespace Content.Server.Database }); else prefs.SelectedCharacterSlot = slot; - _prefsCtx.SaveChanges(); + await _prefsCtx.SaveChangesAsync(); } - public void SaveCharacterSlot(string username, HumanoidProfile newProfile) + public async Task SaveCharacterSlotAsync(string username, HumanoidProfile newProfile) { var prefs = _prefsCtx .Preferences @@ -53,17 +55,29 @@ namespace Content.Server.Database .SingleOrDefault(h => h.Slot == newProfile.Slot); if (!(oldProfile is null)) prefs.HumanoidProfiles.Remove(oldProfile); prefs.HumanoidProfiles.Add(newProfile); - _prefsCtx.SaveChanges(); + await _prefsCtx.SaveChangesAsync(); } - public void DeleteCharacterSlot(string username, int slot) + public async Task DeleteCharacterSlotAsync(string username, int slot) { var profile = _prefsCtx .Preferences .Single(p => p.Username == username) .HumanoidProfiles .RemoveAll(h => h.Slot == slot); - _prefsCtx.SaveChanges(); + await _prefsCtx.SaveChangesAsync(); + } + + public async Task> GetProfilesForPlayersAsync(List usernames) + { + return await _prefsCtx.HumanoidProfile + .Include(p => p.Jobs) + .Join(_prefsCtx.Preferences, + profile => new {profile.Slot, profile.PrefsId}, + prefs => new {Slot = prefs.SelectedCharacterSlot, prefs.PrefsId}, + (profile, prefs) => new {prefs.Username, profile}) + .Where(p => usernames.Contains(p.Username)) + .ToDictionaryAsync(arg => arg.Username, arg => arg.profile); } } } diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs index b21ddf4f6f..cec8708c97 100644 --- a/Content.Server/EntryPoint.cs +++ b/Content.Server/EntryPoint.cs @@ -82,9 +82,9 @@ namespace Content.Server { base.PostInit(); + IoCManager.Resolve().FinishInit(); _gameTicker.Initialize(); IoCManager.Resolve().Initialize(); - IoCManager.Resolve().FinishInit(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); IoCManager.Resolve().Initialize(); diff --git a/Content.Server/GameTicking/GameTicker.JobController.cs b/Content.Server/GameTicking/GameTicker.JobController.cs index c7d82c9072..f5e7eb7c46 100644 --- a/Content.Server/GameTicking/GameTicker.JobController.cs +++ b/Content.Server/GameTicking/GameTicker.JobController.cs @@ -19,7 +19,7 @@ namespace Content.Server.GameTicking private readonly Dictionary _spawnedPositions = new Dictionary(); private Dictionary AssignJobs(List available, - Dictionary profiles) + Dictionary profiles) { // Calculate positions available round-start for each job. var availablePositions = GetBasePositions(true); @@ -38,7 +38,7 @@ namespace Content.Server.GameTicking var candidates = available .Select(player => { - var profile = profiles[player]; + var profile = profiles[player.Name]; var availableJobs = profile.JobPriorities .Where(j => diff --git a/Content.Server/GameTicking/GameTicker.cs b/Content.Server/GameTicking/GameTicker.cs index 51b0136957..d5b4715572 100644 --- a/Content.Server/GameTicking/GameTicker.cs +++ b/Content.Server/GameTicking/GameTicker.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using Content.Server.GameObjects; using Content.Server.GameObjects.Components.Access; using Content.Server.GameObjects.Components.Markers; @@ -205,7 +206,7 @@ namespace Content.Server.GameTicking } } - public void StartRound(bool force = false) + public async void StartRound(bool force = false) { DebugTools.Assert(RunLevel == GameRunLevel.PreRoundLobby); Logger.InfoS("ticker", "Starting round!"); @@ -227,7 +228,18 @@ namespace Content.Server.GameTicking RoundLengthMetric.Set(0); // Get the profiles for each player for easier lookup. - var profiles = readyPlayers.ToDictionary(p => p, GetPlayerProfile); + var profiles = (await _prefsManager.GetSelectedProfilesForPlayersAsync( + readyPlayers + .Select(p => p.Name).ToList())) + .ToDictionary(p => p.Key, p => (HumanoidCharacterProfile) p.Value); + + foreach (var readyPlayer in readyPlayers) + { + if (!profiles.ContainsKey(readyPlayer.Name)) + { + profiles.Add(readyPlayer.Name, HumanoidCharacterProfile.Default()); + } + } var assignedJobs = AssignJobs(readyPlayers, profiles); @@ -239,7 +251,7 @@ namespace Content.Server.GameTicking continue; } - var profile = profiles[player]; + var profile = profiles[player.Name]; if (profile.PreferenceUnavailable == PreferenceUnavailableMode.SpawnAsOverflow) { assignedJobs.Add(player, OverflowJob); @@ -259,7 +271,8 @@ namespace Content.Server.GameTicking { SetStartPreset(_configurationManager.GetCVar("game.fallbackpreset")); var newPreset = MakeGamePreset(); - _chatManager.DispatchServerAnnouncement($"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}..."); + _chatManager.DispatchServerAnnouncement( + $"Failed to start {preset.ModeTitle} mode! Defaulting to {newPreset.ModeTitle}..."); if (!newPreset.Start(readyPlayers, force)) { throw new ApplicationException("Fallback preset failed to start!"); @@ -278,8 +291,9 @@ namespace Content.Server.GameTicking IoCManager.Resolve().ServerSendToAll(msg); } - private HumanoidCharacterProfile GetPlayerProfile(IPlayerSession p) => - (HumanoidCharacterProfile) _prefsManager.GetPreferences(p.SessionId.Username).SelectedCharacter; + private async Task GetPlayerProfileAsync(IPlayerSession p) => + (HumanoidCharacterProfile) (await _prefsManager.GetPreferencesAsync(p.SessionId.Username)) + .SelectedCharacter; public void EndRound() { @@ -297,17 +311,19 @@ namespace Content.Server.GameTicking //Generate a list of basic player info to display in the end round summary. var listOfPlayerInfo = new List(); - foreach(var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name)) + foreach (var ply in _playerManager.GetAllPlayers().OrderBy(p => p.Name)) { var mind = ply.ContentData().Mind; - if(mind != null) + if (mind != null) { var antag = mind.AllRoles.Any(role => role.Antag); var playerEndRoundInfo = new RoundEndPlayerInfo() { PlayerOOCName = ply.Name, PlayerICName = mind.CurrentEntity.Name, - Role = antag ? mind.AllRoles.First(role => role.Antag).Name : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"), + Role = antag + ? mind.AllRoles.First(role => role.Antag).Name + : mind.AllRoles.FirstOrDefault()?.Name ?? Loc.GetString("Unknown"), Antag = antag }; listOfPlayerInfo.Add(playerEndRoundInfo); @@ -725,14 +741,14 @@ namespace Content.Server.GameTicking }, _updateShutdownCts.Token); } - private void SpawnPlayer(IPlayerSession session, string jobId = null, bool lateJoin = true) + private async void SpawnPlayer(IPlayerSession session, string jobId = null, bool lateJoin = true) { - var character = (HumanoidCharacterProfile) _prefsManager - .GetPreferences(session.SessionId.Username) - .SelectedCharacter; - _playerJoinGame(session); + var character = (HumanoidCharacterProfile) (await _prefsManager + .GetPreferencesAsync(session.SessionId.Username)) + .SelectedCharacter; + var data = session.ContentData(); data.WipeMind(); data.Mind = new Mind(session.SessionId) @@ -785,15 +801,16 @@ namespace Content.Server.GameTicking accessTags.UnionWith(jobPrototype.Access); pdaComponent.SetPDAOwner(mob); var mindComponent = mob.GetComponent(); - if (mindComponent.HasMind)//Redundancy checks. + if (mindComponent.HasMind) //Redundancy checks. { if (mindComponent.Mind.AllRoles.Any(role => role.Antag)) //Give antags a new uplinkaccount. { - var uplinkAccount = new UplinkAccount(mob.Uid, 20); //TODO: make me into a variable based on server pop or something. + var uplinkAccount = + new UplinkAccount(mob.Uid, + 20); //TODO: make me into a variable based on server pop or something. pdaComponent.InitUplinkAccount(uplinkAccount); } } - } private void AddManifestEntry(string characterName, string jobId) @@ -801,13 +818,14 @@ namespace Content.Server.GameTicking _manifest.Add(new ManifestEntry(characterName, jobId)); } - private void _spawnObserver(IPlayerSession session) + private async void _spawnObserver(IPlayerSession session) { - var name = _prefsManager - .GetPreferences(session.SessionId.Username) + _playerJoinGame(session); + + var name = (await _prefsManager + .GetPreferencesAsync(session.SessionId.Username)) .SelectedCharacter.Name; - _playerJoinGame(session); var data = session.ContentData(); data.WipeMind(); data.Mind = new Mind(session.SessionId); @@ -868,7 +886,7 @@ namespace Content.Server.GameTicking return _localization.GetString(@"Hi and welcome to [color=white]Space Station 14![/color] The current game mode is: [color=white]{0}[/color]. -[color=yellow]{1}[/color]", gmTitle, desc ); +[color=yellow]{1}[/color]", gmTitle, desc); } private void UpdateInfoText() diff --git a/Content.Server/Interfaces/IServerPreferencesManager.cs b/Content.Server/Interfaces/IServerPreferencesManager.cs index f63edf8859..676a16f67f 100644 --- a/Content.Server/Interfaces/IServerPreferencesManager.cs +++ b/Content.Server/Interfaces/IServerPreferencesManager.cs @@ -1,3 +1,5 @@ +using System.Collections.Generic; +using System.Threading.Tasks; using Content.Shared.Preferences; using Robust.Server.Interfaces.Player; @@ -7,7 +9,8 @@ namespace Content.Server.Interfaces { void FinishInit(); void OnClientConnected(IPlayerSession session); - PlayerPreferences GetPreferences(string username); + Task GetPreferencesAsync(string username); + Task>> GetSelectedProfilesForPlayersAsync(List usernames); void StartInit(); } } diff --git a/Content.Server/Preferences/PreferencesDatabase.cs b/Content.Server/Preferences/PreferencesDatabase.cs index 2e5e586e02..b29ff4a255 100644 --- a/Content.Server/Preferences/PreferencesDatabase.cs +++ b/Content.Server/Preferences/PreferencesDatabase.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Content.Server.Database; using Content.Shared.Preferences; using Robust.Shared.Maths; @@ -16,93 +19,143 @@ namespace Content.Server.Preferences private readonly int _maxCharacterSlots; private readonly PrefsDb _prefsDb; + // We use a single DbContext for the entire DB connection, and EFCore doesn't allow concurrent access. + // So we need this semaphore to prevent bugs. + private readonly SemaphoreSlim _prefsSemaphore = new SemaphoreSlim(1, 1); + public PreferencesDatabase(IDatabaseConfiguration dbConfig, int maxCharacterSlots) { _maxCharacterSlots = maxCharacterSlots; _prefsDb = new PrefsDb(dbConfig); } - public PlayerPreferences GetPlayerPreferences(string username) + public async Task GetPlayerPreferencesAsync(string username) { - var prefs = _prefsDb.GetPlayerPreferences(username); - if (prefs is null) return null; - - var profiles = new ICharacterProfile[_maxCharacterSlots]; - foreach (var profile in prefs.HumanoidProfiles) + await _prefsSemaphore.WaitAsync(); + try { - var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority); + var prefs = await _prefsDb.GetPlayerPreferences(username); + if (prefs is null) return null; - profiles[profile.Slot] = new HumanoidCharacterProfile( - profile.CharacterName, - profile.Age, - profile.Sex == "Male" ? Male : Female, - new HumanoidCharacterAppearance - ( - profile.HairName, - Color.FromHex(profile.HairColor), - profile.FacialHairName, - Color.FromHex(profile.FacialHairColor), - Color.FromHex(profile.EyeColor), - Color.FromHex(profile.SkinColor) - ), - jobs, - (PreferenceUnavailableMode) profile.PreferenceUnavailable + var profiles = new ICharacterProfile[_maxCharacterSlots]; + foreach (var profile in prefs.HumanoidProfiles) + { + profiles[profile.Slot] = ConvertProfiles(profile); + } + + return new PlayerPreferences + ( + profiles, + prefs.SelectedCharacterSlot ); } - - return new PlayerPreferences - ( - profiles, - prefs.SelectedCharacterSlot - ); + finally + { + _prefsSemaphore.Release(); + } } - public void SaveSelectedCharacterIndex(string username, int index) + public async Task SaveSelectedCharacterIndexAsync(string username, int index) { - index = index.Clamp(0, _maxCharacterSlots - 1); - _prefsDb.SaveSelectedCharacterIndex(username, index); + await _prefsSemaphore.WaitAsync(); + try + { + index = index.Clamp(0, _maxCharacterSlots - 1); + await _prefsDb.SaveSelectedCharacterIndex(username, index); + } + finally + { + _prefsSemaphore.Release(); + } } - public void SaveCharacterSlot(string username, ICharacterProfile profile, int slot) + public async Task SaveCharacterSlotAsync(string username, ICharacterProfile profile, int slot) { if (slot < 0 || slot >= _maxCharacterSlots) return; - if (profile is null) - { - DeleteCharacterSlot(username, slot); - return; - } - if (!(profile is HumanoidCharacterProfile humanoid)) - // TODO: Handle other ICharacterProfile implementations properly - throw new NotImplementedException(); - var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; - var entity = new HumanoidProfile + await _prefsSemaphore.WaitAsync(); + try { - SlotName = humanoid.Name, - CharacterName = humanoid.Name, - Age = humanoid.Age, - Sex = humanoid.Sex.ToString(), - HairName = appearance.HairStyleName, - HairColor = appearance.HairColor.ToHex(), - FacialHairName = appearance.FacialHairStyleName, - FacialHairColor = appearance.FacialHairColor.ToHex(), - EyeColor = appearance.EyeColor.ToHex(), - SkinColor = appearance.SkinColor.ToHex(), - Slot = slot, - PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable - }; - entity.Jobs.AddRange( - humanoid.JobPriorities - .Where(j => j.Value != JobPriority.Never) - .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value}) - ); - _prefsDb.SaveCharacterSlot(username, entity); + if (profile is null) + { + await DeleteCharacterSlotAsync(username, slot); + return; + } + + if (!(profile is HumanoidCharacterProfile humanoid)) + // TODO: Handle other ICharacterProfile implementations properly + throw new NotImplementedException(); + var appearance = (HumanoidCharacterAppearance) humanoid.CharacterAppearance; + var entity = new HumanoidProfile + { + SlotName = humanoid.Name, + CharacterName = humanoid.Name, + Age = humanoid.Age, + Sex = humanoid.Sex.ToString(), + HairName = appearance.HairStyleName, + HairColor = appearance.HairColor.ToHex(), + FacialHairName = appearance.FacialHairStyleName, + FacialHairColor = appearance.FacialHairColor.ToHex(), + EyeColor = appearance.EyeColor.ToHex(), + SkinColor = appearance.SkinColor.ToHex(), + Slot = slot, + PreferenceUnavailable = (DbPreferenceUnavailableMode) humanoid.PreferenceUnavailable + }; + entity.Jobs.AddRange( + humanoid.JobPriorities + .Where(j => j.Value != JobPriority.Never) + .Select(j => new Job {JobName = j.Key, Priority = (DbJobPriority) j.Value}) + ); + await _prefsDb.SaveCharacterSlotAsync(username, entity); + } + finally + { + _prefsSemaphore.Release(); + } } - private void DeleteCharacterSlot(string username, int slot) + + private async Task DeleteCharacterSlotAsync(string username, int slot) { - _prefsDb.DeleteCharacterSlot(username, slot); + await _prefsDb.DeleteCharacterSlotAsync(username, slot); + } + + public async Task>> GetSelectedProfilesForPlayersAsync( + List usernames) + { + await _prefsSemaphore.WaitAsync(); + try + { + var profiles = await _prefsDb.GetProfilesForPlayersAsync(usernames); + return profiles.Select( + p => new KeyValuePair(p.Key, ConvertProfiles(p.Value))); + } + finally + { + _prefsSemaphore.Release(); + } + } + + private static HumanoidCharacterProfile ConvertProfiles(HumanoidProfile profile) + { + var jobs = profile.Jobs.ToDictionary(j => j.JobName, j => (JobPriority) j.Priority); + return new HumanoidCharacterProfile( + profile.CharacterName, + profile.Age, + profile.Sex == "Male" ? Male : Female, + new HumanoidCharacterAppearance + ( + profile.HairName, + Color.FromHex(profile.HairColor), + profile.FacialHairName, + Color.FromHex(profile.FacialHairColor), + Color.FromHex(profile.EyeColor), + Color.FromHex(profile.SkinColor) + ), + jobs, + (PreferenceUnavailableMode) profile.PreferenceUnavailable + ); } } } diff --git a/Content.Server/Preferences/ServerPreferencesManager.cs b/Content.Server/Preferences/ServerPreferencesManager.cs index ee2a76e525..9b10a7e725 100644 --- a/Content.Server/Preferences/ServerPreferencesManager.cs +++ b/Content.Server/Preferences/ServerPreferencesManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Threading.Tasks; using Content.Server.Database; @@ -82,20 +83,22 @@ namespace Content.Server.Preferences _preferencesDb = _prefsDbLoadTask.Result; } - private void HandleSelectCharacterMessage(MsgSelectCharacter message) + private async void HandleSelectCharacterMessage(MsgSelectCharacter message) { - _preferencesDb.SaveSelectedCharacterIndex(message.MsgChannel.SessionId.Username, message.SelectedCharacterIndex); + await _preferencesDb.SaveSelectedCharacterIndexAsync(message.MsgChannel.SessionId.Username, + message.SelectedCharacterIndex); } - private void HandleUpdateCharacterMessage(MsgUpdateCharacter message) + private async void HandleUpdateCharacterMessage(MsgUpdateCharacter message) { - _preferencesDb.SaveCharacterSlot(message.MsgChannel.SessionId.Username, message.Profile, message.Slot); + await _preferencesDb.SaveCharacterSlotAsync(message.MsgChannel.SessionId.Username, message.Profile, + message.Slot); } - public void OnClientConnected(IPlayerSession session) + public async void OnClientConnected(IPlayerSession session) { var msg = _netManager.CreateNetMessage(); - msg.Preferences = GetPreferences(session.SessionId.Username); + msg.Preferences = await GetPreferencesAsync(session.SessionId.Username); msg.Settings = new GameSettings { MaxCharacterSlots = _configuration.GetCVar("game.maxcharacterslots") @@ -106,26 +109,31 @@ namespace Content.Server.Preferences /// /// Returns the requested or null if not found. /// - private PlayerPreferences GetFromSql(string username) + private async Task GetFromSql(string username) { - return _preferencesDb.GetPlayerPreferences(username); + return await _preferencesDb.GetPlayerPreferencesAsync(username); } /// /// Retrieves preferences for the given username from storage. /// Creates and saves default preferences if they are not found, then returns them. /// - public PlayerPreferences GetPreferences(string username) + public async Task GetPreferencesAsync(string username) { - var prefs = GetFromSql(username); + var prefs = await GetFromSql(username); if (prefs is null) { - _preferencesDb.SaveSelectedCharacterIndex(username, 0); - _preferencesDb.SaveCharacterSlot(username, HumanoidCharacterProfile.Default(), 0); - prefs = GetFromSql(username); + await _preferencesDb.SaveSelectedCharacterIndexAsync(username, 0); + await _preferencesDb.SaveCharacterSlotAsync(username, HumanoidCharacterProfile.Default(), 0); + prefs = await GetFromSql(username); } return prefs; } + + public async Task>> GetSelectedProfilesForPlayersAsync(List usernames) + { + return await _preferencesDb.GetSelectedProfilesForPlayersAsync(usernames); + } } } diff --git a/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs b/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs index c047a1af07..b0a087a5db 100644 --- a/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs +++ b/Content.Tests/Server/Preferences/PreferencesDatabaseTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using Content.Server.Database; using Content.Server.Preferences; using Content.Shared; @@ -44,64 +45,64 @@ namespace Content.Tests.Server.Preferences } [Test] - public void TestUserDoesNotExist() + public async Task TestUserDoesNotExist() { var db = GetDb(); - Assert.Null(db.GetPlayerPreferences("[The database should be empty so any string should do]")); + Assert.Null(await db.GetPlayerPreferencesAsync("[The database should be empty so any string should do]")); } [Test] - public void TestUserDoesExist() + public async Task TestUserDoesExist() { var db = GetDb(); const string username = "bobby"; - db.SaveSelectedCharacterIndex(username, 0); - var prefs = db.GetPlayerPreferences(username); + await db.SaveSelectedCharacterIndexAsync(username, 0); + var prefs = await db.GetPlayerPreferencesAsync(username); Assert.NotNull(prefs); Assert.Zero(prefs.SelectedCharacterIndex); Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); } [Test] - public void TestUpdateCharacter() + public async Task TestUpdateCharacter() { var db = GetDb(); const string username = "charlie"; const int slot = 0; var originalProfile = CharlieCharlieson(); - db.SaveSelectedCharacterIndex(username, slot); - db.SaveCharacterSlot(username, originalProfile, slot); - var prefs = db.GetPlayerPreferences(username); + await db.SaveSelectedCharacterIndexAsync(username, slot); + await db.SaveCharacterSlotAsync(username, originalProfile, slot); + var prefs = await db.GetPlayerPreferencesAsync(username); Assert.That(prefs.Characters.ElementAt(slot).MemberwiseEquals(originalProfile)); } [Test] - public void TestDeleteCharacter() + public async Task TestDeleteCharacter() { var db = GetDb(); const string username = "charlie"; const int slot = 0; - db.SaveSelectedCharacterIndex(username, slot); - db.SaveCharacterSlot(username, CharlieCharlieson(), slot); - db.SaveCharacterSlot(username, null, slot); - var prefs = db.GetPlayerPreferences(username); + await db.SaveSelectedCharacterIndexAsync(username, slot); + await db.SaveCharacterSlotAsync(username, CharlieCharlieson(), slot); + await db.SaveCharacterSlotAsync(username, null, slot); + var prefs = await db.GetPlayerPreferencesAsync(username); Assert.That(prefs.Characters.ToList().TrueForAll(character => character is null)); } [Test] - public void TestInvalidSlot() + public async Task TestInvalidSlot() { var db = GetDb(); const string username = "charlie"; const int slot = -1; - db.SaveSelectedCharacterIndex(username, slot); - db.SaveCharacterSlot(username, CharlieCharlieson(), slot); - var prefs = db.GetPlayerPreferences(username); + await db.SaveSelectedCharacterIndexAsync(username, slot); + await db.SaveCharacterSlotAsync(username, CharlieCharlieson(), slot); + var prefs = await db.GetPlayerPreferencesAsync(username); Assert.AreEqual(prefs.SelectedCharacterIndex, 0); - db.SaveSelectedCharacterIndex(username, MaxCharacterSlots); - prefs = db.GetPlayerPreferences(username); + await db.SaveSelectedCharacterIndexAsync(username, MaxCharacterSlots); + prefs = await db.GetPlayerPreferencesAsync(username); Assert.AreEqual(prefs.SelectedCharacterIndex, MaxCharacterSlots - 1); } }