]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Lobby refactor + species loadouts support (#27576)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Sat, 11 May 2024 23:18:21 +0000 (09:18 +1000)
committerGitHub <noreply@github.com>
Sat, 11 May 2024 23:18:21 +0000 (09:18 +1000)
* Vox stuff

* Species loadouts and lobby refactor

The control flow for lobby is all over the shop so I pulled it all up from the individual controls so now they handle the bare minimum required and LobbyUIController handles the rest.

* a

* Bulk changes

* a

* weh

* Character import / export

* finalise

* woops this stuff too

* Also datafield exporting

* comments

* Review

49 files changed:
Content.Client/Entry/EntryPoint.cs
Content.Client/IoC/ClientContentIoC.cs
Content.Client/Lobby/ClientPreferencesManager.cs [moved from Content.Client/Preferences/ClientPreferencesManager.cs with 97% similarity]
Content.Client/Lobby/IClientPreferencesManager.cs [moved from Content.Client/Preferences/IClientPreferencesManager.cs with 92% similarity]
Content.Client/Lobby/LobbyState.cs
Content.Client/Lobby/LobbyUIController.cs
Content.Client/Lobby/UI/CharacterPickerButton.xaml [new file with mode: 0644]
Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs [new file with mode: 0644]
Content.Client/Lobby/UI/CharacterSetupGui.xaml [moved from Content.Client/Preferences/UI/CharacterSetupGui.xaml with 91% similarity]
Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs [new file with mode: 0644]
Content.Client/Lobby/UI/HighlightedContainer.xaml [moved from Content.Client/Preferences/UI/HighlightedContainer.xaml with 100% similarity]
Content.Client/Lobby/UI/HighlightedContainer.xaml.cs [moved from Content.Client/Preferences/UI/HighlightedContainer.xaml.cs with 88% similarity]
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml [moved from Content.Client/Preferences/UI/HumanoidProfileEditor.xaml with 68% similarity]
Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs [moved from Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs with 50% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml [moved from Content.Client/Preferences/UI/LoadoutContainer.xaml with 100% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs [moved from Content.Client/Preferences/UI/LoadoutContainer.xaml.cs with 95% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml [moved from Content.Client/Preferences/UI/LoadoutGroupContainer.xaml with 100% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs [moved from Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs with 84% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml [moved from Content.Client/Preferences/UI/LoadoutWindow.xaml with 100% similarity]
Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs [moved from Content.Client/Preferences/UI/LoadoutWindow.xaml.cs with 69% similarity]
Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
Content.Client/Lobby/UI/LobbyGui.xaml.cs
Content.Client/Lobby/UI/ObserveWarningWindow.xaml.cs
Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml [new file with mode: 0644]
Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml.cs [new file with mode: 0644]
Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml [new file with mode: 0644]
Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs [new file with mode: 0644]
Content.Client/Preferences/UI/AntagPreferenceSelector.cs [deleted file]
Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs [deleted file]
Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs [deleted file]
Content.Client/Preferences/UI/JobPrioritySelector.cs [deleted file]
Content.Client/Preferences/UI/RequirementsSelector.cs [deleted file]
Content.IntegrationTests/Tests/Lobby/CharacterCreationTest.cs
Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
Content.Server/Database/ServerDbBase.cs
Content.Shared/Humanoid/HumanoidCharacterAppearance.cs
Content.Shared/Humanoid/HumanoidProfileExport.cs [new file with mode: 0644]
Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Content.Shared/Preferences/Loadouts/Effects/GroupLoadoutEffect.cs
Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs
Content.Shared/Preferences/Loadouts/Effects/LoadoutEffect.cs
Content.Shared/Preferences/Loadouts/Effects/PointsCostLoadoutEffect.cs
Content.Shared/Preferences/Loadouts/Effects/SpeciesLoadoutEffect.cs
Content.Shared/Preferences/Loadouts/Loadout.cs
Content.Shared/Preferences/Loadouts/RoleLoadout.cs
Resources/Locale/en-US/preferences/ui/character-setup-gui.ftl
Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
Resources/Prototypes/Species/vox.yml

index 25490874e9da183376e62eca7efce113b0fa9329..b28c6a11fbfbcb75d919f771c67e5791626e1ed1 100644 (file)
@@ -10,10 +10,10 @@ using Content.Client.Info;
 using Content.Client.Input;
 using Content.Client.IoC;
 using Content.Client.Launcher;
+using Content.Client.Lobby;
 using Content.Client.MainMenu;
 using Content.Client.Parallax.Managers;
 using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Preferences;
 using Content.Client.Radiation.Overlays;
 using Content.Client.Replay;
 using Content.Client.Screenshot;
index 65e95b76f0810aaf0ac32b929567429646e4bd5d..4703915ae76305fcf5b12bbf3a5fdcc6b78744a6 100644 (file)
@@ -2,23 +2,20 @@ using Content.Client.Administration.Managers;
 using Content.Client.Changelog;
 using Content.Client.Chat.Managers;
 using Content.Client.Clickable;
-using Content.Client.Options;
 using Content.Client.Eui;
 using Content.Client.GhostKick;
 using Content.Client.Info;
 using Content.Client.Launcher;
 using Content.Client.Parallax.Managers;
 using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Preferences;
 using Content.Client.Screenshot;
 using Content.Client.Fullscreen;
 using Content.Client.Stylesheets;
 using Content.Client.Viewport;
 using Content.Client.Voting;
-using Content.Shared.Administration;
 using Content.Shared.Administration.Logs;
-using Content.Shared.Module;
 using Content.Client.Guidebook;
+using Content.Client.Lobby;
 using Content.Client.Replay;
 using Content.Shared.Administration.Managers;
 using Content.Shared.Players.PlayTimeTracking;
similarity index 97%
rename from Content.Client/Preferences/ClientPreferencesManager.cs
rename to Content.Client/Lobby/ClientPreferencesManager.cs
index 89cee7bf79b9e04c16552d0856df7a5005850d9c..3f01e1a8f67a48f4353d5bedf502ba75a15165d2 100644 (file)
@@ -2,12 +2,10 @@ using System.Linq;
 using Content.Shared.Preferences;
 using Robust.Client;
 using Robust.Client.Player;
-using Robust.Shared.Configuration;
 using Robust.Shared.Network;
-using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
-namespace Content.Client.Preferences
+namespace Content.Client.Lobby
 {
     /// <summary>
     ///     Receives <see cref="PlayerPreferences" /> and <see cref="GameSettings" /> from the server during the initial
similarity index 92%
rename from Content.Client/Preferences/IClientPreferencesManager.cs
rename to Content.Client/Lobby/IClientPreferencesManager.cs
index e55d6b600cadbc5cd6c4c7b65de8d77a1c487990..45a770b1621f042d9cd1a4d39cd0d0b8d2af1d2e 100644 (file)
@@ -1,7 +1,6 @@
-using System;
 using Content.Shared.Preferences;
 
-namespace Content.Client.Preferences
+namespace Content.Client.Lobby
 {
     public interface IClientPreferencesManager
     {
index 91730020a4e1c499e397c6a20e740459647050f9..1aabc4ff381e784ffbd92301fceeb9c3c639e29f 100644 (file)
@@ -3,8 +3,6 @@ using Content.Client.GameTicking.Managers;
 using Content.Client.LateJoin;
 using Content.Client.Lobby.UI;
 using Content.Client.Message;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
 using Content.Client.UserInterface.Systems.Chat;
 using Content.Client.Voting;
 using Robust.Client;
@@ -12,8 +10,6 @@ using Robust.Client.Console;
 using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Configuration;
-using Robust.Shared.Prototypes;
 using Robust.Shared.Timing;
 
 
@@ -25,20 +21,15 @@ namespace Content.Client.Lobby
         [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
         [Dependency] private readonly IEntityManager _entityManager = default!;
         [Dependency] private readonly IResourceCache _resourceCache = default!;
-        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
         [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
-        [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
         [Dependency] private readonly IGameTiming _gameTiming = default!;
         [Dependency] private readonly IVoteManager _voteManager = default!;
-        [Dependency] private readonly IConfigurationManager _configurationManager = default!;
-
-        [ViewVariables] private CharacterSetupGui? _characterSetup;
 
         private ClientGameTicker _gameTicker = default!;
         private ContentAudioSystem _contentAudioSystem = default!;
 
         protected override Type? LinkedScreenType { get; } = typeof(LobbyGui);
-        private LobbyGui? _lobby;
+        public LobbyGui? Lobby;
 
         protected override void Startup()
         {
@@ -47,45 +38,23 @@ namespace Content.Client.Lobby
                 return;
             }
 
-            _lobby = (LobbyGui) _userInterfaceManager.ActiveScreen;
+            Lobby = (LobbyGui) _userInterfaceManager.ActiveScreen;
 
             var chatController = _userInterfaceManager.GetUIController<ChatUIController>();
             _gameTicker = _entityManager.System<ClientGameTicker>();
             _contentAudioSystem = _entityManager.System<ContentAudioSystem>();
             _contentAudioSystem.LobbySoundtrackChanged += UpdateLobbySoundtrackInfo;
-            _characterSetup = new CharacterSetupGui(_entityManager, _resourceCache, _preferencesManager,
-                _prototypeManager, _configurationManager);
-            LayoutContainer.SetAnchorPreset(_characterSetup, LayoutContainer.LayoutPreset.Wide);
 
-            _lobby.CharacterSetupState.AddChild(_characterSetup);
             chatController.SetMainChat(true);
 
-            _voteManager.SetPopupContainer(_lobby.VoteContainer);
-
-            _characterSetup.CloseButton.OnPressed += _ =>
-            {
-                // Reset sliders etc.
-                _characterSetup?.UpdateControls();
-
-                var controller = _userInterfaceManager.GetUIController<LobbyUIController>();
-                controller.SetClothes(true);
-                controller.UpdateProfile();
-                _lobby.SwitchState(LobbyGui.LobbyGuiState.Default);
-            };
-
-            _characterSetup.SaveButton.OnPressed += _ =>
-            {
-                _characterSetup.Save();
-                _userInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
-            };
-
-            LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
-            _lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
+            _voteManager.SetPopupContainer(Lobby.VoteContainer);
+            LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
+            Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
             UpdateLobbyUi();
 
-            _lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
-            _lobby.ReadyButton.OnPressed += OnReadyPressed;
-            _lobby.ReadyButton.OnToggled += OnReadyToggled;
+            Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
+            Lobby.ReadyButton.OnPressed += OnReadyPressed;
+            Lobby.ReadyButton.OnToggled += OnReadyToggled;
 
             _gameTicker.InfoBlobUpdated += UpdateLobbyUi;
             _gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
@@ -103,20 +72,23 @@ namespace Content.Client.Lobby
 
             _voteManager.ClearPopupContainer();
 
-            _lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
-            _lobby!.ReadyButton.OnPressed -= OnReadyPressed;
-            _lobby!.ReadyButton.OnToggled -= OnReadyToggled;
+            Lobby!.CharacterPreview.CharacterSetupButton.OnPressed -= OnSetupPressed;
+            Lobby!.ReadyButton.OnPressed -= OnReadyPressed;
+            Lobby!.ReadyButton.OnToggled -= OnReadyToggled;
 
-            _lobby = null;
+            Lobby = null;
+        }
 
-            _characterSetup?.Dispose();
-            _characterSetup = null;
+        public void SwitchState(LobbyGui.LobbyGuiState state)
+        {
+            // Yeah I hate this but LobbyState contains all the badness for now.
+            Lobby?.SwitchState(state);
         }
 
         private void OnSetupPressed(BaseButton.ButtonEventArgs args)
         {
             SetReady(false);
-            _lobby!.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
+            Lobby?.SwitchState(LobbyGui.LobbyGuiState.CharacterSetup);
         }
 
         private void OnReadyPressed(BaseButton.ButtonEventArgs args)
@@ -138,13 +110,13 @@ namespace Content.Client.Lobby
         {
             if (_gameTicker.IsGameStarted)
             {
-                _lobby!.StartTime.Text = string.Empty;
+                Lobby!.StartTime.Text = string.Empty;
                 var roundTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
-                _lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes));
+                Lobby!.StationTime.Text = Loc.GetString("lobby-state-player-status-round-time", ("hours", roundTime.Hours), ("minutes", roundTime.Minutes));
                 return;
             }
 
-            _lobby!.StationTime.Text =  Loc.GetString("lobby-state-player-status-round-not-started");
+            Lobby!.StationTime.Text =  Loc.GetString("lobby-state-player-status-round-not-started");
             string text;
 
             if (_gameTicker.Paused)
@@ -153,7 +125,7 @@ namespace Content.Client.Lobby
             }
             else if (_gameTicker.StartTime < _gameTiming.CurTime)
             {
-                _lobby!.StartTime.Text = Loc.GetString("lobby-state-soon");
+                Lobby!.StartTime.Text = Loc.GetString("lobby-state-soon");
                 return;
             }
             else
@@ -170,7 +142,7 @@ namespace Content.Client.Lobby
                 }
             }
 
-            _lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text));
+            Lobby!.StartTime.Text = Loc.GetString("lobby-state-round-start-countdown-text", ("timeLeft", text));
         }
 
         private void LobbyStatusUpdated()
@@ -181,31 +153,31 @@ namespace Content.Client.Lobby
 
         private void LobbyLateJoinStatusUpdated()
         {
-            _lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin;
+            Lobby!.ReadyButton.Disabled = _gameTicker.DisallowedLateJoin;
         }
 
         private void UpdateLobbyUi()
         {
             if (_gameTicker.IsGameStarted)
             {
-                _lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
-                _lobby!.ReadyButton.ToggleMode = false;
-                _lobby!.ReadyButton.Pressed = false;
-                _lobby!.ObserveButton.Disabled = false;
+                Lobby!.ReadyButton.Text = Loc.GetString("lobby-state-ready-button-join-state");
+                Lobby!.ReadyButton.ToggleMode = false;
+                Lobby!.ReadyButton.Pressed = false;
+                Lobby!.ObserveButton.Disabled = false;
             }
             else
             {
-                _lobby!.StartTime.Text = string.Empty;
-                _lobby!.ReadyButton.Text = Loc.GetString(_lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
-                _lobby!.ReadyButton.ToggleMode = true;
-                _lobby!.ReadyButton.Disabled = false;
-                _lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
-                _lobby!.ObserveButton.Disabled = true;
+                Lobby!.StartTime.Text = string.Empty;
+                Lobby!.ReadyButton.Text = Loc.GetString(Lobby!.ReadyButton.Pressed ? "lobby-state-player-status-ready": "lobby-state-player-status-not-ready");
+                Lobby!.ReadyButton.ToggleMode = true;
+                Lobby!.ReadyButton.Disabled = false;
+                Lobby!.ReadyButton.Pressed = _gameTicker.AreWeReady;
+                Lobby!.ObserveButton.Disabled = true;
             }
 
             if (_gameTicker.ServerInfoBlob != null)
             {
-                _lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
+                Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
             }
         }
 
@@ -213,7 +185,7 @@ namespace Content.Client.Lobby
         {
             if (ev.SoundtrackFilename == null)
             {
-                _lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
+                Lobby!.LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
             }
             else if (
                 ev.SoundtrackFilename != null
@@ -234,7 +206,7 @@ namespace Content.Client.Lobby
                     ("songTitle", title),
                     ("songArtist", artist));
 
-                _lobby!.LobbySong.SetMarkup(markup);
+                Lobby!.LobbySong.SetMarkup(markup);
             }
         }
 
@@ -242,11 +214,11 @@ namespace Content.Client.Lobby
         {
             if (_gameTicker.LobbyBackground != null)
             {
-                _lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
+                Lobby!.Background.Texture = _resourceCache.GetResource<TextureResource>(_gameTicker.LobbyBackground );
             }
             else
             {
-                _lobby!.Background.Texture = null;
+                Lobby!.Background.Texture = null;
             }
 
         }
index 9eb259657dc34b6b691f9ec4cd52892b2a90e2f2..ae9196c1100b87380308781ee6d500248de00300 100644 (file)
@@ -2,190 +2,292 @@ using System.Linq;
 using Content.Client.Humanoid;
 using Content.Client.Inventory;
 using Content.Client.Lobby.UI;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
+using Content.Client.Players.PlayTimeTracking;
 using Content.Client.Station;
+using Content.Shared.CCVar;
 using Content.Shared.Clothing;
 using Content.Shared.GameTicking;
+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.Preferences.Loadouts.Effects;
 using Content.Shared.Roles;
+using Content.Shared.Traits;
+using Robust.Client.Player;
+using Robust.Client.ResourceManagement;
 using Robust.Client.State;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Configuration;
 using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Utility;
 
 namespace Content.Client.Lobby;
 
 public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState>, IOnStateExited<LobbyState>
 {
     [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
-    [Dependency] private readonly IStateManager _stateManager = default!;
+    [Dependency] private readonly IConfigurationManager _configurationManager = default!;
+    [Dependency] private readonly IFileDialogManager _dialogManager = default!;
+    [Dependency] private readonly ILogManager _logManager = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
     [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+    [Dependency] private readonly IResourceCache _resourceCache = default!;
+    [Dependency] private readonly IStateManager _stateManager = default!;
+    [Dependency] private readonly JobRequirementsManager _requirements = default!;
+    [Dependency] private readonly MarkingManager _markings = default!;
     [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
     [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
     [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
 
-    private LobbyCharacterPreviewPanel? _previewPanel;
-
-    private bool _showClothes = true;
-
-    /*
-     * Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
-     * that is shared too.
-     */
+    private CharacterSetupGui? _characterSetup;
+    private HumanoidProfileEditor? _profileEditor;
 
     /// <summary>
-    /// Preview dummy for role gear.
+    /// This is the characher preview panel in the chat. This should only update if their character updates.
     /// </summary>
-    private EntityUid? _previewDummy;
+    private LobbyCharacterPreviewPanel? PreviewPanel => GetLobbyPreview();
 
     /// <summary>
-    /// If we currently have a job prototype selected.
+    /// This is the modified profile currently being edited.
     /// </summary>
-    private JobPrototype? _dummyJob;
-
-    // TODO: Load the species directly and don't update entity ever.
-    public event Action<EntityUid>? PreviewDummyUpdated;
+    private HumanoidCharacterProfile? EditedProfile => _profileEditor?.Profile;
 
-    private HumanoidCharacterProfile? _profile;
+    private int? EditedSlot => _profileEditor?.CharacterSlot;
 
     public override void Initialize()
     {
         base.Initialize();
+        _prototypeManager.PrototypesReloaded += OnProtoReload;
         _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
-    }
+        _requirements.Updated += OnRequirementsUpdated;
 
-    private void PreferencesDataLoaded()
-    {
-        UpdateProfile();
+        _configurationManager.OnValueChanged(CCVars.FlavorText, args =>
+        {
+            _profileEditor?.RefreshFlavorText();
+        });
+
+        _configurationManager.OnValueChanged(CCVars.GameRoleTimers, args =>
+        {
+            _profileEditor?.RefreshAntags();
+            _profileEditor?.RefreshJobs();
+            _profileEditor?.RefreshLoadouts();
+        });
     }
 
-    public void OnStateEntered(LobbyState state)
+    private LobbyCharacterPreviewPanel? GetLobbyPreview()
     {
+        if (_stateManager.CurrentState is LobbyState lobby)
+        {
+            return lobby.Lobby?.CharacterPreview;
+        }
+
+        return null;
     }
 
-    public void OnStateExited(LobbyState state)
+    private void OnRequirementsUpdated()
     {
-        EntityManager.DeleteEntity(_previewDummy);
-        _previewDummy = null;
+        if (_profileEditor != null)
+        {
+            _profileEditor.RefreshAntags();
+            _profileEditor.RefreshJobs();
+        }
     }
 
-    public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
+    private void OnProtoReload(PrototypesReloadedEventArgs obj)
     {
-        _previewPanel = panel;
-        ReloadProfile();
+        if (_profileEditor != null)
+        {
+            if (obj.WasModified<AntagPrototype>())
+            {
+                _profileEditor.RefreshAntags();
+            }
+
+            if (obj.WasModified<JobPrototype>() ||
+                obj.WasModified<DepartmentPrototype>())
+            {
+                _profileEditor.RefreshJobs();
+            }
+
+            if (obj.WasModified<LoadoutPrototype>() ||
+                obj.WasModified<LoadoutGroupPrototype>() ||
+                obj.WasModified<RoleLoadoutPrototype>())
+            {
+                _profileEditor.RefreshLoadouts();
+            }
+
+            if (obj.WasModified<SpeciesPrototype>())
+            {
+                _profileEditor.RefreshSpecies();
+            }
+
+            if (obj.WasModified<TraitPrototype>())
+            {
+                _profileEditor.RefreshTraits();
+            }
+        }
     }
 
-    public void SetClothes(bool value)
+    private void PreferencesDataLoaded()
     {
-        if (_showClothes == value)
+        PreviewPanel?.SetLoaded(true);
+
+        if (_stateManager.CurrentState is not LobbyState)
             return;
 
-        _showClothes = value;
-        ReloadCharacterUI();
+        ReloadCharacterSetup();
     }
 
-    public void SetDummyJob(JobPrototype? job)
+    public void OnStateEntered(LobbyState state)
     {
-        _dummyJob = job;
-        ReloadCharacterUI();
+        PreviewPanel?.SetLoaded(_preferencesManager.ServerDataLoaded);
+        ReloadCharacterSetup();
     }
 
-    /// <summary>
-    /// Updates the character only with the specified profile change.
-    /// </summary>
-    public void ReloadProfile()
+    public void OnStateExited(LobbyState state)
     {
-        // Test moment
-        if (_profile == null || _stateManager.CurrentState is not LobbyState)
-            return;
+        PreviewPanel?.SetLoaded(false);
+        _profileEditor?.Dispose();
+        _characterSetup?.Dispose();
 
-        // Ignore job clothes and the likes so we don't spam entities out every frame of color changes.
-        var previewDummy = EnsurePreviewDummy(_profile);
-        _humanoid.LoadProfile(previewDummy, _profile);
+        _characterSetup = null;
+        _profileEditor = null;
     }
 
     /// <summary>
-    /// Updates the currently selected character's preview.
+    /// Reloads every single character setup control.
     /// </summary>
-    public void ReloadCharacterUI()
+    public void ReloadCharacterSetup()
     {
-        // Test moment
-        if (_profile == null || _stateManager.CurrentState is not LobbyState)
-            return;
-
-        EntityManager.DeleteEntity(_previewDummy);
-        _previewDummy = null;
-        _previewDummy = EnsurePreviewDummy(_profile);
-        _previewPanel?.SetSprite(_previewDummy.Value);
-        _previewPanel?.SetSummaryText(_profile.Summary);
-        _humanoid.LoadProfile(_previewDummy.Value, _profile);
-
-        if (_showClothes)
-            GiveDummyJobClothesLoadout(_previewDummy.Value, _profile);
+        RefreshLobbyPreview();
+        var (characterGui, profileEditor) = EnsureGui();
+        characterGui.ReloadCharacterPickers();
+        profileEditor.SetProfile(
+            (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
+            _preferencesManager.Preferences?.SelectedCharacterIndex);
     }
 
     /// <summary>
-    /// Updates character profile to the default.
+    /// Refreshes the character preview in the lobby chat.
     /// </summary>
-    public void UpdateProfile()
+    private void RefreshLobbyPreview()
     {
-        if (!_preferencesManager.ServerDataLoaded)
-        {
-            _profile = null;
+        if (PreviewPanel == null)
             return;
-        }
 
-        if (_preferencesManager.Preferences?.SelectedCharacter is HumanoidCharacterProfile selectedCharacter)
-        {
-            _profile = selectedCharacter;
-            _previewPanel?.SetLoaded(true);
-        }
-        else
+        // Get selected character, load it, then set it
+        var character = _preferencesManager.Preferences?.SelectedCharacter;
+
+        if (character is not HumanoidCharacterProfile humanoid)
         {
-            _previewPanel?.SetSummaryText(string.Empty);
-            _previewPanel?.SetLoaded(false);
+            PreviewPanel.SetSprite(EntityUid.Invalid);
+            PreviewPanel.SetSummaryText(string.Empty);
+            return;
         }
 
-        ReloadCharacterUI();
+        var dummy = LoadProfileEntity(humanoid, null, true);
+        PreviewPanel.SetSprite(dummy);
+        PreviewPanel.SetSummaryText(humanoid.Summary);
     }
 
-    public void UpdateProfile(HumanoidCharacterProfile? profile)
+    private void SaveProfile()
     {
-        if (_profile?.Equals(profile) == true)
+        DebugTools.Assert(EditedProfile != null);
+
+        if (EditedProfile == null || EditedSlot == null)
             return;
 
-        if (_stateManager.CurrentState is not LobbyState)
+        var selected = _preferencesManager.Preferences?.SelectedCharacterIndex;
+
+        if (selected == null)
             return;
 
-        _profile = profile;
+        _preferencesManager.UpdateCharacter(EditedProfile, EditedSlot.Value);
+        ReloadCharacterSetup();
     }
 
-    private EntityUid EnsurePreviewDummy(HumanoidCharacterProfile profile)
+    private (CharacterSetupGui, HumanoidProfileEditor) EnsureGui()
     {
-        if (_previewDummy != null)
-            return _previewDummy.Value;
+        if (_characterSetup != null && _profileEditor != null)
+        {
+            _characterSetup.Visible = true;
+            _profileEditor.Visible = true;
+            return (_characterSetup, _profileEditor);
+        }
+
+        _profileEditor = new HumanoidProfileEditor(
+            _preferencesManager,
+            _configurationManager,
+            EntityManager,
+            _dialogManager,
+            _logManager,
+            _playerManager,
+            _prototypeManager,
+            _requirements,
+            _markings);
+
+        _characterSetup = new CharacterSetupGui(EntityManager, _prototypeManager, _resourceCache, _preferencesManager, _profileEditor);
+
+        _characterSetup.CloseButton.OnPressed += _ =>
+        {
+            // Reset sliders etc.
+            _profileEditor.SetProfile(null, null);
+            _profileEditor.Visible = false;
+
+            if (_stateManager.CurrentState is LobbyState lobbyGui)
+            {
+                lobbyGui.SwitchState(LobbyGui.LobbyGuiState.Default);
+            }
+        };
+
+        _profileEditor.Save += SaveProfile;
+
+        _characterSetup.SelectCharacter += args =>
+        {
+            _preferencesManager.SelectCharacter(args);
+            ReloadCharacterSetup();
+        };
+
+        _characterSetup.DeleteCharacter += args =>
+        {
+            _preferencesManager.DeleteCharacter(args);
 
-        _previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(profile.Species).DollPrototype, MapCoordinates.Nullspace);
-        PreviewDummyUpdated?.Invoke(_previewDummy.Value);
-        return _previewDummy.Value;
+            // Reload everything
+            if (EditedSlot == args)
+            {
+                ReloadCharacterSetup();
+            }
+            else
+            {
+                // Only need to reload character pickers
+                _characterSetup?.ReloadCharacterPickers();
+            }
+        };
+
+        if (_stateManager.CurrentState is LobbyState lobby)
+        {
+            lobby.Lobby?.CharacterSetupState.AddChild(_characterSetup);
+        }
+
+        return (_characterSetup, _profileEditor);
     }
 
+    #region Helpers
+
     /// <summary>
     /// Applies the highest priority job's clothes to the dummy.
     /// </summary>
-    public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
+    public void GiveDummyJobClothesLoadout(EntityUid dummy, JobPrototype? jobProto, HumanoidCharacterProfile profile)
     {
-        var job = _dummyJob ?? GetPreferredJob(profile);
+        var job = jobProto ?? GetPreferredJob(profile);
         GiveDummyJobClothes(dummy, profile, job);
 
         if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
         {
-            var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
+            var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), profile.Species, EntityManager, _prototypeManager);
             GiveDummyLoadout(dummy, loadout);
         }
     }
@@ -279,8 +381,39 @@ public sealed class LobbyUIController : UIController, IOnStateEntered<LobbyState
         }
     }
 
-    public EntityUid? GetPreviewDummy()
+    /// <summary>
+    /// Loads the profile onto a dummy entity.
+    /// </summary>
+    public EntityUid LoadProfileEntity(HumanoidCharacterProfile? humanoid, JobPrototype? job, bool jobClothes)
     {
-        return _previewDummy;
+        EntityUid dummyEnt;
+
+        if (humanoid is not null)
+        {
+            var dummy = _prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
+            dummyEnt = EntityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
+        }
+        else
+        {
+            dummyEnt = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+        }
+
+        _humanoid.LoadProfile(dummyEnt, humanoid);
+
+        if (humanoid != null && jobClothes)
+        {
+            job ??= GetPreferredJob(humanoid);
+            GiveDummyJobClothes(dummyEnt, humanoid, job);
+
+            if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
+            {
+                var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), humanoid.Species, EntityManager, _prototypeManager);
+                GiveDummyLoadout(dummyEnt, loadout);
+            }
+        }
+
+        return dummyEnt;
     }
+
+    #endregion
 }
diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml b/Content.Client/Lobby/UI/CharacterPickerButton.xaml
new file mode 100644 (file)
index 0000000..af1e640
--- /dev/null
@@ -0,0 +1,22 @@
+<ContainerButton xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         xmlns:style="clr-namespace:Content.Client.Stylesheets">
+    <BoxContainer Orientation="Horizontal"
+                  HorizontalExpand="True"
+                  SeparationOverride="0"
+                  Name="InternalHBox">
+        <SpriteView Scale="2 2"
+                    OverrideDirection="South"
+                    Name="View"/>
+        <Label Name="DescriptionLabel"
+               ClipText="True"
+               HorizontalExpand="True"/>
+        <Button Name="DeleteButton"
+                Text="{Loc 'character-setup-gui-character-picker-button-delete-button'}"/>
+        <Button Name="ConfirmDeleteButton"
+                Text="{Loc 'character-setup-gui-character-picker-button-confirm-delete-button'}"
+                Visible="False"
+                ModulateSelfOverride="{x:Static style:StyleNano.ButtonColorCautionDefault}"/>
+
+    </BoxContainer>
+</ContainerButton>
diff --git a/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs b/Content.Client/Lobby/UI/CharacterPickerButton.xaml.cs
new file mode 100644 (file)
index 0000000..2ad8de7
--- /dev/null
@@ -0,0 +1,92 @@
+using System.Linq;
+using Content.Client.Humanoid;
+using Content.Shared.Clothing;
+using Content.Shared.Humanoid;
+using Content.Shared.Humanoid.Prototypes;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Lobby.UI;
+
+/// <summary>
+/// Holds character data on the side of the setup GUI.
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class CharacterPickerButton : ContainerButton
+{
+    private IEntityManager _entManager;
+
+    private EntityUid _previewDummy;
+
+    /// <summary>
+    /// Invoked if we should delete the attached character
+    /// </summary>
+    public event Action? OnDeletePressed;
+
+    public CharacterPickerButton(
+        IEntityManager entityManager,
+        IPrototypeManager prototypeManager,
+        ButtonGroup group,
+        ICharacterProfile profile,
+        bool isSelected)
+    {
+        RobustXamlLoader.Load(this);
+        _entManager = entityManager;
+        AddStyleClass(StyleClassButton);
+        ToggleMode = true;
+        Group = group;
+        var description = profile.Name;
+
+        if (profile is not HumanoidCharacterProfile humanoid)
+        {
+            _previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
+        }
+        else
+        {
+            _previewDummy = UserInterfaceManager.GetUIController<LobbyUIController>()
+                .LoadProfileEntity(humanoid, null, true);
+
+            var highPriorityJob = humanoid.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
+            if (highPriorityJob != null)
+            {
+                var jobName = prototypeManager.Index<JobPrototype>(highPriorityJob).LocalizedName;
+                description = $"{description}\n{jobName}";
+            }
+        }
+
+        Pressed = isSelected;
+        DeleteButton.Visible = !isSelected;
+
+        View.SetEntity(_previewDummy);
+        DescriptionLabel.Text = description;
+
+        ConfirmDeleteButton.OnPressed += _ =>
+        {
+            Parent?.RemoveChild(this);
+            Parent?.RemoveChild(ConfirmDeleteButton);
+            OnDeletePressed?.Invoke();
+        };
+
+        DeleteButton.OnPressed += _ =>
+        {
+            DeleteButton.Visible = false;
+            ConfirmDeleteButton.Visible = true;
+        };
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+
+        _entManager.DeleteEntity(_previewDummy);
+        _previewDummy = default;
+    }
+}
similarity index 91%
rename from Content.Client/Preferences/UI/CharacterSetupGui.xaml
rename to Content.Client/Lobby/UI/CharacterSetupGui.xaml
index 35067eebfd10798ed51255d435d01a954ca1df1b..f83be2658842c2c9d8871887389d4ed880ac2339 100644 (file)
                 <Button Name="RulesButton"
                         Text="{Loc 'character-setup-gui-character-setup-rules-button'}"
                         StyleClasses="ButtonBig"/>
-                <Button Name="SaveButton"
-                        Access="Public"
-                        Text="{Loc 'character-setup-gui-character-setup-save-button'}"
-                        StyleClasses="ButtonBig"/>
                 <Button Name="CloseButton"
                         Access="Public"
                         Text="{Loc 'character-setup-gui-character-setup-close-button'}"
diff --git a/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs b/Content.Client/Lobby/UI/CharacterSetupGui.xaml.cs
new file mode 100644 (file)
index 0000000..777725b
--- /dev/null
@@ -0,0 +1,118 @@
+using Content.Client.Info;
+using Content.Client.Info.PlaytimeStats;
+using Content.Client.Resources;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Lobby.UI
+{
+    /// <summary>
+    /// Holds the entire character setup GUI, from character picks to individual character editing.
+    /// </summary>
+    [GenerateTypedNameReferences]
+    public sealed partial class CharacterSetupGui : Control
+    {
+        private readonly IClientPreferencesManager _preferencesManager;
+        private readonly IEntityManager _entManager;
+        private readonly IPrototypeManager _protomanager;
+
+        private readonly Button _createNewCharacterButton;
+
+        public event Action<int>? SelectCharacter;
+        public event Action<int>? DeleteCharacter;
+
+        public CharacterSetupGui(
+            IEntityManager entManager,
+            IPrototypeManager protoManager,
+            IResourceCache resourceCache,
+            IClientPreferencesManager preferencesManager,
+            HumanoidProfileEditor profileEditor)
+        {
+            RobustXamlLoader.Load(this);
+            _preferencesManager = preferencesManager;
+            _entManager = entManager;
+            _protomanager = protoManager;
+
+            var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
+            var back = new StyleBoxTexture
+            {
+                Texture = panelTex,
+                Modulate = new Color(37, 37, 42)
+            };
+            back.SetPatchMargin(StyleBox.Margin.All, 10);
+
+            BackgroundPanel.PanelOverride = back;
+
+            _createNewCharacterButton = new Button
+            {
+                Text = Loc.GetString("character-setup-gui-create-new-character-button"),
+            };
+
+            _createNewCharacterButton.OnPressed += args =>
+            {
+                preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
+                ReloadCharacterPickers();
+                args.Event.Handle();
+            };
+
+            CharEditor.AddChild(profileEditor);
+            RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
+
+            StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
+        }
+
+        /// <summary>
+        /// Disposes and reloads all character picker buttons from the preferences data.
+        /// </summary>
+        public void ReloadCharacterPickers()
+        {
+            _createNewCharacterButton.Orphan();
+            Characters.DisposeAllChildren();
+
+            var numberOfFullSlots = 0;
+            var characterButtonsGroup = new ButtonGroup();
+
+            if (!_preferencesManager.ServerDataLoaded)
+            {
+                return;
+            }
+
+            _createNewCharacterButton.ToolTip =
+                Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
+                    ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
+
+            var selectedSlot = _preferencesManager.Preferences?.SelectedCharacterIndex;
+
+            foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
+            {
+                numberOfFullSlots++;
+                var characterPickerButton = new CharacterPickerButton(_entManager,
+                    _protomanager,
+                    characterButtonsGroup,
+                    character,
+                    slot == selectedSlot);
+
+                Characters.AddChild(characterPickerButton);
+
+                characterPickerButton.OnPressed += args =>
+                {
+                    SelectCharacter?.Invoke(slot);
+                };
+
+                characterPickerButton.OnDeletePressed += () =>
+                {
+                    DeleteCharacter?.Invoke(slot);
+                };
+            }
+
+            _createNewCharacterButton.Disabled = numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
+            Characters.AddChild(_createNewCharacterButton);
+        }
+    }
+}
similarity index 88%
rename from Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
rename to Content.Client/Lobby/UI/HighlightedContainer.xaml.cs
index 68294d0f05964184602b0ddfa2721b7cbcd838b7..084c1c370982a9710cb3ba006b33ffd8536feba4 100644 (file)
@@ -2,7 +2,7 @@ using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
 
-namespace Content.Client.Preferences.UI;
+namespace Content.Client.Lobby.UI;
 
 [GenerateTypedNameReferences]
 public sealed partial class HighlightedContainer : PanelContainer
similarity index 68%
rename from Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
rename to Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 5926aee8987e56fd3e051b08bf81f971a48a53ff..918b6840b45b760e5e80dd590a0173183846d86c 100644 (file)
@@ -1,8 +1,8 @@
 <BoxContainer xmlns="https://spacestation14.io"
          xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-         xmlns:prefUi="clr-namespace:Content.Client.Preferences.UI"
          xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
          xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
+         xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
          HorizontalExpand="True">
         <!-- Left side -->
         <BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
             <BoxContainer Orientation="Horizontal" SeparationOverride="10">
                 <!-- Name box-->
                 <BoxContainer Orientation="Vertical">
-                    <prefUi:HighlightedContainer>
+                    <ui:HighlightedContainer>
                         <BoxContainer Orientation="Vertical">
-                            <prefUi:HighlightedContainer>
+                            <ui:HighlightedContainer>
                                 <BoxContainer Orientation="Vertical">
                                     <BoxContainer Orientation="Horizontal" VerticalExpand="True">
                                         <Label Text="{Loc 'humanoid-profile-editor-name-label'}" />
-                                        <LineEdit Name="CNameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" />
-                                        <Button Name="CNameRandomize" Text="{Loc 'humanoid-profile-editor-name-random-button'}" />
+                                        <LineEdit Name="NameEdit" MinSize="270 0" VerticalAlignment="Center" Margin="5 0 0 0" />
+                                        <Button Name="NameRandomize" Text="{Loc 'humanoid-profile-editor-name-random-button'}" />
                                     </BoxContainer>
-                                    <Button Name="CRandomizeEverything" HorizontalAlignment="Center"
+                                    <Button Name="RandomizeEverythingButton" HorizontalAlignment="Center"
                                             HorizontalExpand="False" MaxWidth="256"
                                             Text="{Loc 'humanoid-profile-editor-randomize-everything-button'}" />
-                                    <RichTextLabel Name="CWarningLabel" HorizontalExpand="False"
+                                    <RichTextLabel Name="WarningLabel" HorizontalExpand="False"
                                                    VerticalExpand="True" MaxWidth="425"
                                                    HorizontalAlignment="Left" />
                                 </BoxContainer>
-                            </prefUi:HighlightedContainer>
+                            </ui:HighlightedContainer>
                         </BoxContainer>
-                    </prefUi:HighlightedContainer>
+                    </ui:HighlightedContainer>
                 </BoxContainer>
                 <!-- Import/Export -->
                 <BoxContainer Orientation="Vertical">
-                    <prefUi:HighlightedContainer>
-                        <BoxContainer Orientation="Horizontal">
-                            <Button Text="{Loc 'humanoid-profile-editor-import-button'}" Disabled="True"
-                                    ToolTip="{Loc 'generic-not-yet-implemented'}" />
-                            <Button Text="{Loc 'humanoid-profile-editor-export-button'}" Disabled="True"
-                                    ToolTip="{Loc 'generic-not-yet-implemented'}" />
+                    <ui:HighlightedContainer>
+                        <BoxContainer Orientation="Horizontal" SeparationOverride="5">
+                            <Button Name="ImportButton" Text="{Loc 'humanoid-profile-editor-import-button'}"/>
+                            <Button Name="ExportButton" Text="{Loc 'humanoid-profile-editor-export-button'}"/>
+                            <Button Name="SaveButton" Text="{Loc 'humanoid-profile-editor-save-button'}" />
+                            <Button Name="ResetButton" Disabled="True" Text="{Loc 'humanoid-profile-editor-reset-button'}"/>
                         </BoxContainer>
-                    </prefUi:HighlightedContainer>
-                    <!-- Save -->
-                    <prefUi:HighlightedContainer>
-                        <Button Name="CSaveButton" Text="{Loc 'humanoid-profile-editor-save-button'}" HorizontalAlignment="Center" />
-                    </prefUi:HighlightedContainer>
+                    </ui:HighlightedContainer>
                 </BoxContainer>
             </BoxContainer>
             <Control MinHeight="10" />
             <!-- tabContainer -->
-            <TabContainer Name="CTabContainer" VerticalExpand="True">
+            <TabContainer Name="TabContainer" VerticalExpand="True">
                 <BoxContainer Orientation="Vertical">
                     <ScrollContainer VerticalExpand="True">
                         <!-- appearanceList -->
                                     <TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3"
                                                    VerticalAlignment="Center"
                                                    ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/>
-                                    <OptionButton Name="CSpeciesButton" HorizontalAlignment="Right" />
+                                    <OptionButton Name="SpeciesButton" HorizontalAlignment="Right" />
                                 </BoxContainer>
                                 <!-- Age -->
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-age-label'}" />
                                     <Control HorizontalExpand="True"/>
-                                    <LineEdit Name="CAgeEdit" MinSize="40 0" HorizontalAlignment="Right" />
+                                    <LineEdit Name="AgeEdit" MinSize="40 0" HorizontalAlignment="Right" />
                                 </BoxContainer>
                                 <!-- Sex -->
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-sex-label'}" />
                                     <Control HorizontalExpand="True"/>
-                                    <OptionButton Name="CSexButton" HorizontalAlignment="Right" />
+                                    <OptionButton Name="SexButton" HorizontalAlignment="Right" />
                                 </BoxContainer>
                                 <!-- Pronouns -->
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-pronouns-label'}" />
                                     <Control HorizontalExpand="True"/>
-                                    <OptionButton Name="CPronounsButton" HorizontalAlignment="Right" />
+                                    <OptionButton Name="PronounsButton" HorizontalAlignment="Right" />
                                 </BoxContainer>
                                 <!-- Show clothing -->
                                 <BoxContainer HorizontalExpand="True">
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" />
                                     <Control HorizontalExpand="True"/>
-                                    <OptionButton Name="CSpawnPriorityButton" HorizontalAlignment="Right" />
+                                    <OptionButton Name="SpawnPriorityButton" HorizontalAlignment="Right" />
                                 </BoxContainer>
                             </BoxContainer>
                             <!-- Skin -->
                             <BoxContainer Margin="10" HorizontalExpand="True" Orientation="Vertical">
                                 <Label Text="{Loc 'humanoid-profile-editor-skin-color-label'}" />
-                                <Slider HorizontalExpand="True" Name="CSkin" MinValue="0" MaxValue="100" Value="20" />
-                                <BoxContainer Name="CRgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer>
+                                <Slider HorizontalExpand="True" Name="Skin" MinValue="0" MaxValue="100" Value="20" />
+                                <BoxContainer Name="RgbSkinColorContainer" Visible="False" Orientation="Vertical" HorizontalExpand="True"></BoxContainer>
                             </BoxContainer>
                             <!-- Hair -->
                             <BoxContainer Margin="10" Orientation="Horizontal">
-                                <humanoid:SingleMarkingPicker Name="CHairStylePicker" Category="Hair" />
-                                <humanoid:SingleMarkingPicker Name="CFacialHairPicker" Category="FacialHair" />
+                                <humanoid:SingleMarkingPicker Name="HairStylePicker" Category="Hair" />
+                                <humanoid:SingleMarkingPicker Name="FacialHairPicker" Category="FacialHair" />
                             </BoxContainer>
                             <!-- Eyes -->
                             <BoxContainer Margin="10" Orientation="Vertical">
                                 <Label Text="{Loc 'humanoid-profile-editor-eyes-label'}" />
-                                <humanoid:EyeColorPicker Name="CEyeColorPicker" />
+                                <humanoid:EyeColorPicker Name="EyeColorPicker" />
                             </BoxContainer>
                         </BoxContainer>
                     </ScrollContainer>
                 </BoxContainer>
                 <BoxContainer Orientation="Vertical">
                     <!-- Jobs -->
-                    <OptionButton Name="CPreferenceUnavailableButton" />
+                    <OptionButton Name="PreferenceUnavailableButton" />
                     <ScrollContainer VerticalExpand="True">
-                        <BoxContainer Name="CJobList" Orientation="Vertical" />
+                        <BoxContainer Name="JobList" Orientation="Vertical" />
                     </ScrollContainer>
                 </BoxContainer>
                 <BoxContainer Orientation="Vertical" Margin="10">
                     <!-- Antags -->
                     <ScrollContainer VerticalExpand="True">
-                        <BoxContainer Name="CAntagList" Orientation="Vertical" />
+                        <BoxContainer Name="AntagList" Orientation="Vertical" />
                     </ScrollContainer>
                 </BoxContainer>
                 <BoxContainer Orientation="Vertical" Margin="10">
                     <!-- Traits -->
                     <ScrollContainer VerticalExpand="True">
-                        <BoxContainer Name="CTraitsList" Orientation="Vertical" />
+                        <BoxContainer Name="TraitsList" Orientation="Vertical" />
                     </ScrollContainer>
                 </BoxContainer>
-                <BoxContainer Name="CMarkingsTab" Orientation="Vertical" Margin="10">
+                <BoxContainer Name="MarkingsTab" Orientation="Vertical" Margin="10">
                     <!-- Markings -->
                     <ScrollContainer VerticalExpand="True">
-                        <humanoid:MarkingPicker Name="CMarkings" IgnoreCategories="Hair,FacialHair" />
+                        <humanoid:MarkingPicker Name="Markings" IgnoreCategories="Hair,FacialHair" />
                     </ScrollContainer>
                 </BoxContainer>
             </TabContainer>
         </BoxContainer>
         <!-- Right side -->
         <BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
-            <SpriteView Name="CSpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
+            <SpriteView Name="SpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
             <BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
-                <Button Name="CSpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
+                <Button Name="SpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
                 <cc:VSeparator Margin="2 0 3 0" />
-                <Button Name="CSpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
+                <Button Name="SpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
             </BoxContainer>
         </BoxContainer>
 </BoxContainer>
similarity index 50%
rename from Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
rename to Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index dad20a641fde8f00763f13676a8a866f97d60467..a0e32a3a49974a4991d233ebc262c145af5f6189 100644 (file)
@@ -1,12 +1,12 @@
+using System.IO;
 using System.Linq;
 using System.Numerics;
 using Content.Client.Guidebook;
 using Content.Client.Humanoid;
-using Content.Client.Lobby;
+using Content.Client.Lobby.UI.Loadouts;
+using Content.Client.Lobby.UI.Roles;
 using Content.Client.Message;
 using Content.Client.Players.PlayTimeTracking;
-using Content.Client.Stylesheets;
-using Content.Client.UserInterface.Controls;
 using Content.Client.UserInterface.Systems.Guidebook;
 using Content.Shared.CCVar;
 using Content.Shared.Clothing;
@@ -16,11 +16,12 @@ using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
 using Content.Shared.Roles;
+using Content.Shared.StatusIcon;
 using Content.Shared.Traits;
 using Robust.Client.AutoGenerated;
 using Robust.Client.Graphics;
+using Robust.Client.Player;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
@@ -31,91 +32,134 @@ using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 using Direction = Robust.Shared.Maths.Direction;
 
-namespace Content.Client.Preferences.UI
+namespace Content.Client.Lobby.UI
 {
     [GenerateTypedNameReferences]
     public sealed partial class HumanoidProfileEditor : BoxContainer
     {
         private readonly IClientPreferencesManager _preferencesManager;
+        private readonly IConfigurationManager _cfgManager;
+        private readonly IEntityManager _entManager;
+        private readonly IFileDialogManager _dialogManager;
+        private readonly IPlayerManager _playerManager;
         private readonly IPrototypeManager _prototypeManager;
         private readonly MarkingManager _markingManager;
         private readonly JobRequirementsManager _requirements;
+        private readonly LobbyUIController _controller;
 
-        private LineEdit _ageEdit => CAgeEdit;
-        private LineEdit _nameEdit => CNameEdit;
+        private FlavorText.FlavorText? _flavorText;
         private TextEdit? _flavorTextEdit;
-        private Button _nameRandomButton => CNameRandomize;
-        private Button _randomizeEverythingButton => CRandomizeEverything;
-        private RichTextLabel _warningLabel => CWarningLabel;
-        private Button _saveButton => CSaveButton;
-        private OptionButton _sexButton => CSexButton;
-        private OptionButton _genderButton => CPronounsButton;
-        private Slider _skinColor => CSkin;
-        private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
-        private SingleMarkingPicker _hairPicker => CHairStylePicker;
-        private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
-        private EyeColorPicker _eyesPicker => CEyeColorPicker;
-
-        private TabContainer _tabContainer => CTabContainer;
-        private BoxContainer _jobList => CJobList;
-        private BoxContainer _antagList => CAntagList;
-        private BoxContainer _traitsList => CTraitsList;
-        private readonly List<JobPrioritySelector> _jobPriorities;
-        private OptionButton _preferenceUnavailableButton => CPreferenceUnavailableButton;
+
+        // One at a time.
+        private LoadoutWindow? _loadoutWindow;
+
+        private bool _exporting;
+
+        /// <summary>
+        /// If we're attempting to save.
+        /// </summary>
+        public event Action? Save;
+
+        /// <summary>
+        /// Entity used for the profile editor preview
+        /// </summary>
+        public EntityUid PreviewDummy;
+
+        /// <summary>
+        /// Temporary override of their selected job, used to preview roles.
+        /// </summary>
+        public JobPrototype? JobOverride;
+
+        /// <summary>
+        /// The character slot for the current profile.
+        /// </summary>
+        public int? CharacterSlot;
+
+        /// <summary>
+        /// The work in progress profile being edited.
+        /// </summary>
+        public HumanoidCharacterProfile? Profile;
+
+        private List<SpeciesPrototype> _species = new();
+
+        private List<(string, RequirementsSelector)> _jobPriorities = new();
+
         private readonly Dictionary<string, BoxContainer> _jobCategories;
-        // Mildly hacky, as I don't trust prototype order to stay consistent and don't want the UI to break should a new one get added mid-edit. --moony
-        private readonly List<SpeciesPrototype> _speciesList;
-        private readonly List<AntagPreferenceSelector> _antagPreferences = new();
-        private readonly List<TraitPreferenceSelector> _traitPreferences;
-
-        private SpriteView _previewSpriteView => CSpriteView;
-        private Button _previewRotateLeftButton => CSpriteRotateLeft;
-        private Button _previewRotateRightButton => CSpriteRotateRight;
+
         private Direction _previewRotation = Direction.North;
 
-        private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
         private ColorSelectorSliders _rgbSkinColorSelector;
 
         private bool _isDirty;
-        public int CharacterSlot;
-        public HumanoidCharacterProfile? Profile;
-
-        public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
 
         [ValidatePrototypeId<GuideEntryPrototype>]
         private const string DefaultSpeciesGuidebook = "Species";
 
-        public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
+        private ISawmill _sawmill;
+
+        public HumanoidProfileEditor(
+            IClientPreferencesManager preferencesManager,
+            IConfigurationManager configurationManager,
+            IEntityManager entManager,
+            IFileDialogManager dialogManager,
+            ILogManager logManager,
+            IPlayerManager playerManager,
+            IPrototypeManager prototypeManager,
+            JobRequirementsManager requirements,
+            MarkingManager markings)
         {
             RobustXamlLoader.Load(this);
+            _sawmill = logManager.GetSawmill("profile.editor");
+            _cfgManager = configurationManager;
+            _entManager = entManager;
+            _dialogManager = dialogManager;
+            _playerManager = playerManager;
             _prototypeManager = prototypeManager;
+            _markingManager = markings;
             _preferencesManager = preferencesManager;
-            _markingManager = IoCManager.Resolve<MarkingManager>();
-            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-            controller.PreviewDummyUpdated += OnDummyUpdate;
+            _requirements = requirements;
+            _controller = UserInterfaceManager.GetUIController<LobbyUIController>();
+
+            ImportButton.OnPressed += args =>
+            {
+                ImportProfile();
+            };
+
+            ExportButton.OnPressed += args =>
+            {
+                ExportProfile();
+            };
 
-            _previewSpriteView.SetEntity(controller.GetPreviewDummy());
+            ResetButton.OnPressed += args =>
+            {
+                SetProfile((HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter, _preferencesManager.Preferences?.SelectedCharacterIndex);
+            };
+
+            SaveButton.OnPressed += args =>
+            {
+                Save?.Invoke();
+            };
 
             #region Left
 
             #region Name
 
-            _nameEdit.OnTextChanged += args => { SetName(args.Text); };
-            _nameRandomButton.OnPressed += args => RandomizeName();
-            _randomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
-            _warningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
+            NameEdit.OnTextChanged += args => { SetName(args.Text); };
+            NameRandomize.OnPressed += args => RandomizeName();
+            RandomizeEverythingButton.OnPressed += args => { RandomizeEverything(); };
+            WarningLabel.SetMarkup($"[color=red]{Loc.GetString("humanoid-profile-editor-naming-rules-warning")}[/color]");
 
             #endregion Name
 
             #region Appearance
 
-            _tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
+            TabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
 
             #region Sex
 
-            _sexButton.OnItemSelected += args =>
+            SexButton.OnItemSelected += args =>
             {
-                _sexButton.SelectId(args.Id);
+                SexButton.SelectId(args.Id);
                 SetSex((Sex) args.Id);
             };
 
@@ -123,10 +167,11 @@ namespace Content.Client.Preferences.UI
 
             #region Age
 
-            _ageEdit.OnTextChanged += args =>
+            AgeEdit.OnTextChanged += args =>
             {
                 if (!int.TryParse(args.Text, out var newAge))
                     return;
+
                 SetAge(newAge);
             };
 
@@ -134,47 +179,37 @@ namespace Content.Client.Preferences.UI
 
             #region Gender
 
-            _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
-            _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
-            _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
-            _genderButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
+            PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-male-text"), (int) Gender.Male);
+            PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-female-text"), (int) Gender.Female);
+            PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-epicene-text"), (int) Gender.Epicene);
+            PronounsButton.AddItem(Loc.GetString("humanoid-profile-editor-pronouns-neuter-text"), (int) Gender.Neuter);
 
-            _genderButton.OnItemSelected += args =>
+            PronounsButton.OnItemSelected += args =>
             {
-                _genderButton.SelectId(args.Id);
+                PronounsButton.SelectId(args.Id);
                 SetGender((Gender) args.Id);
             };
 
             #endregion Gender
 
-            #region Species
-
-            _speciesList = prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart).ToList();
-            for (var i = 0; i < _speciesList.Count; i++)
-            {
-                var name = Loc.GetString(_speciesList[i].Name);
-                CSpeciesButton.AddItem(name, i);
-            }
+            RefreshSpecies();
 
-            CSpeciesButton.OnItemSelected += args =>
+            SpeciesButton.OnItemSelected += args =>
             {
-                CSpeciesButton.SelectId(args.Id);
-                SetSpecies(_speciesList[args.Id].ID);
+                SpeciesButton.SelectId(args.Id);
+                SetSpecies(_species[args.Id].ID);
                 UpdateHairPickers();
                 OnSkinColorOnValueChanged();
             };
 
-            #endregion Species
-
             #region Skin
 
-
-            _skinColor.OnValueChanged += _ =>
+            Skin.OnValueChanged += _ =>
             {
                 OnSkinColorOnValueChanged();
             };
 
-            _rgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
+            RgbSkinColorContainer.AddChild(_rgbSkinColorSelector = new ColorSelectorSliders());
             _rgbSkinColorSelector.OnColorChanged += _ =>
             {
                 OnSkinColorOnValueChanged();
@@ -184,45 +219,45 @@ namespace Content.Client.Preferences.UI
 
             #region Hair
 
-            _hairPicker.OnMarkingSelect += newStyle =>
+            HairStylePicker.OnMarkingSelect += newStyle =>
             {
                 if (Profile is null)
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithHairStyleName(newStyle.id));
-                SetDirty();
+                ReloadPreview();
             };
 
-            _hairPicker.OnColorChanged += newColor =>
+            HairStylePicker.OnColorChanged += newColor =>
             {
                 if (Profile is null)
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithHairColor(newColor.marking.MarkingColors[0]));
                 UpdateCMarkingsHair();
-                SetDirty();
+                ReloadPreview();
             };
 
-            _facialHairPicker.OnMarkingSelect += newStyle =>
+            FacialHairPicker.OnMarkingSelect += newStyle =>
             {
                 if (Profile is null)
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithFacialHairStyleName(newStyle.id));
-                SetDirty();
+                ReloadPreview();
             };
 
-            _facialHairPicker.OnColorChanged += newColor =>
+            FacialHairPicker.OnColorChanged += newColor =>
             {
                 if (Profile is null)
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithFacialHairColor(newColor.marking.MarkingColors[0]));
                 UpdateCMarkingsFacialHair();
-                SetDirty();
+                ReloadPreview();
             };
 
-            _hairPicker.OnSlotRemove += _ =>
+            HairStylePicker.OnSlotRemove += _ =>
             {
                 if (Profile is null)
                     return;
@@ -231,10 +266,10 @@ namespace Content.Client.Preferences.UI
                 );
                 UpdateHairPickers();
                 UpdateCMarkingsHair();
-                SetDirty();
+                ReloadPreview();
             };
 
-            _facialHairPicker.OnSlotRemove += _ =>
+            FacialHairPicker.OnSlotRemove += _ =>
             {
                 if (Profile is null)
                     return;
@@ -243,10 +278,10 @@ namespace Content.Client.Preferences.UI
                 );
                 UpdateHairPickers();
                 UpdateCMarkingsFacialHair();
-                SetDirty();
+                ReloadPreview();
             };
 
-            _hairPicker.OnSlotAdd += delegate()
+            HairStylePicker.OnSlotAdd += delegate()
             {
                 if (Profile is null)
                     return;
@@ -263,10 +298,10 @@ namespace Content.Client.Preferences.UI
 
                 UpdateHairPickers();
                 UpdateCMarkingsHair();
-                SetDirty();
+                ReloadPreview();
             };
 
-            _facialHairPicker.OnSlotAdd += delegate()
+            FacialHairPicker.OnSlotAdd += delegate()
             {
                 if (Profile is null)
                     return;
@@ -283,7 +318,7 @@ namespace Content.Client.Preferences.UI
 
                 UpdateHairPickers();
                 UpdateCMarkingsFacialHair();
-                SetDirty();
+                ReloadPreview();
             };
 
             #endregion Hair
@@ -292,12 +327,12 @@ namespace Content.Client.Preferences.UI
 
             foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
             {
-                _spawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value);
+                SpawnPriorityButton.AddItem(Loc.GetString($"humanoid-profile-editor-preference-spawn-priority-{value.ToString().ToLower()}"), (int) value);
             }
 
-            _spawnPriorityButton.OnItemSelected += args =>
+            SpawnPriorityButton.OnItemSelected += args =>
             {
-                _spawnPriorityButton.SelectId(args.Id);
+                SpawnPriorityButton.SelectId(args.Id);
                 SetSpawnPriority((SpawnPriorityPreference) args.Id);
             };
 
@@ -305,14 +340,14 @@ namespace Content.Client.Preferences.UI
 
             #region Eyes
 
-            _eyesPicker.OnEyeColorPicked += newColor =>
+            EyeColorPicker.OnEyeColorPicked += newColor =>
             {
                 if (Profile is null)
                     return;
                 Profile = Profile.WithCharacterAppearance(
                     Profile.Appearance.WithEyeColor(newColor));
-                CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
-                SetDirty();
+                Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+                ReloadProfilePreview();
             };
 
             #endregion Eyes
@@ -321,144 +356,340 @@ namespace Content.Client.Preferences.UI
 
             #region Jobs
 
-            _tabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab"));
+            TabContainer.SetTabTitle(1, Loc.GetString("humanoid-profile-editor-jobs-tab"));
 
-            _preferenceUnavailableButton.AddItem(
+            PreferenceUnavailableButton.AddItem(
                 Loc.GetString("humanoid-profile-editor-preference-unavailable-stay-in-lobby-button"),
                 (int) PreferenceUnavailableMode.StayInLobby);
-            _preferenceUnavailableButton.AddItem(
+            PreferenceUnavailableButton.AddItem(
                 Loc.GetString("humanoid-profile-editor-preference-unavailable-spawn-as-overflow-button",
                               ("overflowJob", Loc.GetString(SharedGameTicker.FallbackOverflowJobName))),
                 (int) PreferenceUnavailableMode.SpawnAsOverflow);
 
-            _preferenceUnavailableButton.OnItemSelected += args =>
+            PreferenceUnavailableButton.OnItemSelected += args =>
             {
-                _preferenceUnavailableButton.SelectId(args.Id);
-
+                PreferenceUnavailableButton.SelectId(args.Id);
                 Profile = Profile?.WithPreferenceUnavailable((PreferenceUnavailableMode) args.Id);
-                SetDirty();
             };
 
-            _jobPriorities = new List<JobPrioritySelector>();
             _jobCategories = new Dictionary<string, BoxContainer>();
-            _requirements = IoCManager.Resolve<JobRequirementsManager>();
-            // TODO: Move this to the LobbyUIController instead of being spaghetti everywhere.
-            _requirements.Updated += UpdateAntagRequirements;
-            _requirements.Updated += UpdateRoleRequirements;
-            UpdateAntagRequirements();
-            UpdateRoleRequirements();
+
+            RefreshAntags();
+            RefreshJobs();
 
             #endregion Jobs
 
-            _tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
+            TabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
+
+            RefreshTraits();
 
-            #region Traits
+            #region Markings
 
-            var traits = prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
-            _traitPreferences = new List<TraitPreferenceSelector>();
-            _tabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
+            TabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
+
+            Markings.OnMarkingAdded += OnMarkingChange;
+            Markings.OnMarkingRemoved += OnMarkingChange;
+            Markings.OnMarkingColorChange += OnMarkingChange;
+            Markings.OnMarkingRankChange += OnMarkingChange;
+
+            #endregion Markings
+
+            RefreshFlavorText();
+
+            #region Dummy
+
+            SpriteRotateLeft.OnPressed += _ =>
+            {
+                _previewRotation = _previewRotation.TurnCw();
+                SetPreviewRotation(_previewRotation);
+            };
+            SpriteRotateRight.OnPressed += _ =>
+            {
+                _previewRotation = _previewRotation.TurnCcw();
+                SetPreviewRotation(_previewRotation);
+            };
+
+            #endregion Dummy
+
+            #endregion Left
+
+            ShowClothes.OnToggled += args =>
+            {
+                ReloadPreview();
+            };
+
+            SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
+
+            UpdateSpeciesGuidebookIcon();
+            ReloadPreview();
+            IsDirty = false;
+        }
+
+        /// <summary>
+        /// Refreshes the flavor text editor status.
+        /// </summary>
+        public void RefreshFlavorText()
+        {
+            if (_cfgManager.GetCVar(CCVars.FlavorText))
+            {
+                if (_flavorText != null)
+                    return;
+
+                _flavorText = new FlavorText.FlavorText();
+                TabContainer.AddChild(_flavorText);
+                TabContainer.SetTabTitle(TabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
+                _flavorTextEdit = _flavorText.CFlavorTextInput;
+
+                _flavorText.OnFlavorTextChanged += OnFlavorTextChange;
+            }
+            else
+            {
+                if (_flavorText == null)
+                    return;
+
+                TabContainer.RemoveChild(_flavorText);
+                _flavorText.OnFlavorTextChanged -= OnFlavorTextChange;
+                _flavorText.Dispose();
+                _flavorTextEdit?.Dispose();
+                _flavorTextEdit = null;
+                _flavorText = null;
+            }
+        }
+
+        /// <summary>
+        /// Refreshes traits selector
+        /// </summary>
+        public void RefreshTraits()
+        {
+            TraitsList.DisposeAllChildren();
+
+            var traits = _prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
+            TabContainer.SetTabTitle(3, Loc.GetString("humanoid-profile-editor-traits-tab"));
 
             if (traits.Count > 0)
             {
                 foreach (var trait in traits)
                 {
                     var selector = new TraitPreferenceSelector(trait);
-                    _traitsList.AddChild(selector);
-                    _traitPreferences.Add(selector);
+
+                    if (Profile?.TraitPreferences.Contains(trait.ID) == true)
+                    {
+                        selector.Preference = true;
+                    }
+                    else
+                    {
+                        selector.Preference = false;
+                    }
 
                     selector.PreferenceChanged += preference =>
                     {
                         Profile = Profile?.WithTraitPreference(trait.ID, preference);
                         SetDirty();
                     };
+
+                    TraitsList.AddChild(selector);
                 }
             }
             else
             {
-                _traitsList.AddChild(new Label
+                TraitsList.AddChild(new Label
                 {
+                    // TODO: Localise
                     Text = "No traits available :(",
                     FontColorOverride = Color.Gray,
                 });
             }
+        }
 
-            #endregion
+        /// <summary>
+        /// Refreshes the species selector.
+        /// </summary>
+        public void RefreshSpecies()
+        {
+            SpeciesButton.Clear();
+            _species.Clear();
 
-            #region Save
+            _species.AddRange(_prototypeManager.EnumeratePrototypes<SpeciesPrototype>().Where(o => o.RoundStart));
+            var speciesIds = _species.Select(o => o.ID).ToList();
 
-            _saveButton.OnPressed += _ => { Save(); };
+            for (var i = 0; i < _species.Count; i++)
+            {
+                var name = Loc.GetString(_species[i].Name);
+                SpeciesButton.AddItem(name, i);
 
-            #endregion Save
+                if (Profile?.Species.Equals(_species[i].ID) == true)
+                {
+                    SpeciesButton.SelectId(i);
+                }
+            }
 
-            #region Markings
-            _tabContainer.SetTabTitle(4, Loc.GetString("humanoid-profile-editor-markings-tab"));
+            // If our species isn't available then reset it to default.
+            if (Profile != null)
+            {
+                if (!speciesIds.Contains(Profile.Species))
+                {
+                    SetSpecies(SharedHumanoidAppearanceSystem.DefaultSpecies);
+                }
+            }
+        }
 
-            CMarkings.OnMarkingAdded += OnMarkingChange;
-            CMarkings.OnMarkingRemoved += OnMarkingChange;
-            CMarkings.OnMarkingColorChange += OnMarkingChange;
-            CMarkings.OnMarkingRankChange += OnMarkingChange;
+        public void RefreshAntags()
+        {
+            AntagList.DisposeAllChildren();
+            var items = new[]
+            {
+                ("humanoid-profile-editor-antag-preference-yes-button", 0),
+                ("humanoid-profile-editor-antag-preference-no-button", 1)
+            };
 
-            #endregion Markings
+            foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
+            {
+                if (!antag.SetPreference)
+                    continue;
 
-            #region FlavorText
+                var antagContainer = new BoxContainer()
+                {
+                    Orientation = LayoutOrientation.Horizontal,
+                };
 
-            if (configurationManager.GetCVar(CCVars.FlavorText))
-            {
-                var flavorText = new FlavorText.FlavorText();
-                _tabContainer.AddChild(flavorText);
-                _tabContainer.SetTabTitle(_tabContainer.ChildCount - 1, Loc.GetString("humanoid-profile-editor-flavortext-tab"));
-                _flavorTextEdit = flavorText.CFlavorTextInput;
+                var selector = new RequirementsSelector()
+                {
+                    Margin = new Thickness(3f, 3f, 3f, 0f),
+                };
 
-                flavorText.OnFlavorTextChanged += OnFlavorTextChange;
-            }
+                var title = Loc.GetString(antag.Name);
+                var description = Loc.GetString(antag.Objective);
+                selector.Setup(items, title, 250, description);
+                selector.Select(Profile?.AntagPreferences.Contains(antag.ID) == true ? 0 : 1);
 
-            #endregion FlavorText
+                if (!_requirements.CheckRoleTime(antag.Requirements, out var reason))
+                {
+                    selector.LockRequirements(reason);
+                    Profile = Profile?.WithAntagPreference(antag.ID, false);
+                    SetDirty();
+                }
+                else
+                {
+                    selector.UnlockRequirements();
+                }
 
-            #region Dummy
+                selector.OnSelected += preference =>
+                {
+                    Profile = Profile?.WithAntagPreference(antag.ID, preference == 0);
+                    SetDirty();
+                };
 
-            _previewRotateLeftButton.OnPressed += _ =>
-            {
-                _previewRotation = _previewRotation.TurnCw();
-                SetPreviewRotation(_previewRotation);
-            };
-            _previewRotateRightButton.OnPressed += _ =>
-            {
-                _previewRotation = _previewRotation.TurnCcw();
-                SetPreviewRotation(_previewRotation);
-            };
+                antagContainer.AddChild(selector);
 
-            #endregion Dummy
+                antagContainer.AddChild(new Button()
+                {
+                    Disabled = true,
+                    Text = Loc.GetString("loadout-window"),
+                    HorizontalAlignment = HAlignment.Right,
+                    Margin = new Thickness(3f, 0f, 0f, 0f),
+                });
 
-            #endregion Left
+                AntagList.AddChild(antagContainer);
+            }
+        }
 
-            if (preferencesManager.ServerDataLoaded)
+        private void SetDirty()
+        {
+            // If it equals default then reset the button.
+            if (Profile == null || _preferencesManager.Preferences?.SelectedCharacter.MemberwiseEquals(Profile) == true)
             {
-                LoadServerData();
+                IsDirty = false;
+                return;
             }
 
-            ShowClothes.OnToggled += args =>
-            {
-                var lobby = UserInterfaceManager.GetUIController<LobbyUIController>();
-                lobby.SetClothes(args.Pressed);
-                SetDirty();
-            };
+            // TODO: Check if profile matches default.
+            IsDirty = true;
+        }
 
-            preferencesManager.OnServerDataLoaded += LoadServerData;
+        /// <summary>
+        /// Refresh all loadouts.
+        /// </summary>
+        public void RefreshLoadouts()
+        {
+            _loadoutWindow?.Dispose();
+        }
 
-            SpeciesInfoButton.OnPressed += OnSpeciesInfoButtonPressed;
+        /// <summary>
+        /// Reloads the entire dummy entity for preview.
+        /// </summary>
+        /// <remarks>
+        /// This is expensive so not recommended to run if you have a slider.
+        /// </remarks>
+        private void ReloadPreview()
+        {
+            _entManager.DeleteEntity(PreviewDummy);
+            PreviewDummy = EntityUid.Invalid;
 
-            UpdateSpeciesGuidebookIcon();
+            if (Profile == null || !_prototypeManager.HasIndex<SpeciesPrototype>(Profile.Species))
+                return;
+
+            PreviewDummy = _controller.LoadProfileEntity(Profile, JobOverride, ShowClothes.Pressed);
+            SpriteView.SetEntity(PreviewDummy);
+        }
+
+        /// <summary>
+        /// Resets the profile to the defaults.
+        /// </summary>
+        public void ResetToDefault()
+        {
+            SetProfile(
+                (HumanoidCharacterProfile?) _preferencesManager.Preferences?.SelectedCharacter,
+                _preferencesManager.Preferences?.SelectedCharacterIndex);
+        }
 
+        /// <summary>
+        /// Sets the editor to the specified profile with the specified slot.
+        /// </summary>
+        public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
+        {
+            Profile = profile?.Clone();
+            CharacterSlot = slot;
             IsDirty = false;
-            controller.UpdateProfile();
+            JobOverride = null;
+
+            UpdateNameEdit();
+            UpdateFlavorTextEdit();
+            UpdateSexControls();
+            UpdateGenderControls();
+            UpdateSkinColor();
+            UpdateSpawnPriorityControls();
+            UpdateAgeEdit();
+            UpdateEyePickers();
+            UpdateSaveButton();
+            UpdateMarkings();
+            UpdateHairPickers();
+            UpdateCMarkingsHair();
+            UpdateCMarkingsFacialHair();
+
+            RefreshAntags();
+            RefreshJobs();
+            RefreshLoadouts();
+            RefreshSpecies();
+            RefreshTraits();
+            RefreshFlavorText();
+            ReloadPreview();
+
+            if (Profile != null)
+            {
+                PreferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
+            }
         }
 
-        private void SetDirty()
+
+        /// <summary>
+        /// A slim reload that only updates the entity itself and not any of the job entities, etc.
+        /// </summary>
+        private void ReloadProfilePreview()
         {
-            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-            controller.UpdateProfile(Profile);
-            controller.ReloadCharacterUI();
-            IsDirty = true;
+            if (Profile == null || !_entManager.EntityExists(PreviewDummy))
+                return;
+
+            _entManager.System<HumanoidAppearanceSystem>().LoadProfile(PreviewDummy, Profile);
         }
 
         private void OnSpeciesInfoButtonPressed(BaseButton.ButtonEventArgs args)
@@ -478,53 +709,27 @@ namespace Content.Client.Preferences.UI
             }
         }
 
-        private void OnDummyUpdate(EntityUid value)
-        {
-            _previewSpriteView.SetEntity(value);
-        }
-
-        private void UpdateAntagRequirements()
+        /// <summary>
+        /// Refreshes all job selectors.
+        /// </summary>
+        public void RefreshJobs()
         {
-            _antagList.DisposeAllChildren();
-            _antagPreferences.Clear();
-            var btnGroup = new ButtonGroup();
-
-            foreach (var antag in _prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
-            {
-                if (!antag.SetPreference)
-                    continue;
-
-                var selector = new AntagPreferenceSelector(antag, btnGroup)
-                {
-                    Margin = new Thickness(3f, 3f, 3f, 0f),
-                };
-                _antagList.AddChild(selector);
-                _antagPreferences.Add(selector);
-                if (selector.Disabled)
-                {
-                    Profile = Profile?.WithAntagPreference(antag.ID, false);
-                    SetDirty();
-                }
-
-                selector.PreferenceChanged += preference =>
-                {
-                    Profile = Profile?.WithAntagPreference(antag.ID, preference);
-                    SetDirty();
-                };
-            }
-
-        }
-
-        private void UpdateRoleRequirements()
-        {
-            _jobList.DisposeAllChildren();
-            _jobPriorities.Clear();
+            JobList.DisposeAllChildren();
             _jobCategories.Clear();
+            _jobPriorities.Clear();
             var firstCategory = true;
 
             var departments = _prototypeManager.EnumeratePrototypes<DepartmentPrototype>().ToArray();
             Array.Sort(departments, DepartmentUIComparer.Instance);
 
+            var items = new[]
+            {
+                ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
+                ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
+                ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
+                ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
+            };
+
             foreach (var department in departments)
             {
                 var departmentName = Loc.GetString($"department-{department.ID}");
@@ -566,70 +771,167 @@ namespace Content.Client.Preferences.UI
                     });
 
                     _jobCategories[department.ID] = category;
-                    _jobList.AddChild(category);
+                    JobList.AddChild(category);
                 }
 
                 var jobs = department.Roles.Select(jobId => _prototypeManager.Index<JobPrototype>(jobId))
                     .Where(job => job.SetPreference)
                     .ToArray();
+
                 Array.Sort(jobs, JobUIComparer.Instance);
-                var jobLoadoutGroup = new ButtonGroup();
 
                 foreach (var job in jobs)
                 {
-                    RoleLoadout? loadout = null;
+                    var jobContainer = new BoxContainer()
+                    {
+                        Orientation = LayoutOrientation.Horizontal,
+                    };
 
-                    // Clone so we don't modify the underlying loadout.
-                    Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
-                    loadout = loadout?.Clone();
-                    var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
+                    var selector = new RequirementsSelector()
                     {
                         Margin = new Thickness(3f, 3f, 3f, 0f),
                     };
 
+                    var icon = new TextureRect
+                    {
+                        TextureScale = new Vector2(2, 2),
+                        VerticalAlignment = VAlignment.Center
+                    };
+                    var jobIcon = _prototypeManager.Index<StatusIconPrototype>(job.Icon);
+                    icon.Texture = jobIcon.Icon.Frame0();
+                    selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
+
                     if (!_requirements.IsAllowed(job, out var reason))
                     {
                         selector.LockRequirements(reason);
                     }
-
-                    category.AddChild(selector);
-                    _jobPriorities.Add(selector);
-
-                    selector.LoadoutUpdated += args =>
+                    else
                     {
-                        Profile = Profile?.WithLoadout(args);
-                        SetDirty();
-                    };
+                        selector.UnlockRequirements();
+                    }
 
-                    selector.PriorityChanged += priority =>
+                    selector.OnSelected += selectedPrio =>
                     {
-                        Profile = Profile?.WithJobPriority(job.ID, priority);
+                        var selectedJobPrio = (JobPriority) selectedPrio;
+                        Profile = Profile?.WithJobPriority(job.ID, selectedJobPrio);
 
-                        foreach (var jobSelector in _jobPriorities)
+                        foreach (var (jobId, other) in _jobPriorities)
                         {
                             // Sync other selectors with the same job in case of multiple department jobs
-                            if (jobSelector.Proto == selector.Proto)
+                            if (jobId == job.ID)
                             {
-                                jobSelector.Priority = priority;
+                                other.Select(selectedPrio);
                             }
-                            else if (priority == JobPriority.High && jobSelector.Priority == JobPriority.High)
+                            else if (selectedJobPrio == JobPriority.High && (JobPriority) other.Selected == JobPriority.High)
                             {
                                 // Lower any other high priorities to medium.
-                                jobSelector.Priority = JobPriority.Medium;
-                                Profile = Profile?.WithJobPriority(jobSelector.Proto.ID, JobPriority.Medium);
+                                other.Select((int) JobPriority.Medium);
+                                Profile = Profile?.WithJobPriority(jobId, JobPriority.Medium);
                             }
                         }
 
+                        // TODO: Only reload on high change (either to or from).
+                        ReloadPreview();
+
+                        UpdateJobPriorities();
                         SetDirty();
                     };
 
+                    var loadoutWindowBtn = new Button()
+                    {
+                        Text = Loc.GetString("loadout-window"),
+                        HorizontalAlignment = HAlignment.Right,
+                        VerticalAlignment = VAlignment.Center,
+                        Margin = new Thickness(3f, 3f, 0f, 0f),
+                    };
+
+                    var collection = IoCManager.Instance!;
+                    var protoManager = collection.Resolve<IPrototypeManager>();
+
+                    // If no loadout found then disabled button
+                    if (!protoManager.TryIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID), out var roleLoadoutProto))
+                    {
+                        loadoutWindowBtn.Disabled = true;
+                    }
+                    // else
+                    else
+                    {
+                        loadoutWindowBtn.OnPressed += args =>
+                        {
+                            RoleLoadout? loadout = null;
+
+                            // Clone so we don't modify the underlying loadout.
+                            Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
+                            loadout = loadout?.Clone();
+
+                            if (loadout == null)
+                            {
+                                loadout = new RoleLoadout(roleLoadoutProto.ID);
+                                loadout.SetDefault(_prototypeManager);
+                            }
+
+                            OpenLoadout(job, loadout, roleLoadoutProto);
+                        };
+                    }
+
+                    _jobPriorities.Add((job.ID, selector));
+                    jobContainer.AddChild(selector);
+                    jobContainer.AddChild(loadoutWindowBtn);
+                    category.AddChild(jobContainer);
                 }
             }
 
-            if (Profile is not null)
+            UpdateJobPriorities();
+        }
+
+        private void OpenLoadout(JobPrototype? jobProto, RoleLoadout roleLoadout, RoleLoadoutPrototype roleLoadoutProto)
+        {
+            _loadoutWindow?.Dispose();
+            _loadoutWindow = null;
+            var collection = IoCManager.Instance;
+
+            if (collection == null || _playerManager.LocalSession == null || Profile == null)
+                return;
+
+            JobOverride = jobProto;
+            var session = _playerManager.LocalSession;
+
+            _loadoutWindow = new LoadoutWindow(Profile, roleLoadout, roleLoadoutProto, _playerManager.LocalSession, collection)
             {
-                UpdateJobPriorities();
-            }
+                Title = jobProto?.ID + "-loadout",
+            };
+
+            // Refresh the buttons etc.
+            _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
+            _loadoutWindow.OpenCenteredLeft();
+
+            _loadoutWindow.OnLoadoutPressed += (loadoutGroup, loadoutProto) =>
+            {
+                roleLoadout.AddLoadout(loadoutGroup, loadoutProto, _prototypeManager);
+                _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
+                Profile = Profile?.WithLoadout(roleLoadout);
+                SetDirty();
+                ReloadPreview();
+            };
+
+            _loadoutWindow.OnLoadoutUnpressed += (loadoutGroup, loadoutProto) =>
+            {
+                roleLoadout.RemoveLoadout(loadoutGroup, loadoutProto, _prototypeManager);
+                _loadoutWindow.RefreshLoadouts(roleLoadout, session, collection);
+                Profile = Profile?.WithLoadout(roleLoadout);
+                SetDirty();
+                ReloadPreview();
+            };
+
+            JobOverride = jobProto;
+            ReloadPreview();
+
+            _loadoutWindow.OnClose += () =>
+            {
+                JobOverride = null;
+                SetDirty();
+                ReloadPreview();
+            };
         }
 
         private void OnFlavorTextChange(string content)
@@ -647,10 +949,8 @@ namespace Content.Client.Preferences.UI
                 return;
 
             Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
-            IsDirty = true;
-            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-            controller.UpdateProfile(Profile);
-            controller.ReloadProfile();
+            SetDirty();
+            ReloadProfilePreview();
         }
 
         private void OnSkinColorOnValueChanged()
@@ -663,64 +963,62 @@ namespace Content.Client.Preferences.UI
             {
                 case HumanoidSkinColor.HumanToned:
                 {
-                    if (!_skinColor.Visible)
+                    if (!Skin.Visible)
                     {
-                        _skinColor.Visible = true;
-                        _rgbSkinColorContainer.Visible = false;
+                        Skin.Visible = true;
+                        RgbSkinColorContainer.Visible = false;
                     }
 
-                    var color = SkinColor.HumanSkinTone((int) _skinColor.Value);
+                    var color = SkinColor.HumanSkinTone((int) Skin.Value);
 
-                    CMarkings.CurrentSkinColor = color;
+                    Markings.CurrentSkinColor = color;
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));//
                     break;
                 }
                 case HumanoidSkinColor.Hues:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
-                    CMarkings.CurrentSkinColor = _rgbSkinColorSelector.Color;
+                    Markings.CurrentSkinColor = _rgbSkinColorSelector.Color;
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(_rgbSkinColorSelector.Color));
                     break;
                 }
                 case HumanoidSkinColor.TintedHues:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
                     var color = SkinColor.TintedHues(_rgbSkinColorSelector.Color);
 
-                    CMarkings.CurrentSkinColor = color;
+                    Markings.CurrentSkinColor = color;
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
                     break;
                 }
                 case HumanoidSkinColor.VoxFeathers:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
                     var color = SkinColor.ClosestVoxColor(_rgbSkinColorSelector.Color);
 
-                    CMarkings.CurrentSkinColor = color;
+                    Markings.CurrentSkinColor = color;
                     Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithSkinColor(color));
                     break;
                 }
             }
 
-            IsDirty = true;
-            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-            controller.UpdateProfile(Profile);
-            controller.ReloadProfile();
+            SetDirty();
+            ReloadProfilePreview();
         }
 
         protected override void Dispose(bool disposing)
@@ -729,26 +1027,16 @@ namespace Content.Client.Preferences.UI
             if (!disposing)
                 return;
 
-            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-            controller.PreviewDummyUpdated -= OnDummyUpdate;
-            _requirements.Updated -= UpdateAntagRequirements;
-            _requirements.Updated -= UpdateRoleRequirements;
-            _preferencesManager.OnServerDataLoaded -= LoadServerData;
-        }
-
-        public void LoadServerData()
-        {
-            Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
-            CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
-
-            UpdateAntagRequirements();
-            UpdateControls();
-            ShowClothes.Pressed = true;
+            _loadoutWindow?.Dispose();
+            _loadoutWindow = null;
+            _entManager.DeleteEntity(PreviewDummy);
+            PreviewDummy = EntityUid.Invalid;
         }
 
         private void SetAge(int newAge)
         {
             Profile = Profile?.WithAge(newAge);
+            ReloadPreview();
             SetDirty();
         }
 
@@ -768,14 +1056,17 @@ namespace Content.Client.Preferences.UI
                     Profile = Profile?.WithGender(Gender.Epicene);
                     break;
             }
+
             UpdateGenderControls();
-            CMarkings.SetSex(newSex);
+            Markings.SetSex(newSex);
+            ReloadPreview();
             SetDirty();
         }
 
         private void SetGender(Gender newGender)
         {
             Profile = Profile?.WithGender(newGender);
+            ReloadPreview();
             SetDirty();
         }
 
@@ -783,11 +1074,15 @@ namespace Content.Client.Preferences.UI
         {
             Profile = Profile?.WithSpecies(newSpecies);
             OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
-            CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
+            Markings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
+            // In case there's job restrictions for the species
+            RefreshJobs();
+            // In case there's species restrictions for loadouts
+            RefreshLoadouts();
             UpdateSexControls(); // update sex for new species
             UpdateSpeciesGuidebookIcon();
             SetDirty();
-            UpdatePreview();
+            ReloadPreview();
         }
 
         private void SetName(string newName)
@@ -802,24 +1097,14 @@ namespace Content.Client.Preferences.UI
             SetDirty();
         }
 
-        public void Save()
-        {
-            IsDirty = false;
-
-            if (Profile == null)
-                return;
-
-            _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
-            OnProfileChanged?.Invoke(Profile, CharacterSlot);
-            // Reset profile to default.
-            UserInterfaceManager.GetUIController<LobbyUIController>().UpdateProfile();
-        }
-
         private bool IsDirty
         {
             get => _isDirty;
             set
             {
+                if (_isDirty == value)
+                    return;
+
                 _isDirty = value;
                 UpdateSaveButton();
             }
@@ -827,12 +1112,12 @@ namespace Content.Client.Preferences.UI
 
         private void UpdateNameEdit()
         {
-            _nameEdit.Text = Profile?.Name ?? "";
+            NameEdit.Text = Profile?.Name ?? "";
         }
 
         private void UpdateFlavorTextEdit()
         {
-            if(_flavorTextEdit != null)
+            if (_flavorTextEdit != null)
             {
                 _flavorTextEdit.TextRope = new Rope.Leaf(Profile?.FlavorText ?? "");
             }
@@ -840,7 +1125,19 @@ namespace Content.Client.Preferences.UI
 
         private void UpdateAgeEdit()
         {
-            _ageEdit.Text = Profile?.Age.ToString() ?? "";
+            AgeEdit.Text = Profile?.Age.ToString() ?? "";
+        }
+
+        /// <summary>
+        /// Updates selected job priorities to the profile's.
+        /// </summary>
+        private void UpdateJobPriorities()
+        {
+            foreach (var (jobId, prioritySelector) in _jobPriorities)
+            {
+                var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
+                prioritySelector.Select((int) priority);
+            }
         }
 
         private void UpdateSexControls()
@@ -848,7 +1145,7 @@ namespace Content.Client.Preferences.UI
             if (Profile == null)
                 return;
 
-            _sexButton.Clear();
+            SexButton.Clear();
 
             var sexes = new List<Sex>();
 
@@ -859,7 +1156,8 @@ namespace Content.Client.Preferences.UI
                 {
                     sexes.Add(sex);
                 }
-            } else
+            }
+            else
             {
                 sexes.Add(Sex.Unsexed);
             }
@@ -867,13 +1165,13 @@ namespace Content.Client.Preferences.UI
             // add button for each sex
             foreach (var sex in sexes)
             {
-                _sexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex);
+                SexButton.AddItem(Loc.GetString($"humanoid-profile-editor-sex-{sex.ToString().ToLower()}-text"), (int) sex);
             }
 
             if (sexes.Contains(Profile.Sex))
-                _sexButton.SelectId((int) Profile.Sex);
+                SexButton.SelectId((int) Profile.Sex);
             else
-                _sexButton.SelectId((int) sexes[0]);
+                SexButton.SelectId((int) sexes[0]);
         }
 
         private void UpdateSkinColor()
@@ -887,22 +1185,22 @@ namespace Content.Client.Preferences.UI
             {
                 case HumanoidSkinColor.HumanToned:
                 {
-                    if (!_skinColor.Visible)
+                    if (!Skin.Visible)
                     {
-                        _skinColor.Visible = true;
-                        _rgbSkinColorContainer.Visible = false;
+                        Skin.Visible = true;
+                        RgbSkinColorContainer.Visible = false;
                     }
 
-                    _skinColor.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
+                    Skin.Value = SkinColor.HumanSkinToneFromColor(Profile.Appearance.SkinColor);
 
                     break;
                 }
                 case HumanoidSkinColor.Hues:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
                     // set the RGB values to the direct values otherwise
@@ -911,10 +1209,10 @@ namespace Content.Client.Preferences.UI
                 }
                 case HumanoidSkinColor.TintedHues:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
                     // set the RGB values to the direct values otherwise
@@ -923,10 +1221,10 @@ namespace Content.Client.Preferences.UI
                 }
                 case HumanoidSkinColor.VoxFeathers:
                 {
-                    if (!_rgbSkinColorContainer.Visible)
+                    if (!RgbSkinColorContainer.Visible)
                     {
-                        _skinColor.Visible = false;
-                        _rgbSkinColorContainer.Visible = true;
+                        Skin.Visible = false;
+                        RgbSkinColorContainer.Visible = true;
                     }
 
                     _rgbSkinColorSelector.Color = SkinColor.ClosestVoxColor(Profile.Appearance.SkinColor);
@@ -963,21 +1261,11 @@ namespace Content.Client.Preferences.UI
                 return;
             }
 
-            CMarkings.SetData(Profile.Appearance.Markings, Profile.Species,
+            Markings.SetData(Profile.Appearance.Markings, Profile.Species,
                 Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor
             );
         }
 
-        private void UpdateSpecies()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-
-            CSpeciesButton.Select(_speciesList.FindIndex(x => x.ID == Profile.Species));
-        }
-
         private void UpdateGenderControls()
         {
             if (Profile == null)
@@ -985,7 +1273,7 @@ namespace Content.Client.Preferences.UI
                 return;
             }
 
-            _genderButton.SelectId((int) Profile.Gender);
+            PronounsButton.SelectId((int) Profile.Gender);
         }
 
         private void UpdateSpawnPriorityControls()
@@ -995,7 +1283,7 @@ namespace Content.Client.Preferences.UI
                 return;
             }
 
-            _spawnPriorityButton.SelectId((int) Profile.SpawnPriority);
+            SpawnPriorityButton.SelectId((int) Profile.SpawnPriority);
         }
 
         private void UpdateHairPickers()
@@ -1016,11 +1304,11 @@ namespace Content.Client.Preferences.UI
                 _ => new() { new(Profile.Appearance.FacialHairStyleId, new List<Color>() { Profile.Appearance.FacialHairColor }) },
             };
 
-            _hairPicker.UpdateData(
+            HairStylePicker.UpdateData(
                 hairMarking,
                 Profile.Species,
                 1);
-            _facialHairPicker.UpdateData(
+            FacialHairPicker.UpdateData(
                 facialHairMarking,
                 Profile.Species,
                 1);
@@ -1053,11 +1341,11 @@ namespace Content.Client.Preferences.UI
             }
             if (hairColor != null)
             {
-                CMarkings.HairMarking = new (Profile.Appearance.HairStyleId, new List<Color>() { hairColor.Value });
+                Markings.HairMarking = new (Profile.Appearance.HairStyleId, new List<Color>() { hairColor.Value });
             }
             else
             {
-                CMarkings.HairMarking = null;
+                Markings.HairMarking = null;
             }
         }
 
@@ -1071,8 +1359,7 @@ namespace Content.Client.Preferences.UI
             // facial hair color
             Color? facialHairColor = null;
             if ( Profile.Appearance.FacialHairStyleId != HairStyles.DefaultFacialHairStyle &&
-                _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto)
-            )
+                _markingManager.Markings.TryGetValue(Profile.Appearance.FacialHairStyleId, out var facialHairProto))
             {
                 if (_markingManager.CanBeApplied(Profile.Species, Profile.Sex, facialHairProto, _prototypeManager))
                 {
@@ -1088,11 +1375,11 @@ namespace Content.Client.Preferences.UI
             }
             if (facialHairColor != null)
             {
-                CMarkings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<Color>() { facialHairColor.Value });
+                Markings.FacialHairMarking = new (Profile.Appearance.FacialHairStyleId, new List<Color>() { facialHairColor.Value });
             }
             else
             {
-                CMarkings.FacialHairMarking = null;
+                Markings.FacialHairMarking = null;
             }
         }
 
@@ -1103,132 +1390,111 @@ namespace Content.Client.Preferences.UI
                 return;
             }
 
-            CMarkings.CurrentEyeColor = Profile.Appearance.EyeColor;
-            _eyesPicker.SetData(Profile.Appearance.EyeColor);
+            Markings.CurrentEyeColor = Profile.Appearance.EyeColor;
+            EyeColorPicker.SetData(Profile.Appearance.EyeColor);
         }
 
         private void UpdateSaveButton()
         {
-            _saveButton.Disabled = Profile is null || !IsDirty;
+            SaveButton.Disabled = Profile is null || !IsDirty;
+            ResetButton.Disabled = Profile is null || !IsDirty;
         }
 
-        private void UpdatePreview()
+        private void SetPreviewRotation(Direction direction)
         {
-            if (Profile is null)
-                return;
-
-            UserInterfaceManager.GetUIController<LobbyUIController>().ReloadProfile();
-            SetPreviewRotation(_previewRotation);
+            SpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
         }
 
-        private void SetPreviewRotation(Direction direction)
+        private void RandomizeEverything()
         {
-            _previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
+            Profile = HumanoidCharacterProfile.Random();
+            SetProfile(Profile, CharacterSlot);
+            SetDirty();
         }
 
-        public void UpdateControls()
+        private void RandomizeName()
         {
-            if (Profile is null) return;
+            if (Profile == null) return;
+            var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
+            SetName(name);
             UpdateNameEdit();
-            UpdateFlavorTextEdit();
-            UpdateSexControls();
-            UpdateGenderControls();
-            UpdateSkinColor();
-            UpdateSpecies();
-            UpdateSpawnPriorityControls();
-            UpdateAgeEdit();
-            UpdateEyePickers();
-            UpdateSaveButton();
-            UpdateLoadouts();
-            UpdateRoleRequirements();
-            UpdateJobPriorities();
-            UpdateAntagPreferences();
-            UpdateTraitPreferences();
-            UpdateMarkings();
-            UpdateHairPickers();
-            UpdateCMarkingsHair();
-            UpdateCMarkingsFacialHair();
-
-            _preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
         }
 
-        private void UpdateJobPriorities()
+        private async void ImportProfile()
         {
-            foreach (var prioritySelector in _jobPriorities)
-            {
-                var jobId = prioritySelector.Proto.ID;
+            if (_exporting || CharacterSlot == null || Profile == null)
+                return;
 
-                var priority = Profile?.JobPriorities.GetValueOrDefault(jobId, JobPriority.Never) ?? JobPriority.Never;
+            StartExport();
+            await using var file = await _dialogManager.OpenFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
 
-                prioritySelector.Priority = priority;
+            if (file == null)
+            {
+                EndExport();
+                return;
             }
-        }
 
-        private void UpdateLoadouts()
-        {
-            foreach (var prioritySelector in _jobPriorities)
+            try
             {
-                prioritySelector.CloseLoadout();
-            }
-        }
+                var profile = _entManager.System<HumanoidAppearanceSystem>().FromStream(file, _playerManager.LocalSession!);
+                var oldProfile = Profile;
+                SetProfile(profile, CharacterSlot);
 
-        private void UpdateAntagPreferences()
-        {
-            foreach (var preferenceSelector in _antagPreferences)
+                IsDirty = !profile.MemberwiseEquals(oldProfile);
+            }
+            catch (Exception exc)
             {
-                var antagId = preferenceSelector.Proto.ID;
-                var preference = Profile?.AntagPreferences.Contains(antagId) ?? false;
-                preferenceSelector.Preference = preference;
+                _sawmill.Error($"Error when importing profile\n{exc.StackTrace}");
             }
-        }
-
-        private void UpdateTraitPreferences()
-        {
-            foreach (var preferenceSelector in _traitPreferences)
+            finally
             {
-                var traitId = preferenceSelector.Trait.ID;
-                var preference = Profile?.TraitPreferences.Contains(traitId) ?? false;
-
-                preferenceSelector.Preference = preference;
+                EndExport();
             }
         }
 
-        private sealed class TraitPreferenceSelector : Control
+        private async void ExportProfile()
         {
-            public TraitPrototype Trait { get; }
-            private readonly CheckBox _checkBox;
+            if (Profile == null || _exporting)
+                return;
 
-            public bool Preference
+            StartExport();
+            var file = await _dialogManager.SaveFile(new FileDialogFilters(new FileDialogFilters.Group("yml")));
+
+            if (file == null)
             {
-                get => _checkBox.Pressed;
-                set => _checkBox.Pressed = value;
+                EndExport();
+                return;
             }
 
-            public event Action<bool>? PreferenceChanged;
-
-            public TraitPreferenceSelector(TraitPrototype trait)
+            try
             {
-                Trait = trait;
-
-                _checkBox = new CheckBox {Text = Loc.GetString(trait.Name)};
-                _checkBox.OnToggled += OnCheckBoxToggled;
-
-                if (trait.Description is { } desc)
-                {
-                    _checkBox.ToolTip = Loc.GetString(desc);
-                }
-
-                AddChild(new BoxContainer
-                {
-                    Orientation = LayoutOrientation.Horizontal,
-                    Children = { _checkBox },
-                });
+                var dataNode = _entManager.System<HumanoidAppearanceSystem>().ToDataNode(Profile);
+                await using var writer = new StreamWriter(file.Value.fileStream);
+                dataNode.Write(writer);
             }
-
-            private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
+            catch (Exception exc)
             {
-                PreferenceChanged?.Invoke(Preference);
+                _sawmill.Error($"Error when exporting profile\n{exc.StackTrace}");
             }
+            finally
+            {
+                EndExport();
+                await file.Value.fileStream.DisposeAsync();
+            }
+        }
+
+        private void StartExport()
+        {
+            _exporting = true;
+            ImportButton.Disabled = true;
+            ExportButton.Disabled = true;
+        }
+
+        private void EndExport()
+        {
+            _exporting = false;
+            ImportButton.Disabled = false;
+            ExportButton.Disabled = false;
         }
     }
 }
similarity index 95%
rename from Content.Client/Preferences/UI/LoadoutContainer.xaml.cs
rename to Content.Client/Lobby/UI/Loadouts/LoadoutContainer.xaml.cs
index 45a982b5a8978d5188b0d7f83779a052b27be4d2..36f0772d784e5db6c0da0e62821d8236fdeb30cb 100644 (file)
@@ -8,7 +8,7 @@ using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
-namespace Content.Client.Preferences.UI;
+namespace Content.Client.Lobby.UI.Loadouts;
 
 [GenerateTypedNameReferences]
 public sealed partial class LoadoutContainer : BoxContainer
@@ -45,7 +45,7 @@ public sealed partial class LoadoutContainer : BoxContainer
 
                 var spriteTooltip = new Tooltip();
                 spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
-                Sprite.TooltipSupplier = _ => spriteTooltip;
+                TooltipSupplier = _ => spriteTooltip;
             }
         }
     }
similarity index 84%
rename from Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs
rename to Content.Client/Lobby/UI/Loadouts/LoadoutGroupContainer.xaml.cs
index 8dc1c405394f410c77d07cb9a4868b966eae02dc..bc7cfc7f481ea6aad7bc579d9b9c11dc6b538e44 100644 (file)
@@ -1,5 +1,6 @@
 using System.Linq;
 using Content.Shared.Clothing;
+using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.Controls;
@@ -7,7 +8,7 @@ using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 
-namespace Content.Client.Preferences.UI;
+namespace Content.Client.Lobby.UI.Loadouts;
 
 [GenerateTypedNameReferences]
 public sealed partial class LoadoutGroupContainer : BoxContainer
@@ -17,18 +18,18 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
     public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
     public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
 
-    public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
+    public LoadoutGroupContainer(HumanoidCharacterProfile profile, RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
     {
         RobustXamlLoader.Load(this);
         _groupProto = groupProto;
 
-        RefreshLoadouts(loadout, session, collection);
+        RefreshLoadouts(profile, loadout, session, collection);
     }
 
     /// <summary>
     /// Updates button availabilities and buttons.
     /// </summary>
-    public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
+    public void RefreshLoadouts(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
     {
         var protoMan = collection.Resolve<IPrototypeManager>();
         var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
@@ -74,7 +75,7 @@ public sealed partial class LoadoutGroupContainer : BoxContainer
             var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
             var pressed = matchingLoadout != null;
 
-            var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
+            var enabled = loadout.IsValid(profile, session, loadoutProto, collection, out var reason);
             var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
             loadoutContainer.Select.Pressed = pressed;
             loadoutContainer.Text = loadoutSystem.GetName(loadProto);
similarity index 69%
rename from Content.Client/Preferences/UI/LoadoutWindow.xaml.cs
rename to Content.Client/Lobby/UI/Loadouts/LoadoutWindow.xaml.cs
index 8e1ef0f1697702671ebaa890f7c808dc82f72dc6..2737eef1f1ab02eb76296377003025b40fdca2b3 100644 (file)
@@ -1,13 +1,12 @@
-using Content.Client.Lobby;
 using Content.Client.UserInterface.Controls;
+using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 
-namespace Content.Client.Preferences.UI;
+namespace Content.Client.Lobby.UI.Loadouts;
 
 [GenerateTypedNameReferences]
 public sealed partial class LoadoutWindow : FancyWindow
@@ -17,9 +16,12 @@ public sealed partial class LoadoutWindow : FancyWindow
 
     private List<LoadoutGroupContainer> _groups = new();
 
-    public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
+    public HumanoidCharacterProfile Profile;
+
+    public LoadoutWindow(HumanoidCharacterProfile profile, RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
     {
         RobustXamlLoader.Load(this);
+        Profile = profile;
         var protoManager = collection.Resolve<IPrototypeManager>();
 
         foreach (var group in proto.Groups)
@@ -27,7 +29,7 @@ public sealed partial class LoadoutWindow : FancyWindow
             if (!protoManager.TryIndex(group, out var groupProto))
                 continue;
 
-            var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
+            var container = new LoadoutGroupContainer(profile, loadout, protoManager.Index(group), session, collection);
             LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
             _groups.Add(container);
 
@@ -43,18 +45,11 @@ public sealed partial class LoadoutWindow : FancyWindow
         }
     }
 
-    public override void Close()
-    {
-        base.Close();
-        var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-        controller.SetDummyJob(null);
-    }
-
     public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
     {
         foreach (var group in _groups)
         {
-            group.RefreshLoadouts(loadout, session, collection);
+            group.RefreshLoadouts(Profile, loadout, session, collection);
         }
     }
 }
index b0dcbc25fdb6c013e0a12772f828bc6e92805ffe..14709f8b1f017f4a9325e4282b17469ab4ffa75a 100644 (file)
@@ -10,12 +10,16 @@ namespace Content.Client.Lobby.UI;
 [GenerateTypedNameReferences]
 public sealed partial class LobbyCharacterPreviewPanel : Control
 {
+    [Dependency] private readonly IEntityManager _entManager = default!;
+
     public Button CharacterSetupButton => CharacterSetup;
 
+    private EntityUid? _previewDummy;
+
     public LobbyCharacterPreviewPanel()
     {
         RobustXamlLoader.Load(this);
-        UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this);
+        IoCManager.InjectDependencies(this);
     }
 
     public void SetLoaded(bool value)
@@ -26,11 +30,18 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
 
     public void SetSummaryText(string value)
     {
-        Summary.Text = string.Empty;
+        Summary.Text = value;
     }
 
     public void SetSprite(EntityUid uid)
     {
+        if (_previewDummy != null)
+        {
+            _entManager.DeleteEntity(_previewDummy);
+        }
+
+        _previewDummy = uid;
+
         ViewBox.DisposeAllChildren();
         var spriteView = new SpriteView
         {
@@ -42,4 +53,11 @@ public sealed partial class LobbyCharacterPreviewPanel : Control
         spriteView.SetEntity(uid);
         ViewBox.AddChild(spriteView);
     }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        _entManager.DeleteEntity(_previewDummy);
+        _previewDummy = null;
+    }
 }
index 5a0b580262ee178d4b091987c251c02f201fe6d8..6471edb6f3792cdade8a20a29084bd3d0b0d5961 100644 (file)
@@ -8,10 +8,9 @@ using Robust.Client.UserInterface.XAML;
 namespace Content.Client.Lobby.UI
 {
     [GenerateTypedNameReferences]
-    internal sealed partial class LobbyGui : UIScreen
+    public sealed partial class LobbyGui : UIScreen
     {
         [Dependency] private readonly IClientConsoleHost _consoleHost = default!;
-        [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
 
         public LobbyGui()
         {
@@ -23,7 +22,7 @@ namespace Content.Client.Lobby.UI
             LobbySong.SetMarkup(Loc.GetString("lobby-state-song-no-song-text"));
 
             LeaveButton.OnPressed += _ => _consoleHost.ExecuteCommand("disconnect");
-            OptionsButton.OnPressed += _ => _userInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
+            OptionsButton.OnPressed += _ => UserInterfaceManager.GetUIController<OptionsUIController>().ToggleWindow();
         }
 
         public void SwitchState(LobbyGuiState state)
@@ -40,7 +39,7 @@ namespace Content.Client.Lobby.UI
                 case LobbyGuiState.CharacterSetup:
                     CharacterSetupState.Visible = true;
 
-                    var actualWidth = (float) _userInterfaceManager.RootControl.PixelWidth;
+                    var actualWidth = (float) UserInterfaceManager.RootControl.PixelWidth;
                     var setupWidth = (float) LeftSide.PixelWidth;
 
                     if (1 - (setupWidth / actualWidth) > 0.30)
@@ -48,6 +47,8 @@ namespace Content.Client.Lobby.UI
                         RightSide.Visible = false;
                     }
 
+                    UserInterfaceManager.GetUIController<LobbyUIController>().ReloadCharacterSetup();
+
                     break;
             }
         }
index 2d37cb97df63cd80442fb78f19f9d8c9c131a48e..718f40b2aa064561bfe5ee8832805ff5847566cd 100644 (file)
@@ -2,23 +2,20 @@ using JetBrains.Annotations;
 using Robust.Client.AutoGenerated;
 using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
 
-namespace Content.Client.Lobby.UI
+namespace Content.Client.Lobby.UI;
+
+[GenerateTypedNameReferences]
+[UsedImplicitly]
+public sealed partial class ObserveWarningWindow : DefaultWindow
 {
-    [GenerateTypedNameReferences]
-    [UsedImplicitly]
-    internal sealed partial class ObserveWarningWindow : DefaultWindow
+    public ObserveWarningWindow()
     {
-        public ObserveWarningWindow()
-        {
-            Title = Loc.GetString("observe-warning-window-title");
-            RobustXamlLoader.Load(this);
-            IoCManager.InjectDependencies(this);
+        Title = Loc.GetString("observe-warning-window-title");
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
 
-            ObserveButton.OnPressed += _ => { this.Close(); };
-            NevermindButton.OnPressed += _ => { this.Close(); };
-        }
+        ObserveButton.OnPressed += _ => { this.Close(); };
+        NevermindButton.OnPressed += _ => { this.Close(); };
     }
 }
diff --git a/Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml b/Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml
new file mode 100644 (file)
index 0000000..88bf49b
--- /dev/null
@@ -0,0 +1,9 @@
+<BoxContainer xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         Orientation="Horizontal">
+        <Label Name="TitleLabel"
+                      Margin="5 0"
+                      MouseFilter="Stop"/>
+        <BoxContainer Name="OptionsContainer"
+                      SetWidth="400"/>
+</BoxContainer>
diff --git a/Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml.cs b/Content.Client/Lobby/UI/Roles/RequirementsSelector.xaml.cs
new file mode 100644 (file)
index 0000000..8b4b21b
--- /dev/null
@@ -0,0 +1,118 @@
+using System.Numerics;
+using Content.Client.Stylesheets;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Lobby.UI.Roles;
+
+/// <summary>
+/// A generic locking selector.
+/// </summary>
+[GenerateTypedNameReferences]
+public sealed partial class RequirementsSelector : BoxContainer
+{
+    private readonly RadioOptions<int> _options;
+    private readonly StripeBack _lockStripe;
+
+    public event Action<int>? OnSelected;
+
+    public int Selected => _options.SelectedId;
+
+    public RequirementsSelector()
+    {
+        RobustXamlLoader.Load(this);
+        _options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
+        {
+            FirstButtonStyle = StyleBase.ButtonOpenRight,
+            ButtonStyle = StyleBase.ButtonOpenBoth,
+            LastButtonStyle = StyleBase.ButtonOpenLeft,
+            HorizontalExpand = true,
+        };
+        //Override default radio option button width
+        _options.GenerateItem = GenerateButton;
+
+        _options.OnItemSelected += args =>
+        {
+            _options.Select(args.Id);
+            OnSelected?.Invoke(args.Id);
+        };
+
+        var requirementsLabel = new Label()
+        {
+            Text = Loc.GetString("role-timer-locked"),
+            Visible = true,
+            HorizontalAlignment = HAlignment.Center,
+            StyleClasses = {StyleBase.StyleClassLabelSubText},
+        };
+
+        _lockStripe = new StripeBack()
+        {
+            Visible = false,
+            HorizontalExpand = true,
+            HasMargins = false,
+            MouseFilter = MouseFilterMode.Stop,
+            Children =
+            {
+                requirementsLabel
+            }
+        };
+    }
+
+    /// <summary>
+    /// Actually adds the controls.
+    /// </summary>
+    public void Setup((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
+    {
+        foreach (var (text, value) in items)
+        {
+            _options.AddItem(Loc.GetString(text), value);
+        }
+
+        TitleLabel.Text = title;
+        TitleLabel.MinSize = new Vector2(titleSize, 0f);
+        TitleLabel.ToolTip = description;
+
+        if (icon != null)
+        {
+            AddChild(icon);
+            icon.SetPositionFirst();
+        }
+
+        OptionsContainer.AddChild(_options);
+        OptionsContainer.AddChild(_lockStripe);
+    }
+
+    public void LockRequirements(FormattedMessage requirements)
+    {
+        var tooltip = new Tooltip();
+        tooltip.SetMessage(requirements);
+        _lockStripe.TooltipSupplier = _ => tooltip;
+        _lockStripe.Visible = true;
+        _options.Visible = false;
+    }
+
+    public void UnlockRequirements()
+    {
+        _lockStripe.Visible = false;
+        _options.Visible = true;
+    }
+
+    private Button GenerateButton(string text, int value)
+    {
+        return new Button
+        {
+            Text = text,
+            MinWidth = 90,
+            HorizontalExpand = true,
+        };
+    }
+
+    public void Select(int id)
+    {
+        _options.Select(id);
+    }
+}
diff --git a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml
new file mode 100644 (file)
index 0000000..18dabe3
--- /dev/null
@@ -0,0 +1,7 @@
+<Control xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
+        <BoxContainer Name="Container"
+                      Orientation="Horizontal">
+            <CheckBox Name="Checkbox"/>
+        </BoxContainer>
+</Control>
diff --git a/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs b/Content.Client/Lobby/UI/Roles/TraitPreferenceSelector.xaml.cs
new file mode 100644 (file)
index 0000000..498a5ca
--- /dev/null
@@ -0,0 +1,36 @@
+using Content.Shared.Traits;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lobby.UI.Roles;
+
+[GenerateTypedNameReferences]
+public sealed partial class TraitPreferenceSelector : Control
+{
+    public bool Preference
+    {
+        get => Checkbox.Pressed;
+        set => Checkbox.Pressed = value;
+    }
+
+    public event Action<bool>? PreferenceChanged;
+
+    public TraitPreferenceSelector(TraitPrototype trait)
+    {
+        RobustXamlLoader.Load(this);
+        Checkbox.Text = Loc.GetString(trait.Name);
+        Checkbox.OnToggled += OnCheckBoxToggled;
+
+        if (trait.Description is { } desc)
+        {
+            Checkbox.ToolTip = Loc.GetString(desc);
+        }
+    }
+
+    private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
+    {
+        PreferenceChanged?.Invoke(Preference);
+    }
+}
diff --git a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
deleted file mode 100644 (file)
index 654c393..0000000
+++ /dev/null
@@ -1,41 +0,0 @@
-using Content.Client.Players.PlayTimeTracking;
-using Content.Shared.Roles;
-using Robust.Client.UserInterface.Controls;
-
-namespace Content.Client.Preferences.UI;
-
-public sealed class AntagPreferenceSelector : RequirementsSelector<AntagPrototype>
-{
-    // 0 is yes and 1 is no
-    public bool Preference
-    {
-        get => Options.SelectedValue == 0;
-        set => Options.Select((value && !Disabled) ? 0 : 1);
-    }
-
-    public event Action<bool>? PreferenceChanged;
-
-    public AntagPreferenceSelector(AntagPrototype proto, ButtonGroup btnGroup)
-        : base(proto, btnGroup)
-    {
-        Options.OnItemSelected += args => PreferenceChanged?.Invoke(Preference);
-
-        var items = new[]
-        {
-            ("humanoid-profile-editor-antag-preference-yes-button", 0),
-            ("humanoid-profile-editor-antag-preference-no-button", 1)
-        };
-        var title = Loc.GetString(proto.Name);
-        var description = Loc.GetString(proto.Objective);
-        // Not supported yet get fucked.
-        Setup(null, items, title, 250, description);
-
-        // immediately lock requirements if they arent met.
-        // another function checks Disabled after creating the selector so this has to be done now
-        var requirements = IoCManager.Resolve<JobRequirementsManager>();
-        if (proto.Requirements != null && !requirements.CheckRoleTime(proto.Requirements, out var reason))
-        {
-            LockRequirements(reason);
-        }
-    }
-}
diff --git a/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs b/Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
deleted file mode 100644 (file)
index 8dda022..0000000
+++ /dev/null
@@ -1,276 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.Humanoid;
-using Content.Client.Info;
-using Content.Client.Info.PlaytimeStats;
-using Content.Client.Lobby;
-using Content.Client.Resources;
-using Content.Client.Stylesheets;
-using Content.Shared.Clothing;
-using Content.Shared.Humanoid;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Preferences;
-using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Roles;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Graphics;
-using Robust.Client.ResourceManagement;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.Configuration;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-using Direction = Robust.Shared.Maths.Direction;
-
-namespace Content.Client.Preferences.UI
-{
-    [GenerateTypedNameReferences]
-    public sealed partial class CharacterSetupGui : Control
-    {
-        private readonly IClientPreferencesManager _preferencesManager;
-        private readonly IEntityManager _entityManager;
-        private readonly IPrototypeManager _prototypeManager;
-        private readonly Button _createNewCharacterButton;
-        private readonly HumanoidProfileEditor _humanoidProfileEditor;
-
-        public CharacterSetupGui(
-            IEntityManager entityManager,
-            IResourceCache resourceCache,
-            IClientPreferencesManager preferencesManager,
-            IPrototypeManager prototypeManager,
-            IConfigurationManager configurationManager)
-        {
-            RobustXamlLoader.Load(this);
-            _entityManager = entityManager;
-            _prototypeManager = prototypeManager;
-            _preferencesManager = preferencesManager;
-
-            var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
-            var back = new StyleBoxTexture
-            {
-                Texture = panelTex,
-                Modulate = new Color(37, 37, 42)
-            };
-            back.SetPatchMargin(StyleBox.Margin.All, 10);
-
-            BackgroundPanel.PanelOverride = back;
-
-            _createNewCharacterButton = new Button
-            {
-                Text = Loc.GetString("character-setup-gui-create-new-character-button"),
-            };
-            _createNewCharacterButton.OnPressed += args =>
-            {
-                preferencesManager.CreateCharacter(HumanoidCharacterProfile.Random());
-                UpdateUI();
-                args.Event.Handle();
-            };
-
-            _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
-            _humanoidProfileEditor.OnProfileChanged += ProfileChanged;
-            CharEditor.AddChild(_humanoidProfileEditor);
-
-            UpdateUI();
-
-            RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
-
-            StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
-            preferencesManager.OnServerDataLoaded += UpdateUI;
-        }
-
-        protected override void Dispose(bool disposing)
-        {
-            base.Dispose(disposing);
-            if (!disposing)
-                return;
-
-            _preferencesManager.OnServerDataLoaded -= UpdateUI;
-        }
-
-        public void Save() => _humanoidProfileEditor.Save();
-
-        private void ProfileChanged(ICharacterProfile profile, int profileSlot)
-        {
-            _humanoidProfileEditor.UpdateControls();
-            UpdateUI();
-        }
-
-        public void UpdateControls()
-        {
-            // Reset sliders etc. upon going going back to GUI.
-            _humanoidProfileEditor.LoadServerData();
-        }
-
-        private void UpdateUI()
-        {
-            var numberOfFullSlots = 0;
-            var characterButtonsGroup = new ButtonGroup();
-            Characters.RemoveAllChildren();
-
-            if (!_preferencesManager.ServerDataLoaded)
-            {
-                return;
-            }
-
-            _createNewCharacterButton.ToolTip =
-                Loc.GetString("character-setup-gui-create-new-character-button-tooltip",
-                ("maxCharacters", _preferencesManager.Settings!.MaxCharacterSlots));
-
-            foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
-            {
-                numberOfFullSlots++;
-                var characterPickerButton = new CharacterPickerButton(_entityManager,
-                    _preferencesManager,
-                    _prototypeManager,
-                    characterButtonsGroup,
-                    character);
-                Characters.AddChild(characterPickerButton);
-
-                var characterIndexCopy = slot;
-                characterPickerButton.OnPressed += args =>
-                {
-                    _humanoidProfileEditor.Profile = (HumanoidCharacterProfile)character;
-                    _humanoidProfileEditor.CharacterSlot = characterIndexCopy;
-                    _humanoidProfileEditor.UpdateControls();
-                    _preferencesManager.SelectCharacter(character);
-                    var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-                    controller.UpdateProfile(_humanoidProfileEditor.Profile);
-                    controller.ReloadCharacterUI();
-                    UpdateUI();
-                    args.Event.Handle();
-                };
-            }
-
-            _createNewCharacterButton.Disabled =
-                numberOfFullSlots >= _preferencesManager.Settings.MaxCharacterSlots;
-            Characters.AddChild(_createNewCharacterButton);
-            // TODO: Move this shit to the Lobby UI controller
-        }
-
-        /// <summary>
-        /// Shows individual characters on the side of the character GUI.
-        /// </summary>
-        private sealed class CharacterPickerButton : ContainerButton
-        {
-            private EntityUid _previewDummy;
-
-            public CharacterPickerButton(
-                IEntityManager entityManager,
-                IClientPreferencesManager preferencesManager,
-                IPrototypeManager prototypeManager,
-                ButtonGroup group,
-                ICharacterProfile profile)
-            {
-                AddStyleClass(StyleClassButton);
-                ToggleMode = true;
-                Group = group;
-
-                var humanoid = profile as HumanoidCharacterProfile;
-                if (humanoid is not null)
-                {
-                    var dummy = prototypeManager.Index<SpeciesPrototype>(humanoid.Species).DollPrototype;
-                    _previewDummy = entityManager.SpawnEntity(dummy, MapCoordinates.Nullspace);
-                }
-                else
-                {
-                    _previewDummy = entityManager.SpawnEntity(prototypeManager.Index<SpeciesPrototype>(SharedHumanoidAppearanceSystem.DefaultSpecies).DollPrototype, MapCoordinates.Nullspace);
-                }
-
-                EntitySystem.Get<HumanoidAppearanceSystem>().LoadProfile(_previewDummy, (HumanoidCharacterProfile)profile);
-
-                if (humanoid != null)
-                {
-                    var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-                    var job = controller.GetPreferredJob(humanoid);
-                    controller.GiveDummyJobClothes(_previewDummy, humanoid, job);
-
-                    if (prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
-                    {
-                        var loadout = humanoid.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), entityManager, prototypeManager);
-                        controller.GiveDummyLoadout(_previewDummy, loadout);
-                    }
-                }
-
-                var isSelectedCharacter = profile == preferencesManager.Preferences?.SelectedCharacter;
-
-                if (isSelectedCharacter)
-                    Pressed = true;
-
-                var view = new SpriteView
-                {
-                    Scale = new Vector2(2, 2),
-                    OverrideDirection = Direction.South
-                };
-                view.SetEntity(_previewDummy);
-
-                var description = profile.Name;
-
-                var highPriorityJob = humanoid?.JobPriorities.SingleOrDefault(p => p.Value == JobPriority.High).Key;
-                if (highPriorityJob != null)
-                {
-                    var jobName = IoCManager.Resolve<IPrototypeManager>().Index<JobPrototype>(highPriorityJob).LocalizedName;
-                    description = $"{description}\n{jobName}";
-                }
-
-                var descriptionLabel = new Label
-                {
-                    Text = description,
-                    ClipText = true,
-                    HorizontalExpand = true
-                };
-                var deleteButton = new Button
-                {
-                    Text = Loc.GetString("character-setup-gui-character-picker-button-delete-button"),
-                    Visible = !isSelectedCharacter,
-                };
-                var confirmDeleteButton = new Button
-                {
-                    Text = Loc.GetString("character-setup-gui-character-picker-button-confirm-delete-button"),
-                    Visible = false,
-                };
-                confirmDeleteButton.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
-                confirmDeleteButton.OnPressed += _ =>
-                {
-                    Parent?.RemoveChild(this);
-                    Parent?.RemoveChild(confirmDeleteButton);
-                    preferencesManager.DeleteCharacter(profile);
-                };
-                deleteButton.OnPressed += _ =>
-                {
-
-                    deleteButton.Visible = false;
-                    confirmDeleteButton.Visible = true;
-
-                };
-
-                var internalHBox = new BoxContainer
-                {
-                    Orientation = LayoutOrientation.Horizontal,
-                    HorizontalExpand = true,
-                    SeparationOverride = 0,
-                    Children =
-                    {
-                        view,
-                        descriptionLabel,
-                        deleteButton,
-                        confirmDeleteButton
-                    }
-                };
-
-                AddChild(internalHBox);
-            }
-
-            protected override void Dispose(bool disposing)
-            {
-                base.Dispose(disposing);
-                if (!disposing)
-                    return;
-
-                IoCManager.Resolve<IEntityManager>().DeleteEntity(_previewDummy);
-                _previewDummy = default;
-            }
-        }
-    }
-}
diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
deleted file mode 100644 (file)
index 750006b..0000000
+++ /dev/null
@@ -1,23 +0,0 @@
-using Content.Shared.Preferences;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Preferences.UI
-{
-    public sealed partial class HumanoidProfileEditor
-    {
-        private void RandomizeEverything()
-        {
-            Profile = HumanoidCharacterProfile.Random();
-            UpdateControls();
-            IsDirty = true;
-        }
-
-        private void RandomizeName()
-        {
-            if (Profile == null) return;
-            var name = HumanoidCharacterProfile.GetName(Profile.Species, Profile.Gender);
-            SetName(name);
-            UpdateNameEdit();
-        }
-    }
-}
diff --git a/Content.Client/Preferences/UI/JobPrioritySelector.cs b/Content.Client/Preferences/UI/JobPrioritySelector.cs
deleted file mode 100644 (file)
index 243c78f..0000000
+++ /dev/null
@@ -1,46 +0,0 @@
-using System.Numerics;
-using Content.Shared.Preferences;
-using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
-using Content.Shared.Roles;
-using Content.Shared.StatusIcon;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.Utility;
-using Robust.Shared.Prototypes;
-
-namespace Content.Client.Preferences.UI;
-
-public sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
-{
-    public JobPriority Priority
-    {
-        get => (JobPriority) Options.SelectedValue;
-        set => Options.SelectByValue((int) value);
-    }
-
-    public event Action<JobPriority>? PriorityChanged;
-
-    public JobPrioritySelector(RoleLoadout? loadout, JobPrototype proto, ButtonGroup btnGroup, IPrototypeManager protoMan)
-        : base(proto, btnGroup)
-    {
-        Options.OnItemSelected += args => PriorityChanged?.Invoke(Priority);
-
-        var items = new[]
-        {
-            ("humanoid-profile-editor-job-priority-high-button", (int) JobPriority.High),
-            ("humanoid-profile-editor-job-priority-medium-button", (int) JobPriority.Medium),
-            ("humanoid-profile-editor-job-priority-low-button", (int) JobPriority.Low),
-            ("humanoid-profile-editor-job-priority-never-button", (int) JobPriority.Never),
-        };
-
-        var icon = new TextureRect
-        {
-            TextureScale = new Vector2(2, 2),
-            VerticalAlignment = VAlignment.Center
-        };
-        var jobIcon = protoMan.Index<StatusIconPrototype>(proto.Icon);
-        icon.Texture = jobIcon.Icon.Frame0();
-
-        Setup(loadout, items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
-    }
-}
diff --git a/Content.Client/Preferences/UI/RequirementsSelector.cs b/Content.Client/Preferences/UI/RequirementsSelector.cs
deleted file mode 100644 (file)
index e016661..0000000
+++ /dev/null
@@ -1,222 +0,0 @@
-using System.Numerics;
-using Content.Client.Lobby;
-using Content.Client.Stylesheets;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Clothing;
-using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
-using Content.Shared.Roles;
-using Robust.Client.Player;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-namespace Content.Client.Preferences.UI;
-
-public abstract class RequirementsSelector<T> : BoxContainer where T : IPrototype
-{
-    private ButtonGroup _loadoutGroup;
-
-    public T Proto { get; }
-    public bool Disabled => _lockStripe.Visible;
-
-    protected readonly RadioOptions<int> Options;
-    private readonly StripeBack _lockStripe;
-    private LoadoutWindow? _loadoutWindow;
-
-    private RoleLoadout? _loadout;
-
-    /// <summary>
-    /// Raised if a loadout has been updated.
-    /// </summary>
-    public event Action<RoleLoadout>? LoadoutUpdated;
-
-    protected RequirementsSelector(T proto, ButtonGroup loadoutGroup)
-    {
-        _loadoutGroup = loadoutGroup;
-        Proto = proto;
-
-        Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
-        {
-            FirstButtonStyle = StyleBase.ButtonOpenRight,
-            ButtonStyle = StyleBase.ButtonOpenBoth,
-            LastButtonStyle = StyleBase.ButtonOpenLeft,
-            HorizontalExpand = true,
-        };
-        //Override default radio option button width
-        Options.GenerateItem = GenerateButton;
-
-        Options.OnItemSelected += args => Options.Select(args.Id);
-
-        var requirementsLabel = new Label()
-        {
-            Text = Loc.GetString("role-timer-locked"),
-            Visible = true,
-            HorizontalAlignment = HAlignment.Center,
-            StyleClasses = {StyleBase.StyleClassLabelSubText},
-        };
-
-        _lockStripe = new StripeBack()
-        {
-            Visible = false,
-            HorizontalExpand = true,
-            HasMargins = false,
-            MouseFilter = MouseFilterMode.Stop,
-            Children =
-            {
-                requirementsLabel
-            }
-        };
-
-        // Setup must be called after
-    }
-
-    /// <summary>
-    /// Actually adds the controls, must be called in the inheriting class' constructor.
-    /// </summary>
-    protected void Setup(RoleLoadout? loadout, (string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
-    {
-        _loadout = loadout;
-
-        foreach (var (text, value) in items)
-        {
-            Options.AddItem(Loc.GetString(text), value);
-        }
-
-        var titleLabel = new Label()
-        {
-            Margin = new Thickness(5f, 0, 5f, 0),
-            Text = title,
-            MinSize = new Vector2(titleSize, 0),
-            MouseFilter = MouseFilterMode.Stop,
-            ToolTip = description
-        };
-
-        if (icon != null)
-            AddChild(icon);
-
-        AddChild(titleLabel);
-        AddChild(Options);
-        AddChild(_lockStripe);
-
-        var loadoutWindowBtn = new Button()
-        {
-            Text = Loc.GetString("loadout-window"),
-            HorizontalAlignment = HAlignment.Right,
-            Group = _loadoutGroup,
-            Margin = new Thickness(3f, 0f, 0f, 0f),
-        };
-
-        var collection = IoCManager.Instance!;
-        var protoManager = collection.Resolve<IPrototypeManager>();
-
-        // If no loadout found then disabled button
-        if (!protoManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(Proto.ID)))
-        {
-            loadoutWindowBtn.Disabled = true;
-        }
-        // else
-        else
-        {
-            var session = collection.Resolve<IPlayerManager>().LocalSession!;
-            // TODO: Most of lobby state should be a uicontroller
-            // trying to handle all this shit is a big-ass mess.
-            // Every time I touch it I try to make it slightly better but it needs a howitzer dropped on it.
-            loadoutWindowBtn.OnPressed += args =>
-            {
-                if (args.Button.Pressed)
-                {
-                    // We only create a loadout when necessary to avoid unnecessary DB entries.
-                    _loadout ??= new RoleLoadout(LoadoutSystem.GetJobPrototype(Proto.ID));
-                    _loadout.SetDefault(protoManager);
-
-                    _loadoutWindow = new LoadoutWindow(_loadout, protoManager.Index(_loadout.Role), session, collection)
-                    {
-                        Title = Loc.GetString(Proto.ID + "-loadout"),
-                    };
-
-                    _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
-
-                    // If it's a job preview then refresh it.
-                    if (Proto is JobPrototype jobProto)
-                    {
-                        var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-                        controller.SetDummyJob(jobProto);
-                    }
-
-                    _loadoutWindow.OnLoadoutUnpressed += (selectedGroup, selectedLoadout) =>
-                    {
-                        if (!_loadout.RemoveLoadout(selectedGroup, selectedLoadout, protoManager))
-                            return;
-
-                        _loadout.EnsureValid(session, collection);
-                        _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
-                        var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-                        controller.ReloadProfile();
-                        LoadoutUpdated?.Invoke(_loadout);
-                    };
-
-                    _loadoutWindow.OnLoadoutPressed += (selectedGroup, selectedLoadout) =>
-                    {
-                        if (!_loadout.AddLoadout(selectedGroup, selectedLoadout, protoManager))
-                            return;
-
-                        _loadout.EnsureValid(session, collection);
-                        _loadoutWindow.RefreshLoadouts(_loadout, session, collection);
-                        var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
-                        controller.ReloadProfile();
-                        LoadoutUpdated?.Invoke(_loadout);
-                    };
-
-                    _loadoutWindow.OpenCenteredLeft();
-                    _loadoutWindow.OnClose += () =>
-                    {
-                        loadoutWindowBtn.Pressed = false;
-                        _loadoutWindow?.Dispose();
-                        _loadoutWindow = null;
-                    };
-                }
-                else
-                {
-                    CloseLoadout();
-                }
-            };
-        }
-
-        AddChild(loadoutWindowBtn);
-    }
-
-    public void CloseLoadout()
-    {
-        _loadoutWindow?.Close();
-        _loadoutWindow?.Dispose();
-        _loadoutWindow = null;
-    }
-
-    public void LockRequirements(FormattedMessage requirements)
-    {
-        var tooltip = new Tooltip();
-        tooltip.SetMessage(requirements);
-        _lockStripe.TooltipSupplier = _ => tooltip;
-        _lockStripe.Visible = true;
-        Options.Visible = false;
-    }
-
-    // TODO: Subscribe to roletimers event. I am too lazy to do this RN But I doubt most people will notice fn
-    public void UnlockRequirements()
-    {
-        _lockStripe.Visible = false;
-        Options.Visible = true;
-    }
-
-    private Button GenerateButton(string text, int value)
-    {
-        return new Button
-        {
-            Text = text,
-            MinWidth = 90,
-            HorizontalExpand = true,
-        };
-    }
-}
index 4abe2839fbae949ba697da606c473e0373bf8de6..60501a781fc58ad16b53daf64390ee201ca2f369 100644 (file)
@@ -1,5 +1,4 @@
 using Content.Client.Lobby;
-using Content.Client.Preferences;
 using Content.Server.Preferences.Managers;
 using Content.Shared.Preferences;
 using Robust.Client.State;
index 0fb9c6a361ba2adea367eae8a75d70124d27472e..afaeafc6655e31cc6672af0ae04943e038dcf6c8 100644 (file)
@@ -39,32 +39,21 @@ namespace Content.IntegrationTests.Tests.Preferences
 
         private static HumanoidCharacterProfile CharlieCharlieson()
         {
-            return new(
-                "Charlie Charlieson",
-                "The biggest boy around.",
-                "Human",
-                21,
-                Sex.Male,
-                Gender.Epicene,
-                new HumanoidCharacterAppearance(
+            return new()
+            {
+                Name = "Charlie Charlieson",
+                FlavorText = "The biggest boy around.",
+                Species = "Human",
+                Age = 21,
+                Appearance = new(
                     "Afro",
                     Color.Aqua,
                     "Shaved",
                     Color.Aquamarine,
                     Color.Azure,
                     Color.Beige,
-                    new ()
-                ),
-                SpawnPriorityPreference.None,
-                new Dictionary<string, JobPriority>
-                {
-                    {SharedGameTicker.FallbackOverflowJob, JobPriority.High}
-                },
-                PreferenceUnavailableMode.StayInLobby,
-                new List<string> (),
-                new List<string>(),
-                new Dictionary<string, RoleLoadout>()
-            );
+                    new ())
+            };
         }
 
         private static ServerDbSqlite GetDb(RobustIntegrationTest.ServerIntegrationInstance server)
index 12d220512b7b2a1a8c4df34eb9a7f12a3a6bca7d..8743a4b5db1cc09c46911cf71f0991f80a0843b2 100644 (file)
@@ -14,7 +14,6 @@ using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Preferences;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
 using Microsoft.EntityFrameworkCore;
 using Robust.Shared.Enums;
 using Robust.Shared.Network;
@@ -253,8 +252,8 @@ namespace Content.Server.Database
                 spawnPriority,
                 jobs,
                 (PreferenceUnavailableMode) profile.PreferenceUnavailable,
-                antags.ToList(),
-                traits.ToList(),
+                antags.ToHashSet(),
+                traits.ToHashSet(),
                 loadouts
             );
         }
index fbbf4ecf721f83177f389a23301d3199fc993887..05bebea075579eb77d3ad590e9aa8aa73d43ace1 100644 (file)
@@ -9,8 +9,29 @@ namespace Content.Shared.Humanoid;
 
 [DataDefinition]
 [Serializable, NetSerializable]
-public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
+public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, IEquatable<HumanoidCharacterAppearance>
 {
+    [DataField("hair")]
+    public string HairStyleId { get; set; } = HairStyles.DefaultHairStyle;
+
+    [DataField]
+    public Color HairColor { get; set; } = Color.Black;
+
+    [DataField("facialHair")]
+    public string FacialHairStyleId { get; set; } = HairStyles.DefaultFacialHairStyle;
+
+    [DataField]
+    public Color FacialHairColor { get; set; } = Color.Black;
+
+    [DataField]
+    public Color EyeColor { get; set; } = Color.Black;
+
+    [DataField]
+    public Color SkinColor { get; set; } = Humanoid.SkinColor.ValidHumanSkinTone;
+
+    [DataField]
+    public List<Marking> Markings { get; set; } = new();
+
     public HumanoidCharacterAppearance(string hairStyleId,
         Color hairColor,
         string facialHairStyleId,
@@ -28,26 +49,11 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
         Markings = markings;
     }
 
-    [DataField("hair")]
-    public string HairStyleId { get; private set; }
-
-    [DataField("hairColor")]
-    public Color HairColor { get; private set; }
-
-    [DataField("facialHair")]
-    public string FacialHairStyleId { get; private set; }
-
-    [DataField("facialHairColor")]
-    public Color FacialHairColor { get; private set; }
-
-    [DataField("eyeColor")]
-    public Color EyeColor { get; private set; }
-
-    [DataField("skinColor")]
-    public Color SkinColor { get; private set; }
+    public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) :
+        this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings))
+    {
 
-    [DataField("markings")]
-    public List<Marking> Markings { get; private set; }
+    }
 
     public HumanoidCharacterAppearance WithHairStyleName(string newName)
     {
@@ -84,18 +90,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
         return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings);
     }
 
-    public HumanoidCharacterAppearance() : this(
-        HairStyles.DefaultHairStyle,
-        Color.Black,
-        HairStyles.DefaultFacialHairStyle,
-        Color.Black,
-        Color.Black,
-        Humanoid.SkinColor.ValidHumanSkinTone,
-        new ()
-    )
-    {
-    }
-
     public static HumanoidCharacterAppearance DefaultWithSpecies(string species)
     {
         var speciesPrototype = IoCManager.Resolve<IPrototypeManager>().Index<SpeciesPrototype>(species);
@@ -245,4 +239,32 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance
         if (!Markings.SequenceEqual(other.Markings)) return false;
         return true;
     }
+
+    public bool Equals(HumanoidCharacterAppearance? other)
+    {
+        if (ReferenceEquals(null, other)) return false;
+        if (ReferenceEquals(this, other)) return true;
+        return HairStyleId == other.HairStyleId &&
+               HairColor.Equals(other.HairColor) &&
+               FacialHairStyleId == other.FacialHairStyleId &&
+               FacialHairColor.Equals(other.FacialHairColor) &&
+               EyeColor.Equals(other.EyeColor) &&
+               SkinColor.Equals(other.SkinColor) &&
+               Markings.SequenceEqual(other.Markings);
+    }
+
+    public override bool Equals(object? obj)
+    {
+        return ReferenceEquals(this, obj) || obj is HumanoidCharacterAppearance other && Equals(other);
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings);
+    }
+
+    public HumanoidCharacterAppearance Clone()
+    {
+        return new(this);
+    }
 }
diff --git a/Content.Shared/Humanoid/HumanoidProfileExport.cs b/Content.Shared/Humanoid/HumanoidProfileExport.cs
new file mode 100644 (file)
index 0000000..2b7f9ac
--- /dev/null
@@ -0,0 +1,19 @@
+using Content.Shared.Preferences;
+
+namespace Content.Shared.Humanoid;
+
+/// <summary>
+/// Holds all of the data for importing / exporting character profiles.
+/// </summary>
+[DataDefinition]
+public sealed partial class HumanoidProfileExport
+{
+    [DataField]
+    public string ForkId;
+
+    [DataField]
+    public int Version = 1;
+
+    [DataField(required: true)]
+    public HumanoidCharacterProfile Profile = default!;
+}
index ffb78dcf1fcf0790238ac59ab6472f21abd7bc68..64c60436f64c0a0c653d656012d2c8d08f695eb5 100644 (file)
@@ -1,13 +1,22 @@
+using System.IO;
 using System.Linq;
+using Content.Shared.CCVar;
 using Content.Shared.Decals;
 using Content.Shared.Examine;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.IdentityManagement;
 using Content.Shared.Preferences;
+using Robust.Shared;
+using Robust.Shared.Configuration;
 using Robust.Shared.GameObjects.Components.Localization;
 using Robust.Shared.Network;
+using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Utility;
+using YamlDotNet.RepresentationModel;
 
 namespace Content.Shared.Humanoid;
 
@@ -22,8 +31,10 @@ namespace Content.Shared.Humanoid;
 /// </summary>
 public abstract class SharedHumanoidAppearanceSystem : EntitySystem
 {
+    [Dependency] private readonly IConfigurationManager _cfgManager = default!;
     [Dependency] private readonly INetManager _netManager = default!;
     [Dependency] private readonly IPrototypeManager _proto = default!;
+    [Dependency] private readonly ISerializationManager _serManager = default!;
     [Dependency] private readonly MarkingManager _markingManager = default!;
 
     [ValidatePrototypeId<SpeciesPrototype>]
@@ -37,6 +48,37 @@ public abstract class SharedHumanoidAppearanceSystem : EntitySystem
         SubscribeLocalEvent<HumanoidAppearanceComponent, ExaminedEvent>(OnExamined);
     }
 
+    public DataNode ToDataNode(HumanoidCharacterProfile profile)
+    {
+        var export = new HumanoidProfileExport()
+        {
+            ForkId = _cfgManager.GetCVar(CVars.BuildForkId),
+            Profile = profile,
+        };
+
+        var dataNode = _serManager.WriteValue(export, alwaysWrite: true, notNullableOverride: true);
+        return dataNode;
+    }
+
+    public HumanoidCharacterProfile FromStream(Stream stream, ICommonSession session)
+    {
+        using var reader = new StreamReader(stream, EncodingHelpers.UTF8);
+        var yamlStream = new YamlStream();
+        yamlStream.Load(reader);
+
+        var root = yamlStream.Documents[0].RootNode;
+        var export = _serManager.Read<HumanoidProfileExport>(root.ToDataNode(), notNullableOverride: true);
+
+        /*
+         * Add custom handling here for forks / version numbers if you care.
+         */
+
+        var profile = export.Profile;
+        var collection = IoCManager.Instance;
+        profile.EnsureValid(session, collection!);
+        return profile;
+    }
+
     private void OnInit(EntityUid uid, HumanoidAppearanceComponent humanoid, ComponentInit args)
     {
         if (string.IsNullOrEmpty(humanoid.Species) || _netManager.IsClient && !IsClientSide(uid))
index f9b037c8ab5cc5cea54b9751716206bf903a9b1b..fd679c7796ea71dc3214da507142416677120ea8 100644 (file)
@@ -5,7 +5,6 @@ using Content.Shared.GameTicking;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Preferences.Loadouts;
-using Content.Shared.Preferences.Loadouts.Effects;
 using Content.Shared.Roles;
 using Content.Shared.Traits;
 using Robust.Shared.Collections;
@@ -32,16 +31,101 @@ namespace Content.Shared.Preferences
         public const int MaxNameLength = 32;
         public const int MaxDescLength = 512;
 
-        private readonly Dictionary<string, JobPriority> _jobPriorities;
-        private readonly List<string> _antagPreferences;
-        private readonly List<string> _traitPreferences;
+        /// <summary>
+        /// Job preferences for initial spawn.
+        /// </summary>
+        [DataField]
+        private Dictionary<string, JobPriority> _jobPriorities = new()
+        {
+            {
+                SharedGameTicker.FallbackOverflowJob, JobPriority.High
+            }
+        };
+
+        /// <summary>
+        /// Antags we have opted in to.
+        /// </summary>
+        [DataField]
+        private HashSet<string> _antagPreferences = new();
+
+        /// <summary>
+        /// Enabled traits.
+        /// </summary>
+        [DataField]
+        private HashSet<string> _traitPreferences = new();
 
+        /// <summary>
+        /// <see cref="_loadouts"/>
+        /// </summary>
         public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
 
-        private Dictionary<string, RoleLoadout> _loadouts;
+        [DataField]
+        private Dictionary<string, RoleLoadout> _loadouts = new();
+
+        [DataField]
+        public string Name { get; set; } = "John Doe";
+
+        /// <summary>
+        /// Detailed text that can appear for the character if <see cref="CCVars.FlavorText"/> is enabled.
+        /// </summary>
+        [DataField]
+        public string FlavorText { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Associated <see cref="SpeciesPrototype"/> for this profile.
+        /// </summary>
+        [DataField]
+        public string Species { get; set; } = SharedHumanoidAppearanceSystem.DefaultSpecies;
+
+        [DataField]
+        public int Age { get; set; } = 18;
+
+        [DataField]
+        public Sex Sex { get; private set; } = Sex.Male;
+
+        [DataField]
+        public Gender Gender { get; private set; } = Gender.Male;
+
+        /// <summary>
+        /// <see cref="Appearance"/>
+        /// </summary>
+        public ICharacterAppearance CharacterAppearance => Appearance;
 
-        // What in the lord is happening here.
-        private HumanoidCharacterProfile(
+        /// <summary>
+        /// Stores markings, eye colors, etc for the profile.
+        /// </summary>
+        [DataField]
+        public HumanoidCharacterAppearance Appearance { get; set; } = new();
+
+        /// <summary>
+        /// When spawning into a round what's the preferred spot to spawn.
+        /// </summary>
+        [DataField]
+        public SpawnPriorityPreference SpawnPriority { get; private set; } = SpawnPriorityPreference.None;
+
+        /// <summary>
+        /// <see cref="_jobPriorities"/>
+        /// </summary>
+        public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
+
+        /// <summary>
+        /// <see cref="_antagPreferences"/>
+        /// </summary>
+        public IReadOnlySet<string> AntagPreferences => _antagPreferences;
+
+        /// <summary>
+        /// <see cref="_traitPreferences"/>
+        /// </summary>
+        public IReadOnlySet<string> TraitPreferences => _traitPreferences;
+
+        /// <summary>
+        /// If we're unable to get one of our preferred jobs do we spawn as a fallback job or do we stay in lobby.
+        /// </summary>
+        [DataField]
+        public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } =
+            PreferenceUnavailableMode.SpawnAsOverflow;
+
+        public HumanoidCharacterProfile(
             string name,
             string flavortext,
             string species,
@@ -52,8 +136,8 @@ namespace Content.Shared.Preferences
             SpawnPriorityPreference spawnPriority,
             Dictionary<string, JobPriority> jobPriorities,
             PreferenceUnavailableMode preferenceUnavailable,
-            List<string> antagPreferences,
-            List<string> traitPreferences,
+            HashSet<string> antagPreferences,
+            HashSet<string> traitPreferences,
             Dictionary<string, RoleLoadout> loadouts)
         {
             Name = name;
@@ -71,40 +155,21 @@ namespace Content.Shared.Preferences
             _loadouts = loadouts;
         }
 
-        /// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
-        private HumanoidCharacterProfile(
-            HumanoidCharacterProfile other,
-            Dictionary<string, JobPriority> jobPriorities,
-            List<string> antagPreferences,
-            List<string> traitPreferences,
-            Dictionary<string, RoleLoadout> loadouts)
-            : this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.SpawnPriority,
-                jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, loadouts)
-        {
-        }
-
         /// <summary>Copy constructor</summary>
-        private HumanoidCharacterProfile(HumanoidCharacterProfile other)
-            : this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
-        {
-        }
-
-        public HumanoidCharacterProfile(
-            string name,
-            string flavortext,
-            string species,
-            int age,
-            Sex sex,
-            Gender gender,
-            HumanoidCharacterAppearance appearance,
-            SpawnPriorityPreference spawnPriority,
-            IReadOnlyDictionary<string, JobPriority> jobPriorities,
-            PreferenceUnavailableMode preferenceUnavailable,
-            IReadOnlyList<string> antagPreferences,
-            IReadOnlyList<string> traitPreferences,
-            Dictionary<string, RoleLoadout> loadouts)
-            : this(name, flavortext, species, age, sex, gender, appearance, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
-                preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences), new Dictionary<string, RoleLoadout>(loadouts))
+        public HumanoidCharacterProfile(HumanoidCharacterProfile other)
+            : this(other.Name,
+                other.FlavorText,
+                other.Species,
+                other.Age,
+                other.Sex,
+                other.Gender,
+                other.Appearance.Clone(),
+                other.SpawnPriority,
+                new Dictionary<string, JobPriority>(other.JobPriorities),
+                other.PreferenceUnavailable,
+                new HashSet<string>(other.AntagPreferences),
+                new HashSet<string>(other.TraitPreferences),
+                new Dictionary<string, RoleLoadout>(other.Loadouts))
         {
         }
 
@@ -113,23 +178,7 @@ namespace Content.Shared.Preferences
         ///     Defaults to <see cref="SharedHumanoidAppearanceSystem.DefaultSpecies"/> for the species.
         /// </summary>
         /// <returns></returns>
-        public HumanoidCharacterProfile() : this(
-            "John Doe",
-            "",
-            SharedHumanoidAppearanceSystem.DefaultSpecies,
-            18,
-            Sex.Male,
-            Gender.Male,
-            new HumanoidCharacterAppearance(),
-            SpawnPriorityPreference.None,
-            new Dictionary<string, JobPriority>
-            {
-                {SharedGameTicker.FallbackOverflowJob, JobPriority.High}
-            },
-            PreferenceUnavailableMode.SpawnAsOverflow,
-            new List<string>(),
-            new List<string>(),
-            new Dictionary<string, RoleLoadout>())
+        public HumanoidCharacterProfile()
         {
         }
 
@@ -140,23 +189,10 @@ namespace Content.Shared.Preferences
         /// <returns>Humanoid character profile with default settings.</returns>
         public static HumanoidCharacterProfile DefaultWithSpecies(string species = SharedHumanoidAppearanceSystem.DefaultSpecies)
         {
-            return new(
-                "John Doe",
-                "",
-                species,
-                18,
-                Sex.Male,
-                Gender.Male,
-                HumanoidCharacterAppearance.DefaultWithSpecies(species),
-                SpawnPriorityPreference.None,
-                new Dictionary<string, JobPriority>
-                {
-                    {SharedGameTicker.FallbackOverflowJob, JobPriority.High}
-                },
-                PreferenceUnavailableMode.SpawnAsOverflow,
-                new List<string>(),
-                new List<string>(),
-                new Dictionary<string, RoleLoadout>());
+            return new()
+            {
+                Species = species,
+            };
         }
 
         // TODO: This should eventually not be a visual change only.
@@ -201,36 +237,17 @@ namespace Content.Shared.Preferences
 
             var name = GetName(species, gender);
 
-            return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), SpawnPriorityPreference.None,
-                new Dictionary<string, JobPriority>
-                {
-                    {SharedGameTicker.FallbackOverflowJob, JobPriority.High},
-                }, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
+            return new HumanoidCharacterProfile()
+            {
+                Name = name,
+                Sex = sex,
+                Age = age,
+                Gender = gender,
+                Species = species,
+                Appearance = HumanoidCharacterAppearance.Random(species, sex),
+            };
         }
 
-        public string Name { get; private set; }
-        public string FlavorText { get; private set; }
-        public string Species { get; private set; }
-
-        [DataField("age")]
-        public int Age { get; private set; }
-
-        [DataField("sex")]
-        public Sex Sex { get; private set; }
-
-        [DataField("gender")]
-        public Gender Gender { get; private set; }
-
-        public ICharacterAppearance CharacterAppearance => Appearance;
-
-        [DataField("appearance")]
-        public HumanoidCharacterAppearance Appearance { get; private set; }
-        public SpawnPriorityPreference SpawnPriority { get; private set; }
-        public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
-        public IReadOnlyList<string> AntagPreferences => _antagPreferences;
-        public IReadOnlyList<string> TraitPreferences => _traitPreferences;
-        public PreferenceUnavailableMode PreferenceUnavailable { get; private set; }
-
         public HumanoidCharacterProfile WithName(string name)
         {
             return new(this) { Name = name };
@@ -274,7 +291,10 @@ namespace Content.Shared.Preferences
 
         public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
         {
-            return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
+            return new(this)
+            {
+                _jobPriorities = new Dictionary<string, JobPriority>(jobPriorities),
+            };
         }
 
         public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@@ -288,7 +308,11 @@ namespace Content.Shared.Preferences
             {
                 dictionary[jobId] = priority;
             }
-            return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
+
+            return new(this)
+            {
+                _jobPriorities = dictionary,
+            };
         }
 
         public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@@ -298,50 +322,47 @@ namespace Content.Shared.Preferences
 
         public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
         {
-            return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
+            return new(this)
+            {
+                _antagPreferences = new HashSet<string>(antagPreferences),
+            };
         }
 
         public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
         {
-            var list = new List<string>(_antagPreferences);
+            var list = new HashSet<string>(_antagPreferences);
             if (pref)
             {
-                if (!list.Contains(antagId))
-                {
-                    list.Add(antagId);
-                }
+                list.Add(antagId);
             }
             else
             {
-                if (list.Contains(antagId))
-                {
-                    list.Remove(antagId);
-                }
+                list.Remove(antagId);
             }
 
-            return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
+            return new(this)
+            {
+                _antagPreferences = list,
+            };
         }
 
         public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
         {
-            var list = new List<string>(_traitPreferences);
+            var list = new HashSet<string>(_traitPreferences);
 
-            // TODO: Maybe just refactor this to HashSet? Same with _antagPreferences
             if (pref)
             {
-                if (!list.Contains(traitId))
-                {
-                    list.Add(traitId);
-                }
+                list.Add(traitId);
             }
             else
             {
-                if (list.Contains(traitId))
-                {
-                    list.Remove(traitId);
-                }
+                list.Remove(traitId);
             }
-            return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
+
+            return new(this)
+            {
+                _traitPreferences = list,
+            };
         }
 
         public string Summary =>
@@ -498,10 +519,10 @@ namespace Content.Shared.Preferences
             PreferenceUnavailable = prefsUnavailableMode;
 
             _antagPreferences.Clear();
-            _antagPreferences.AddRange(antags);
+            _antagPreferences.UnionWith(antags);
 
             _traitPreferences.Clear();
-            _traitPreferences.AddRange(traits);
+            _traitPreferences.UnionWith(traits);
 
             // Checks prototypes exist for all loadouts and dump / set to default if not.
             var toRemove = new ValueList<string>();
@@ -514,7 +535,7 @@ namespace Content.Shared.Preferences
                     continue;
                 }
 
-                loadouts.EnsureValid(session, collection);
+                loadouts.EnsureValid(this, session, collection);
             }
 
             foreach (var value in toRemove)
@@ -540,27 +561,26 @@ namespace Content.Shared.Preferences
 
         public override bool Equals(object? obj)
         {
-            return obj is HumanoidCharacterProfile other && MemberwiseEquals(other);
+            return ReferenceEquals(this, obj) || obj is HumanoidCharacterProfile other && Equals(other);
         }
 
         public override int GetHashCode()
         {
-            return HashCode.Combine(
-                HashCode.Combine(
-                    Name,
-                    Species,
-                    Age,
-                    Sex,
-                    Gender,
-                    Appearance
-                ),
-                SpawnPriority,
-                PreferenceUnavailable,
-                _jobPriorities,
-                _antagPreferences,
-                _traitPreferences,
-                _loadouts
-            );
+            var hashCode = new HashCode();
+            hashCode.Add(_jobPriorities);
+            hashCode.Add(_antagPreferences);
+            hashCode.Add(_traitPreferences);
+            hashCode.Add(_loadouts);
+            hashCode.Add(Name);
+            hashCode.Add(FlavorText);
+            hashCode.Add(Species);
+            hashCode.Add(Age);
+            hashCode.Add((int)Sex);
+            hashCode.Add((int)Gender);
+            hashCode.Add(Appearance);
+            hashCode.Add((int)SpawnPriority);
+            hashCode.Add((int)PreferenceUnavailable);
+            return hashCode.ToHashCode();
         }
 
         public void SetLoadout(RoleLoadout loadout)
@@ -582,10 +602,12 @@ namespace Content.Shared.Preferences
             }
 
             copied[loadout.Role] = loadout.Clone();
-            return new(this, _jobPriorities, _antagPreferences, _traitPreferences, copied);
+            var profile = Clone();
+            profile._loadouts = copied;
+            return profile;
         }
 
-        public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
+        public RoleLoadout GetLoadoutOrDefault(string id, ProtoId<SpeciesPrototype>? species, IEntityManager entManager, IPrototypeManager protoManager)
         {
             if (!_loadouts.TryGetValue(id, out var loadout))
             {
@@ -596,5 +618,10 @@ namespace Content.Shared.Preferences
             loadout.SetDefault(protoManager);
             return loadout;
         }
+
+        public HumanoidCharacterProfile Clone()
+        {
+            return new HumanoidCharacterProfile(this);
+        }
     }
 }
index 5a2cd87cc1fd9022b852892cb34835bf70525793..1be75f7dbc81dc11b7c0d4ee6eeee273a8ee666e 100644 (file)
@@ -13,13 +13,13 @@ public sealed partial class GroupLoadoutEffect : LoadoutEffect
     [DataField(required: true)]
     public ProtoId<LoadoutEffectGroupPrototype> Proto;
 
-    public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
+    public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
     {
         var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
 
         foreach (var effect in effectsProto.Effects)
         {
-            if (!effect.Validate(loadout, session, collection, out reason))
+            if (!effect.Validate(profile, loadout, session, collection, out reason))
                 return false;
         }
 
index 4a750a557833989c621cc1bedb4e8fb368ac3a44..54576d3a53a96abf62c973c82afde02768574028 100644 (file)
@@ -15,7 +15,7 @@ public sealed partial class JobRequirementLoadoutEffect : LoadoutEffect
     [DataField(required: true)]
     public JobRequirement Requirement = default!;
 
-    public override bool Validate(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
+    public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
     {
         var manager = collection.Resolve<ISharedPlaytimeManager>();
         var playtimes = manager.GetPlayTimes(session);
index 65694d52a1858c43e112d89aa28698b2da5b743c..f35b14e2e0da338e613914517b55d891382e99ba 100644 (file)
@@ -11,6 +11,7 @@ public abstract partial class LoadoutEffect
     /// Tries to validate the effect.
     /// </summary>
     public abstract bool Validate(
+        HumanoidCharacterProfile profile,
         RoleLoadout loadout,
         ICommonSession session,
         IDependencyCollection collection,
index 3146ff6163626d833ec4ff89225fc74d1725566f..842b4cfc03614b6fc062c89cf4c16a882a184aa0 100644 (file)
@@ -11,6 +11,7 @@ public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
     public int Cost = 1;
 
     public override bool Validate(
+        HumanoidCharacterProfile profile,
         RoleLoadout loadout,
         ICommonSession session,
         IDependencyCollection collection,
index 74673cbef390c2db4ee5bfbaf3212db0a00efcec..8f886dd2ab89c9e13361dc6613bb1f6238626eb0 100644 (file)
@@ -1,6 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Humanoid.Prototypes;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
 namespace Content.Shared.Preferences.Loadouts.Effects;
 
-public sealed class SpeciesLoadoutEffect
+public sealed partial class SpeciesLoadoutEffect : LoadoutEffect
 {
-    
+    [DataField(required: true)]
+    public List<ProtoId<SpeciesPrototype>> Species = new();
+
+    public override bool Validate(HumanoidCharacterProfile profile, RoleLoadout loadout, ICommonSession session, IDependencyCollection collection,
+        [NotNullWhen(false)] out FormattedMessage? reason)
+    {
+        if (Species.Contains(profile.Species))
+        {
+            reason = null;
+            return true;
+        }
+
+        reason = FormattedMessage.FromUnformatted(Loc.GetString("loadout-group-species-restriction"));
+        return false;
+    }
 }
index 6a4373b621470d52b50c690114bff6d18913f31f..dbe440f58b8e70184fd562d8c8dfe4fef80fe6b3 100644 (file)
@@ -6,8 +6,9 @@ namespace Content.Shared.Preferences.Loadouts;
 /// <summary>
 /// Specifies the selected prototype and custom data for a loadout.
 /// </summary>
-[Serializable, NetSerializable]
-public sealed class Loadout
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class Loadout
 {
+    [DataField]
     public ProtoId<LoadoutPrototype> Prototype;
 }
index e1c6f8395d0481b0b2ef65733b91d7f3410991dc..40e13f0edfa4dfd72c590fd4ca62b4588270fcfd 100644 (file)
@@ -1,4 +1,6 @@
 using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Random;
 using Robust.Shared.Collections;
 using Robust.Shared.Player;
@@ -11,11 +13,13 @@ namespace Content.Shared.Preferences.Loadouts;
 /// <summary>
 /// Contains all of the selected data for a role's loadout.
 /// </summary>
-[Serializable, NetSerializable]
-public sealed class RoleLoadout
+[Serializable, NetSerializable, DataDefinition]
+public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
 {
-    public readonly ProtoId<RoleLoadoutPrototype> Role;
+    [DataField]
+    public ProtoId<RoleLoadoutPrototype> Role;
 
+    [DataField]
     public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
 
     /*
@@ -44,7 +48,7 @@ public sealed class RoleLoadout
     /// <summary>
     /// Ensures all prototypes exist and effects can be applied.
     /// </summary>
-    public void EnsureValid(ICommonSession session, IDependencyCollection collection)
+    public void EnsureValid(HumanoidCharacterProfile profile, ICommonSession session, IDependencyCollection collection)
     {
         var groupRemove = new ValueList<string>();
         var protoManager = collection.Resolve<IPrototypeManager>();
@@ -81,7 +85,7 @@ public sealed class RoleLoadout
                 }
 
                 // Validate the loadout can be applied (e.g. points).
-                if (!IsValid(session, loadout.Prototype, collection, out _))
+                if (!IsValid(profile, session, loadout.Prototype, collection, out _))
                 {
                     loadouts.RemoveAt(i);
                     continue;
@@ -167,7 +171,7 @@ public sealed class RoleLoadout
     /// <summary>
     /// Returns whether a loadout is valid or not.
     /// </summary>
-    public bool IsValid(ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
+    public bool IsValid(HumanoidCharacterProfile profile, ICommonSession session, ProtoId<LoadoutPrototype> loadout, IDependencyCollection collection, [NotNullWhen(false)] out FormattedMessage? reason)
     {
         reason = null;
 
@@ -180,7 +184,7 @@ public sealed class RoleLoadout
             return false;
         }
 
-        if (!protoManager.TryIndex(Role, out var roleProto))
+        if (!protoManager.HasIndex(Role))
         {
             reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
             return false;
@@ -190,7 +194,7 @@ public sealed class RoleLoadout
 
         foreach (var effect in loadoutProto.Effects)
         {
-            valid = valid && effect.Validate(this, session, collection, out reason);
+            valid = valid && effect.Validate(profile, this, session, collection, out reason);
         }
 
         return valid;
@@ -257,4 +261,21 @@ public sealed class RoleLoadout
 
         return false;
     }
+
+    public bool Equals(RoleLoadout? other)
+    {
+        if (ReferenceEquals(null, other)) return false;
+        if (ReferenceEquals(this, other)) return true;
+        return Role.Equals(other.Role) && SelectedLoadouts.SequenceEqual(other.SelectedLoadouts) && Points == other.Points;
+    }
+
+    public override bool Equals(object? obj)
+    {
+        return ReferenceEquals(this, obj) || obj is RoleLoadout other && Equals(other);
+    }
+
+    public override int GetHashCode()
+    {
+        return HashCode.Combine(Role, SelectedLoadouts, Points);
+    }
 }
index bd80815e2319de35417d2ab4329ed2906748fe98..b85d7be38ed1b28d65d6594d3c378e0787d3c4b8 100644 (file)
@@ -1,7 +1,6 @@
 character-setup-gui-character-setup-label = Character setup
 character-setup-gui-character-setup-stats-button = Stats
 character-setup-gui-character-setup-rules-button = Rules
-character-setup-gui-character-setup-save-button = Save
 character-setup-gui-character-setup-close-button = Close
 character-setup-gui-create-new-character-button = Create new slot...
 character-setup-gui-create-new-character-button-tooltip = A maximum of {$maxCharacters} characters are allowed.
index 139d222f797fd8894bffc34de8b3677d63983246..c7a24d540585aee0878a90c07d67627bab25fe6f 100644 (file)
@@ -19,6 +19,7 @@ humanoid-profile-editor-pronouns-neuter-text = It / It
 humanoid-profile-editor-import-button = Import
 humanoid-profile-editor-export-button = Export
 humanoid-profile-editor-save-button = Save
+humanoid-profile-editor-reset-button = Reset
 humanoid-profile-editor-spawn-priority-label = Spawn priority:
 humanoid-profile-editor-eyes-label = Eye color:
 humanoid-profile-editor-jobs-tab = Jobs
index 53ac4258bc2084a238cf09c9d5f517325f99be44..e3fdb2bf08f0de1d05d929c712dc310c4cbf0816 100644 (file)
@@ -1,7 +1,7 @@
 - type: species
   id: Vox
   name: species-name-vox
-  roundStart: false # sad...
+  roundStart: false # sad
   prototype: MobVox
   sprites: MobVoxSprites
   markingLimits: MobVoxMarkingLimits