]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Loadouts redux (#25715)
authormetalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
Tue, 16 Apr 2024 12:57:43 +0000 (22:57 +1000)
committerGitHub <noreply@github.com>
Tue, 16 Apr 2024 12:57:43 +0000 (22:57 +1000)
* Loadouts redux

* Loadout window mockup

* More workout

* rent

* validation

* Developments

* bcs

* More cleanup

* Rebuild working

* Fix model and loading

* obsession

* efcore

* We got a stew goin

* Cleanup

* Optional + SeniorEngineering fix

* Fixes

* Update science.yml

* add

add

* Automatic naming

* Update nukeops

* Coming together

* Right now

* stargate

* rejig the UI

* weh

* Loadouts tweaks

* Merge conflicts + ordering fix

* yerba mate

* chocolat

* More updates

* Add multi-selection support

* test

h

* fikss

* a

* add tech assistant and hazard suit

* huh

* Latest changes

* add medical loadouts

* and science

* finish security loadouts

* cargo

* service done

* added wildcards

* add command

* Move restrictions

* Finalising

* Fix existing work

* Localise next batch

* clothing fix

* Fix storage names

* review

* the scooping room

* Test fixes

* Xamlify

* Xamlify this too

* Update Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/Jobs/Security/detective.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* Update Resources/Prototypes/Loadouts/loadout_groups.yml

Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
* ben

* Margins

---------

Co-authored-by: Firewatch <54725557+musicmanvr@users.noreply.github.com>
Co-authored-by: Mr. 27 <koolthunder019@gmail.com>
Co-authored-by: Mr. 27 <45323883+Dutch-VanDerLinde@users.noreply.github.com>
155 files changed:
Content.Benchmarks/SpawnEquipDeleteBenchmark.cs
Content.Client/IoC/ClientContentIoC.cs
Content.Client/Lobby/LobbyState.cs
Content.Client/Lobby/LobbyUIController.cs [new file with mode: 0644]
Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs [deleted file]
Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml [new file with mode: 0644]
Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs [new file with mode: 0644]
Content.Client/Lobby/UI/LobbyGui.xaml.cs
Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
Content.Client/Preferences/ClientPreferencesManager.cs
Content.Client/Preferences/UI/AntagPreferenceSelector.cs [new file with mode: 0644]
Content.Client/Preferences/UI/CharacterSetupGui.xaml
Content.Client/Preferences/UI/CharacterSetupGui.xaml.cs
Content.Client/Preferences/UI/HighlightedContainer.xaml [new file with mode: 0644]
Content.Client/Preferences/UI/HighlightedContainer.xaml.cs [new file with mode: 0644]
Content.Client/Preferences/UI/HumanoidProfileEditor.Random.cs
Content.Client/Preferences/UI/HumanoidProfileEditor.xaml
Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs
Content.Client/Preferences/UI/JobPrioritySelector.cs [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutContainer.xaml [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutContainer.xaml.cs [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutGroupContainer.xaml [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutWindow.xaml [new file with mode: 0644]
Content.Client/Preferences/UI/LoadoutWindow.xaml.cs [new file with mode: 0644]
Content.Client/Preferences/UI/RequirementsSelector.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Minds/MindTest.DeleteAllThenGhost.cs
Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs [new file with mode: 0644]
Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.Designer.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.cs [new file with mode: 0644]
Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs
Content.Server.Database/Model.cs
Content.Server/Administration/Commands/SetOutfitCommand.cs
Content.Server/Database/ServerDbBase.cs
Content.Server/GameTicking/Rules/NukeopsRuleSystem.cs
Content.Server/GameTicking/Rules/PiratesRuleSystem.cs
Content.Server/IoC/ServerContentIoC.cs
Content.Server/Players/PlayTimeTracking/PlayTimeTrackingManager.cs
Content.Server/Preferences/Managers/ServerPreferencesManager.cs
Content.Server/Spawners/EntitySystems/SpawnPointSystem.cs
Content.Server/Station/Systems/StationSpawningSystem.cs
Content.Shared/Clothing/LoadoutSystem.cs
Content.Shared/Humanoid/Prototypes/SpeciesPrototype.cs
Content.Shared/Players/PlayTimeTracking/ISharedPlaytimeManager.cs [new file with mode: 0644]
Content.Shared/Preferences/BackpackPreference.cs [deleted file]
Content.Shared/Preferences/ClothingPreference.cs [deleted file]
Content.Shared/Preferences/HumanoidCharacterProfile.cs
Content.Shared/Preferences/ICharacterProfile.cs
Content.Shared/Preferences/Loadouts/Effects/GroupLoadoutEffect.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/Effects/LoadoutEffect.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/Effects/LoadoutEffectGroupPrototype.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/Effects/PointsCostLoadoutEffect.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/Loadout.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/LoadoutPrototype.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/RoleLoadout.cs [new file with mode: 0644]
Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs [new file with mode: 0644]
Content.Shared/Roles/JobRequirements.cs
Content.Shared/Roles/StartingGearPrototype.cs
Content.Shared/Station/SharedStationSpawningSystem.cs
Resources/Locale/en-US/job/loadouts.ftl [new file with mode: 0644]
Resources/Locale/en-US/preferences/loadout-groups.ftl [new file with mode: 0644]
Resources/Locale/en-US/preferences/loadouts.ftl [new file with mode: 0644]
Resources/Locale/en-US/preferences/ui/humanoid-profile-editor.ftl
Resources/Prototypes/Catalog/Fills/Backpacks/StarterGear/backpack.yml
Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Cargo/salvage_specialist.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/botanist.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/chef.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/lawyer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/librarian.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/mime.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Command/captain.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Engineering/atmospheric_technician.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Engineering/technical_assistant.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Medical/chemist.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Medical/chief_medical_officer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Medical/medical_doctor.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Medical/medical_intern.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Security/detective.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Security/security_cadet.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Security/warden.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Wildcards/boxer.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/loadout_groups.yml [new file with mode: 0644]
Resources/Prototypes/Loadouts/role_loadouts.yml [new file with mode: 0644]
Resources/Prototypes/Roles/Antags/pirate.yml
Resources/Prototypes/Roles/Jobs/Cargo/cargo_technician.yml
Resources/Prototypes/Roles/Jobs/Cargo/quartermaster.yml
Resources/Prototypes/Roles/Jobs/Cargo/salvage_specialist.yml
Resources/Prototypes/Roles/Jobs/Civilian/assistant.yml
Resources/Prototypes/Roles/Jobs/Civilian/bartender.yml
Resources/Prototypes/Roles/Jobs/Civilian/botanist.yml
Resources/Prototypes/Roles/Jobs/Civilian/chaplain.yml
Resources/Prototypes/Roles/Jobs/Civilian/chef.yml
Resources/Prototypes/Roles/Jobs/Civilian/clown.yml
Resources/Prototypes/Roles/Jobs/Civilian/janitor.yml
Resources/Prototypes/Roles/Jobs/Civilian/lawyer.yml
Resources/Prototypes/Roles/Jobs/Civilian/librarian.yml
Resources/Prototypes/Roles/Jobs/Civilian/mime.yml
Resources/Prototypes/Roles/Jobs/Civilian/musician.yml
Resources/Prototypes/Roles/Jobs/Civilian/service_worker.yml
Resources/Prototypes/Roles/Jobs/Command/captain.yml
Resources/Prototypes/Roles/Jobs/Command/head_of_personnel.yml
Resources/Prototypes/Roles/Jobs/Engineering/atmospheric_technician.yml
Resources/Prototypes/Roles/Jobs/Engineering/chief_engineer.yml
Resources/Prototypes/Roles/Jobs/Engineering/station_engineer.yml
Resources/Prototypes/Roles/Jobs/Engineering/technical_assistant.yml
Resources/Prototypes/Roles/Jobs/Fun/cult_startinggear.yml
Resources/Prototypes/Roles/Jobs/Fun/misc_startinggear.yml
Resources/Prototypes/Roles/Jobs/Fun/wizard_startinggear.yml
Resources/Prototypes/Roles/Jobs/Medical/chemist.yml
Resources/Prototypes/Roles/Jobs/Medical/chief_medical_officer.yml
Resources/Prototypes/Roles/Jobs/Medical/medical_doctor.yml
Resources/Prototypes/Roles/Jobs/Medical/medical_intern.yml
Resources/Prototypes/Roles/Jobs/Medical/paramedic.yml
Resources/Prototypes/Roles/Jobs/Science/research_assistant.yml
Resources/Prototypes/Roles/Jobs/Science/research_director.yml
Resources/Prototypes/Roles/Jobs/Science/scientist.yml
Resources/Prototypes/Roles/Jobs/Security/detective.yml
Resources/Prototypes/Roles/Jobs/Security/head_of_security.yml
Resources/Prototypes/Roles/Jobs/Security/security_cadet.yml
Resources/Prototypes/Roles/Jobs/Security/security_officer.yml
Resources/Prototypes/Roles/Jobs/Security/warden.yml
Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/nanotrasen.yml [deleted file]
Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/syndicate.yml [deleted file]
Resources/Prototypes/Roles/Jobs/Wildcards/boxer.yml
Resources/Prototypes/Roles/Jobs/Wildcards/psychologist.yml
Resources/Prototypes/Roles/Jobs/Wildcards/reporter.yml
Resources/Prototypes/Roles/Jobs/Wildcards/zookeeper.yml

index de51b2fb1926b6d833ec84646315d65d5a5fedb2..8512107b69dea804e4f6affc2ea43f3b0f28f44a 100644 (file)
@@ -58,7 +58,7 @@ public class SpawnEquipDeleteBenchmark
             for (var i = 0; i < N; i++)
             {
                 _entity = server.EntMan.SpawnAttachedTo(Mob, _coords);
-                _spawnSys.EquipStartingGear(_entity, _gear, null);
+                _spawnSys.EquipStartingGear(_entity, _gear);
                 server.EntMan.DeleteEntity(_entity);
             }
         });
index 70fe1916584eb44ad57a62c540f9fac64d74cbda..65e95b76f0810aaf0ac32b929567429646e4bd5d 100644 (file)
@@ -21,6 +21,7 @@ using Content.Shared.Module;
 using Content.Client.Guidebook;
 using Content.Client.Replay;
 using Content.Shared.Administration.Managers;
+using Content.Shared.Players.PlayTimeTracking;
 
 
 namespace Content.Client.IoC
@@ -29,26 +30,29 @@ namespace Content.Client.IoC
     {
         public static void Register()
         {
-            IoCManager.Register<IParallaxManager, ParallaxManager>();
-            IoCManager.Register<IChatManager, ChatManager>();
-            IoCManager.Register<IClientPreferencesManager, ClientPreferencesManager>();
-            IoCManager.Register<IStylesheetManager, StylesheetManager>();
-            IoCManager.Register<IScreenshotHook, ScreenshotHook>();
-            IoCManager.Register<FullscreenHook, FullscreenHook>();
-            IoCManager.Register<IClickMapManager, ClickMapManager>();
-            IoCManager.Register<IClientAdminManager, ClientAdminManager>();
-            IoCManager.Register<ISharedAdminManager, ClientAdminManager>();
-            IoCManager.Register<EuiManager, EuiManager>();
-            IoCManager.Register<IVoteManager, VoteManager>();
-            IoCManager.Register<ChangelogManager, ChangelogManager>();
-            IoCManager.Register<RulesManager, RulesManager>();
-            IoCManager.Register<ViewportManager, ViewportManager>();
-            IoCManager.Register<ISharedAdminLogManager, SharedAdminLogManager>();
-            IoCManager.Register<GhostKickManager>();
-            IoCManager.Register<ExtendedDisconnectInformationManager>();
-            IoCManager.Register<JobRequirementsManager>();
-            IoCManager.Register<DocumentParsingManager>();
-            IoCManager.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
+            var collection = IoCManager.Instance!;
+
+            collection.Register<IParallaxManager, ParallaxManager>();
+            collection.Register<IChatManager, ChatManager>();
+            collection.Register<IClientPreferencesManager, ClientPreferencesManager>();
+            collection.Register<IStylesheetManager, StylesheetManager>();
+            collection.Register<IScreenshotHook, ScreenshotHook>();
+            collection.Register<FullscreenHook, FullscreenHook>();
+            collection.Register<IClickMapManager, ClickMapManager>();
+            collection.Register<IClientAdminManager, ClientAdminManager>();
+            collection.Register<ISharedAdminManager, ClientAdminManager>();
+            collection.Register<EuiManager, EuiManager>();
+            collection.Register<IVoteManager, VoteManager>();
+            collection.Register<ChangelogManager, ChangelogManager>();
+            collection.Register<RulesManager, RulesManager>();
+            collection.Register<ViewportManager, ViewportManager>();
+            collection.Register<ISharedAdminLogManager, SharedAdminLogManager>();
+            collection.Register<GhostKickManager>();
+            collection.Register<ExtendedDisconnectInformationManager>();
+            collection.Register<JobRequirementsManager>();
+            collection.Register<DocumentParsingManager>();
+            collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
+            collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
         }
     }
 }
index fe31dce062911113ab817228c20ae4613efa2acb..98c109afde18f30ef1a19fbad0bcab00cd1937d0 100644 (file)
@@ -70,7 +70,7 @@ namespace Content.Client.Lobby
             _characterSetup.SaveButton.OnPressed += _ =>
             {
                 _characterSetup.Save();
-                _lobby.CharacterPreview.UpdateUI();
+                _userInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
             };
 
             LayoutContainer.SetAnchorPreset(_lobby, LayoutContainer.LayoutPreset.Wide);
@@ -84,10 +84,6 @@ namespace Content.Client.Lobby
             _gameTicker.InfoBlobUpdated += UpdateLobbyUi;
             _gameTicker.LobbyStatusUpdated += LobbyStatusUpdated;
             _gameTicker.LobbyLateJoinStatusUpdated += LobbyLateJoinStatusUpdated;
-
-            _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
-
-            _lobby.CharacterPreview.UpdateUI();
         }
 
         protected override void Shutdown()
@@ -109,13 +105,6 @@ namespace Content.Client.Lobby
 
             _characterSetup?.Dispose();
             _characterSetup = null;
-
-            _preferencesManager.OnServerDataLoaded -= PreferencesDataLoaded;
-        }
-
-        private void PreferencesDataLoaded()
-        {
-            _lobby?.CharacterPreview.UpdateUI();
         }
 
         private void OnSetupPressed(BaseButton.ButtonEventArgs args)
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
new file mode 100644 (file)
index 0000000..19f43e0
--- /dev/null
@@ -0,0 +1,223 @@
+using System.Linq;
+using Content.Client.Humanoid;
+using Content.Client.Inventory;
+using Content.Client.Lobby.UI;
+using Content.Client.Preferences;
+using Content.Client.Station;
+using Content.Shared.Clothing;
+using Content.Shared.GameTicking;
+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 Robust.Client.State;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controllers;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+
+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 IPrototypeManager _prototypeManager = default!;
+    [UISystemDependency] private readonly HumanoidAppearanceSystem _humanoid = default!;
+    [UISystemDependency] private readonly ClientInventorySystem _inventory = default!;
+    [UISystemDependency] private readonly StationSpawningSystem _spawn = default!;
+
+    private LobbyCharacterPreviewPanel? _previewPanel;
+
+    /*
+     * Each character profile has its own dummy. There is also a dummy for the lobby screen + character editor
+     * that is shared too.
+     */
+
+    /// <summary>
+    /// Preview dummy for role gear.
+    /// </summary>
+    private EntityUid? _previewDummy;
+
+    /// <summary>
+    /// If we currently have a loadout selected.
+    /// </summary>
+    private JobPrototype? _dummyJob;
+
+    // TODO: Load the species directly and don't update entity ever.
+    public event Action<EntityUid>? PreviewDummyUpdated;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+        _preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
+    }
+
+    private void PreferencesDataLoaded()
+    {
+        UpdateCharacterUI();
+    }
+
+    public void OnStateEntered(LobbyState state)
+    {
+    }
+
+    public void OnStateExited(LobbyState state)
+    {
+        EntityManager.DeleteEntity(_previewDummy);
+        _previewDummy = null;
+    }
+
+    public void SetPreviewPanel(LobbyCharacterPreviewPanel? panel)
+    {
+        _previewPanel = panel;
+        UpdateCharacterUI();
+    }
+
+    public void SetDummyJob(JobPrototype? job)
+    {
+        _dummyJob = job;
+        UpdateCharacterUI();
+    }
+
+    public void UpdateCharacterUI()
+    {
+        // Test moment
+        if (_stateManager.CurrentState is not LobbyState)
+            return;
+
+        if (!_preferencesManager.ServerDataLoaded)
+        {
+            _previewPanel?.SetLoaded(false);
+            return;
+        }
+
+        _previewPanel?.SetLoaded(true);
+
+        if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
+        {
+            _previewPanel?.SetSummaryText(string.Empty);
+        }
+        else
+        {
+            EntityManager.DeleteEntity(_previewDummy);
+            _previewDummy = EntityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
+            _previewPanel?.SetSprite(_previewDummy.Value);
+            _previewPanel?.SetSummaryText(selectedCharacter.Summary);
+            _humanoid.LoadProfile(_previewDummy.Value, selectedCharacter);
+
+            GiveDummyJobClothesLoadout(_previewDummy.Value, selectedCharacter);
+            PreviewDummyUpdated?.Invoke(_previewDummy.Value);
+        }
+    }
+
+    /// <summary>
+    /// Applies the highest priority job's clothes to the dummy.
+    /// </summary>
+    public void GiveDummyJobClothesLoadout(EntityUid dummy, HumanoidCharacterProfile profile)
+    {
+        var job = _dummyJob ?? GetPreferredJob(profile);
+        GiveDummyJobClothes(dummy, profile, job);
+
+        if (_prototypeManager.HasIndex<RoleLoadoutPrototype>(LoadoutSystem.GetJobPrototype(job.ID)))
+        {
+            var loadout = profile.GetLoadoutOrDefault(LoadoutSystem.GetJobPrototype(job.ID), EntityManager, _prototypeManager);
+            GiveDummyLoadout(dummy, loadout);
+        }
+    }
+
+    /// <summary>
+    /// Gets the highest priority job for the profile.
+    /// </summary>
+    public JobPrototype GetPreferredJob(HumanoidCharacterProfile profile)
+    {
+        var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
+        // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
+        return _prototypeManager.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
+    }
+
+    public void GiveDummyLoadout(EntityUid uid, RoleLoadout? roleLoadout)
+    {
+        if (roleLoadout == null)
+            return;
+
+        foreach (var group in roleLoadout.SelectedLoadouts.Values)
+        {
+            foreach (var loadout in group)
+            {
+                if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
+                    continue;
+
+                _spawn.EquipStartingGear(uid, _prototypeManager.Index(loadoutProto.Equipment));
+            }
+        }
+    }
+
+    /// <summary>
+    /// Applies the specified job's clothes to the dummy.
+    /// </summary>
+    public void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile, JobPrototype job)
+    {
+        if (!_inventory.TryGetSlots(dummy, out var slots))
+            return;
+
+        // Apply loadout
+        if (profile.Loadouts.TryGetValue(job.ID, out var jobLoadout))
+        {
+            foreach (var loadouts in jobLoadout.SelectedLoadouts.Values)
+            {
+                foreach (var loadout in loadouts)
+                {
+                    if (!_prototypeManager.TryIndex(loadout.Prototype, out var loadoutProto))
+                        continue;
+
+                    // TODO: Need some way to apply starting gear to an entity coz holy fucking shit dude.
+                    var loadoutGear = _prototypeManager.Index(loadoutProto.Equipment);
+
+                    foreach (var slot in slots)
+                    {
+                        var itemType = loadoutGear.GetGear(slot.Name);
+
+                        if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+                        {
+                            EntityManager.DeleteEntity(unequippedItem.Value);
+                        }
+
+                        if (itemType != string.Empty)
+                        {
+                            var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+                            _inventory.TryEquip(dummy, item, slot.Name, true, true);
+                        }
+                    }
+                }
+            }
+        }
+
+        if (job.StartingGear == null)
+            return;
+
+        var gear = _prototypeManager.Index<StartingGearPrototype>(job.StartingGear);
+
+        foreach (var slot in slots)
+        {
+            var itemType = gear.GetGear(slot.Name);
+
+            if (_inventory.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
+            {
+                EntityManager.DeleteEntity(unequippedItem.Value);
+            }
+
+            if (itemType != string.Empty)
+            {
+                var item = EntityManager.SpawnEntity(itemType, MapCoordinates.Nullspace);
+                _inventory.TryEquip(dummy, item, slot.Name, true, true);
+            }
+        }
+    }
+
+    public EntityUid? GetPreviewDummy()
+    {
+        return _previewDummy;
+    }
+}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.cs
deleted file mode 100644 (file)
index f9481ca..0000000
+++ /dev/null
@@ -1,166 +0,0 @@
-using System.Linq;
-using System.Numerics;
-using Content.Client.Alerts;
-using Content.Client.Humanoid;
-using Content.Client.Inventory;
-using Content.Client.Preferences;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.GameTicking;
-using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Inventory;
-using Content.Shared.Preferences;
-using Content.Shared.Roles;
-using Robust.Client.GameObjects;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Map;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
-
-namespace Content.Client.Lobby.UI
-{
-    public sealed class LobbyCharacterPreviewPanel : Control
-    {
-        [Dependency] private readonly IEntityManager _entityManager = default!;
-        [Dependency] private readonly IClientPreferencesManager _preferencesManager = default!;
-        [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
-
-
-        private EntityUid? _previewDummy;
-        private readonly Label _summaryLabel;
-        private readonly BoxContainer _loaded;
-        private readonly BoxContainer _viewBox;
-        private readonly Label _unloaded;
-
-        public LobbyCharacterPreviewPanel()
-        {
-            IoCManager.InjectDependencies(this);
-            var header = new NanoHeading
-            {
-                Text = Loc.GetString("lobby-character-preview-panel-header")
-            };
-
-            CharacterSetupButton = new Button
-            {
-                Text = Loc.GetString("lobby-character-preview-panel-character-setup-button"),
-                HorizontalAlignment = HAlignment.Center,
-                Margin = new Thickness(0, 5, 0, 0),
-            };
-
-            _summaryLabel = new Label
-            {
-                HorizontalAlignment = HAlignment.Center,
-                Margin = new Thickness(3, 3),
-            };
-
-            var vBox = new BoxContainer
-            {
-                Orientation = LayoutOrientation.Vertical
-            };
-            _unloaded = new Label { Text = Loc.GetString("lobby-character-preview-panel-unloaded-preferences-label") };
-
-            _loaded = new BoxContainer
-            {
-                Orientation = LayoutOrientation.Vertical,
-                Visible = false
-            };
-            _viewBox = new BoxContainer
-            {
-                Orientation = LayoutOrientation.Horizontal,
-                HorizontalAlignment = HAlignment.Center,
-            };
-            var _vSpacer = new VSpacer();
-
-            _loaded.AddChild(_summaryLabel);
-            _loaded.AddChild(_viewBox);
-            _loaded.AddChild(_vSpacer);
-            _loaded.AddChild(CharacterSetupButton);
-
-            vBox.AddChild(header);
-            vBox.AddChild(_loaded);
-            vBox.AddChild(_unloaded);
-            AddChild(vBox);
-
-            UpdateUI();
-        }
-
-        public Button CharacterSetupButton { get; }
-
-        protected override void Dispose(bool disposing)
-        {
-            base.Dispose(disposing);
-
-            if (!disposing) return;
-            if (_previewDummy != null) _entityManager.DeleteEntity(_previewDummy.Value);
-            _previewDummy = default;
-        }
-
-        public void UpdateUI()
-        {
-            if (!_preferencesManager.ServerDataLoaded)
-            {
-                _loaded.Visible = false;
-                _unloaded.Visible = true;
-            }
-            else
-            {
-                _loaded.Visible = true;
-                _unloaded.Visible = false;
-                if (_preferencesManager.Preferences?.SelectedCharacter is not HumanoidCharacterProfile selectedCharacter)
-                {
-                    _summaryLabel.Text = string.Empty;
-                }
-                else
-                {
-                    _previewDummy = _entityManager.SpawnEntity(_prototypeManager.Index<SpeciesPrototype>(selectedCharacter.Species).DollPrototype, MapCoordinates.Nullspace);
-                    _viewBox.DisposeAllChildren();
-                    var spriteView = new SpriteView
-                    {
-                        OverrideDirection = Direction.South,
-                        Scale = new Vector2(4f, 4f),
-                        MaxSize = new Vector2(112, 112),
-                        Stretch = SpriteView.StretchMode.Fill,
-                    };
-                    spriteView.SetEntity(_previewDummy.Value);
-                    _viewBox.AddChild(spriteView);
-                    _summaryLabel.Text = selectedCharacter.Summary;
-                    _entityManager.System<HumanoidAppearanceSystem>().LoadProfile(_previewDummy.Value, selectedCharacter);
-                    GiveDummyJobClothes(_previewDummy.Value, selectedCharacter);
-                }
-            }
-        }
-
-        public static void GiveDummyJobClothes(EntityUid dummy, HumanoidCharacterProfile profile)
-        {
-            var protoMan = IoCManager.Resolve<IPrototypeManager>();
-            var entMan = IoCManager.Resolve<IEntityManager>();
-            var invSystem = EntitySystem.Get<ClientInventorySystem>();
-
-            var highPriorityJob = profile.JobPriorities.FirstOrDefault(p => p.Value == JobPriority.High).Key;
-
-            // ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract (what is resharper smoking?)
-            var job = protoMan.Index<JobPrototype>(highPriorityJob ?? SharedGameTicker.FallbackOverflowJob);
-
-            if (job.StartingGear != null && invSystem.TryGetSlots(dummy, out var slots))
-            {
-                var gear = protoMan.Index<StartingGearPrototype>(job.StartingGear);
-
-                foreach (var slot in slots)
-                {
-                    var itemType = gear.GetGear(slot.Name, profile);
-
-                    if (invSystem.TryUnequip(dummy, slot.Name, out var unequippedItem, silent: true, force: true, reparent: false))
-                    {
-                        entMan.DeleteEntity(unequippedItem.Value);
-                    }
-
-                    if (itemType != string.Empty)
-                    {
-                        var item = entMan.SpawnEntity(itemType, MapCoordinates.Nullspace);
-                        invSystem.TryEquip(dummy, item, slot.Name, true, true);
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml
new file mode 100644 (file)
index 0000000..9975074
--- /dev/null
@@ -0,0 +1,22 @@
+<Control
+    xmlns="https://spacestation14.io"
+    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
+    <BoxContainer Name="VBox" Orientation="Vertical">
+        <controls:NanoHeading Name="Header" Text="{Loc 'lobby-character-preview-panel-header'}">
+
+        </controls:NanoHeading>
+        <BoxContainer Name="Loaded" Orientation="Vertical"
+                      Visible="False">
+            <Label Name="Summary" HorizontalAlignment="Center" Margin="3 3"/>
+            <BoxContainer Name="ViewBox" Orientation="Horizontal" HorizontalAlignment="Center">
+
+            </BoxContainer>
+            <controls:VSpacer/>
+            <Button Name="CharacterSetup" Text="{Loc 'lobby-character-preview-panel-character-setup-button'}"
+                    HorizontalAlignment="Center"
+                    Margin="0 5 0 0"/>
+        </BoxContainer>
+        <Label Name="Unloaded" Text="{Loc 'lobby-character-preview-panel-unloaded-preferences-label'}"/>
+    </BoxContainer>
+</Control>
diff --git a/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs b/Content.Client/Lobby/UI/LobbyCharacterPreviewPanel.xaml.cs
new file mode 100644 (file)
index 0000000..b0dcbc2
--- /dev/null
@@ -0,0 +1,45 @@
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lobby.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LobbyCharacterPreviewPanel : Control
+{
+    public Button CharacterSetupButton => CharacterSetup;
+
+    public LobbyCharacterPreviewPanel()
+    {
+        RobustXamlLoader.Load(this);
+        UserInterfaceManager.GetUIController<LobbyUIController>().SetPreviewPanel(this);
+    }
+
+    public void SetLoaded(bool value)
+    {
+        Loaded.Visible = value;
+        Unloaded.Visible = !value;
+    }
+
+    public void SetSummaryText(string value)
+    {
+        Summary.Text = string.Empty;
+    }
+
+    public void SetSprite(EntityUid uid)
+    {
+        ViewBox.DisposeAllChildren();
+        var spriteView = new SpriteView
+        {
+            OverrideDirection = Direction.South,
+            Scale = new Vector2(4f, 4f),
+            MaxSize = new Vector2(112, 112),
+            Stretch = SpriteView.StretchMode.Fill,
+        };
+        spriteView.SetEntity(uid);
+        ViewBox.AddChild(spriteView);
+    }
+}
index 69867ea90cb4dfb1d3889d2a671f2ed1dbe46005..5a0b580262ee178d4b091987c251c02f201fe6d8 100644 (file)
@@ -1,23 +1,9 @@
-using Content.Client.Chat.UI;
-using Content.Client.Info;
 using Content.Client.Message;
-using Content.Client.Preferences;
-using Content.Client.Preferences.UI;
-using Content.Client.UserInterface.Screens;
-using Content.Client.UserInterface.Systems.Chat.Widgets;
 using Content.Client.UserInterface.Systems.EscapeMenu;
 using Robust.Client.AutoGenerated;
 using Robust.Client.Console;
-using Robust.Client.Graphics;
-using Robust.Client.State;
 using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Maths;
-using Robust.Shared.Prototypes;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
 
 namespace Content.Client.Lobby.UI
 {
index 82ac4aa28f3781a9400020b1ea1e9bac962b498a..8193f795696b9510afd060549de620853c224cb8 100644 (file)
@@ -7,12 +7,13 @@ using Robust.Client;
 using Robust.Client.Player;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
+using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
 
 namespace Content.Client.Players.PlayTimeTracking;
 
-public sealed class JobRequirementsManager
+public sealed class JobRequirementsManager : ISharedPlaytimeManager
 {
     [Dependency] private readonly IBaseClient _client = default!;
     [Dependency] private readonly IClientNetManager _net = default!;
@@ -133,5 +134,13 @@ public sealed class JobRequirementsManager
         }
     }
 
+    public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
+    {
+        if (session != _playerManager.LocalSession)
+        {
+            return new Dictionary<string, TimeSpan>();
+        }
 
+        return _roles;
+    }
 }
index b518493c9d4ddd3b6e2187785d39fa4c5d46f618..89cee7bf79b9e04c16552d0856df7a5005850d9c 100644 (file)
@@ -1,10 +1,8 @@
-using System;
-using System.Collections.Generic;
 using System.Linq;
 using Content.Shared.Preferences;
 using Robust.Client;
+using Robust.Client.Player;
 using Robust.Shared.Configuration;
-using Robust.Shared.IoC;
 using Robust.Shared.Network;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Utility;
@@ -20,8 +18,7 @@ namespace Content.Client.Preferences
     {
         [Dependency] private readonly IClientNetManager _netManager = default!;
         [Dependency] private readonly IBaseClient _baseClient = default!;
-        [Dependency] private readonly IConfigurationManager _cfg = default!;
-        [Dependency] private readonly IPrototypeManager _prototypes = default!;
+        [Dependency] private readonly IPlayerManager _playerManager = default!;
 
         public event Action? OnServerDataLoaded;
 
@@ -64,7 +61,8 @@ namespace Content.Client.Preferences
 
         public void UpdateCharacter(ICharacterProfile profile, int slot)
         {
-            profile.EnsureValid(_cfg, _prototypes);
+            var collection = IoCManager.Instance!;
+            profile.EnsureValid(_playerManager.LocalSession!, collection);
             var characters = new Dictionary<int, ICharacterProfile>(Preferences.Characters) {[slot] = profile};
             Preferences = new PlayerPreferences(characters, Preferences.SelectedCharacterIndex, Preferences.AdminOOCColor);
             var msg = new MsgUpdateCharacter
diff --git a/Content.Client/Preferences/UI/AntagPreferenceSelector.cs b/Content.Client/Preferences/UI/AntagPreferenceSelector.cs
new file mode 100644 (file)
index 0000000..654c393
--- /dev/null
@@ -0,0 +1,41 @@
+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);
+        }
+    }
+}
index 9a76029ce0b716655ac18aec6128eb62d4a23718..35067eebfd10798ed51255d435d01a954ca1df1b 100644 (file)
@@ -40,7 +40,7 @@
                         <gfx:StyleBoxFlat BackgroundColor="{x:Static style:StyleNano.NanoGold}" ContentMarginTopOverride="2" />
                     </PanelContainer.PanelOverride>
                 </PanelContainer>
-                <BoxContainer Name="CharEditor" />
+                <BoxContainer Name="CharEditor" HorizontalExpand="True" />
             </BoxContainer>
         </BoxContainer>
     </Control>
index f1052086de6603b53ce11cf017b6373ab8506a70..0dcd5e6ad30d9bba601b22af6c3d963cac0c6479 100644 (file)
@@ -3,27 +3,23 @@ using System.Numerics;
 using Content.Client.Humanoid;
 using Content.Client.Info;
 using Content.Client.Info.PlaytimeStats;
-using Content.Client.Lobby.UI;
+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.GameObjects;
 using Robust.Client.Graphics;
 using Robust.Client.ResourceManagement;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Configuration;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Localization;
 using Robust.Shared.Map;
-using Robust.Shared.Maths;
 using Robust.Shared.Prototypes;
 using static Robust.Client.UserInterface.Controls.BoxContainer;
 using Direction = Robust.Shared.Maths.Direction;
@@ -36,7 +32,6 @@ namespace Content.Client.Preferences.UI
         private readonly IClientPreferencesManager _preferencesManager;
         private readonly IEntityManager _entityManager;
         private readonly IPrototypeManager _prototypeManager;
-        private readonly IConfigurationManager _configurationManager;
         private readonly Button _createNewCharacterButton;
         private readonly HumanoidProfileEditor _humanoidProfileEditor;
 
@@ -51,7 +46,6 @@ namespace Content.Client.Preferences.UI
             _entityManager = entityManager;
             _prototypeManager = prototypeManager;
             _preferencesManager = preferencesManager;
-            _configurationManager = configurationManager;
 
             var panelTex = resourceCache.GetTexture("/Textures/Interface/Nano/button.svg.96dpi.png");
             var back = new StyleBoxTexture
@@ -74,7 +68,7 @@ namespace Content.Client.Preferences.UI
                 args.Event.Handle();
             };
 
-            _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, entityManager, configurationManager);
+            _humanoidProfileEditor = new HumanoidProfileEditor(preferencesManager, prototypeManager, configurationManager);
             _humanoidProfileEditor.OnProfileChanged += ProfileChanged;
             CharEditor.AddChild(_humanoidProfileEditor);
 
@@ -105,6 +99,7 @@ namespace Content.Client.Preferences.UI
 
         private void UpdateUI()
         {
+            UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
             var numberOfFullSlots = 0;
             var characterButtonsGroup = new ButtonGroup();
             Characters.RemoveAllChildren();
@@ -120,11 +115,6 @@ namespace Content.Client.Preferences.UI
 
             foreach (var (slot, character) in _preferencesManager.Preferences!.Characters)
             {
-                if (character is null)
-                {
-                    continue;
-                }
-
                 numberOfFullSlots++;
                 var characterPickerButton = new CharacterPickerButton(_entityManager,
                     _preferencesManager,
@@ -148,8 +138,12 @@ namespace Content.Client.Preferences.UI
             _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;
@@ -180,7 +174,15 @@ namespace Content.Client.Preferences.UI
 
                 if (humanoid != null)
                 {
-                    LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy, humanoid);
+                    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;
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml b/Content.Client/Preferences/UI/HighlightedContainer.xaml
new file mode 100644 (file)
index 0000000..8cf6e2d
--- /dev/null
@@ -0,0 +1,11 @@
+<PanelContainer
+    xmlns="https://spacestation14.io"
+    xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
+    <PanelContainer.PanelOverride>
+        <graphics:StyleBoxFlat BackgroundColor="#2F2F35"
+                               ContentMarginTopOverride="10"
+                               ContentMarginBottomOverride="10"
+                               ContentMarginLeftOverride="10"
+                               ContentMarginRightOverride="10"/>
+    </PanelContainer.PanelOverride>
+</PanelContainer>
diff --git a/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs b/Content.Client/Preferences/UI/HighlightedContainer.xaml.cs
new file mode 100644 (file)
index 0000000..68294d0
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class HighlightedContainer : PanelContainer
+{
+    public HighlightedContainer()
+    {
+        RobustXamlLoader.Load(this);
+    }
+}
index c9e184dfc238b56e211e3fc5237d2ea48d3ef184..750006bf7a98804e56f3cb9ae51beac08badf0e9 100644 (file)
@@ -5,8 +5,6 @@ namespace Content.Client.Preferences.UI
 {
     public sealed partial class HumanoidProfileEditor
     {
-        private readonly IPrototypeManager _prototypeManager;
-
         private void RandomizeEverything()
         {
             Profile = HumanoidCharacterProfile.Random();
index d0dd02a58add776833f171a357799f32d14a226e..5926aee8987e56fd3e051b08bf81f971a48a53ff 100644 (file)
@@ -1,11 +1,11 @@
-<Control xmlns="https://spacestation14.io"
+<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">
-    <BoxContainer Orientation="Horizontal">
+         xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
+         HorizontalExpand="True">
         <!-- Left side -->
-        <BoxContainer Orientation="Vertical" Margin="10 10 10 10">
+        <BoxContainer Orientation="Vertical" Margin="10 10 10 10" HorizontalExpand="True">
             <!-- Middle container -->
             <BoxContainer Orientation="Horizontal" SeparationOverride="10">
                 <!-- Name box-->
@@ -58,7 +58,9 @@
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-species-label'}" />
                                     <Control HorizontalExpand="True"/>
-                                    <TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3" VerticalAlignment="Center"></TextureButton>
+                                    <TextureButton Name="SpeciesInfoButton" Scale="0.3 0.3"
+                                                   VerticalAlignment="Center"
+                                                   ToolTip="{Loc 'humanoid-profile-editor-guidebook-button-tooltip'}"/>
                                     <OptionButton Name="CSpeciesButton" HorizontalAlignment="Right" />
                                 </BoxContainer>
                                 <!-- Age -->
                                     <Control HorizontalExpand="True"/>
                                     <Button Name="ShowClothes" Pressed="True" ToggleMode="True" Text="{Loc 'humanoid-profile-editor-clothing-show'}" HorizontalAlignment="Right" />
                                 </BoxContainer>
-                                <!-- Clothing -->
-                                <BoxContainer HorizontalExpand="True">
-                                    <Label Text="{Loc 'humanoid-profile-editor-clothing-label'}" />
-                                    <Control HorizontalExpand="True"/>
-                                    <OptionButton Name="CClothingButton" HorizontalAlignment="Right" />
-                                </BoxContainer>
-                                <!-- Backpack -->
-                                <BoxContainer HorizontalExpand="True">
-                                    <Label Text="{Loc 'humanoid-profile-editor-backpack-label'}" />
-                                    <Control HorizontalExpand="True"/>
-                                    <OptionButton Name="CBackpackButton" HorizontalAlignment="Right" />
-                                </BoxContainer>
                                 <!-- Spawn Priority -->
                                 <BoxContainer HorizontalExpand="True">
                                     <Label Text="{Loc 'humanoid-profile-editor-spawn-priority-label'}" />
             </TabContainer>
         </BoxContainer>
         <!-- Right side -->
-        <BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Center">
+        <BoxContainer Orientation="Vertical" VerticalExpand="True" VerticalAlignment="Center">
             <SpriteView Name="CSpriteView" Scale="8 8" SizeFlagsStretchRatio="1" />
             <BoxContainer Orientation="Horizontal" HorizontalAlignment="Center" Margin="0 5">
                 <Button Name="CSpriteRotateLeft" Text="◀" StyleClasses="OpenRight" />
                 <Button Name="CSpriteRotateRight" Text="▶" StyleClasses="OpenLeft" />
             </BoxContainer>
         </BoxContainer>
-    </BoxContainer>
-</Control>
+</BoxContainer>
index afc9c6eb46ed5392baaecd7f656c0d108f9a77da..e1135523f1ab68851643dbcc68a58bf5837a8690 100644 (file)
@@ -2,69 +2,48 @@ using System.Linq;
 using System.Numerics;
 using Content.Client.Guidebook;
 using Content.Client.Humanoid;
-using Content.Client.Lobby.UI;
+using Content.Client.Lobby;
 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;
 using Content.Shared.GameTicking;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Inventory;
 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.GameObjects;
 using Robust.Client.Graphics;
 using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Client.Utility;
 using Robust.Shared.Configuration;
 using Robust.Shared.Enums;
-using Robust.Shared.Map;
 using Robust.Shared.Prototypes;
-using Robust.Shared.Random;
-using Robust.Shared.Timing;
 using Robust.Shared.Utility;
-using static Robust.Client.UserInterface.Controls.BoxContainer;
 using Direction = Robust.Shared.Maths.Direction;
 
 namespace Content.Client.Preferences.UI
 {
-    public sealed class HighlightedContainer : PanelContainer
-    {
-        public HighlightedContainer()
-        {
-            PanelOverride = new StyleBoxFlat()
-            {
-                BackgroundColor = new Color(47, 47, 53),
-                ContentMarginTopOverride = 10,
-                ContentMarginBottomOverride = 10,
-                ContentMarginLeftOverride = 10,
-                ContentMarginRightOverride = 10
-            };
-        }
-    }
-
     [GenerateTypedNameReferences]
-    public sealed partial class HumanoidProfileEditor : Control
+    public sealed partial class HumanoidProfileEditor : BoxContainer
     {
         private readonly IClientPreferencesManager _preferencesManager;
-        private readonly IEntityManager _entMan;
-        private readonly IConfigurationManager _configurationManager;
+        private readonly IPrototypeManager _prototypeManager;
         private readonly MarkingManager _markingManager;
         private readonly JobRequirementsManager _requirements;
 
         private LineEdit _ageEdit => CAgeEdit;
         private LineEdit _nameEdit => CNameEdit;
-        private TextEdit _flavorTextEdit = null!;
+        private TextEdit? _flavorTextEdit;
         private Button _nameRandomButton => CNameRandomize;
         private Button _randomizeEverythingButton => CRandomizeEverything;
         private RichTextLabel _warningLabel => CWarningLabel;
@@ -72,8 +51,6 @@ namespace Content.Client.Preferences.UI
         private OptionButton _sexButton => CSexButton;
         private OptionButton _genderButton => CPronounsButton;
         private Slider _skinColor => CSkin;
-        private OptionButton _clothingButton => CClothingButton;
-        private OptionButton _backpackButton => CBackpackButton;
         private OptionButton _spawnPriorityButton => CSpawnPriorityButton;
         private SingleMarkingPicker _hairPicker => CHairStylePicker;
         private SingleMarkingPicker _facialHairPicker => CFacialHairPicker;
@@ -88,44 +65,39 @@ namespace Content.Client.Preferences.UI
         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;
+        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 EntityUid? _previewDummy;
 
         private BoxContainer _rgbSkinColorContainer => CRgbSkinColorContainer;
         private ColorSelectorSliders _rgbSkinColorSelector;
 
         private bool _isDirty;
-        private bool _needUpdatePreview;
         public int CharacterSlot;
         public HumanoidCharacterProfile? Profile;
-        private MarkingSet _markingSet = new(); // storing this here feels iffy but a few things need it this high up
 
         public event Action<HumanoidCharacterProfile, int>? OnProfileChanged;
 
-        public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager,
-            IEntityManager entityManager, IConfigurationManager configurationManager)
+        [ValidatePrototypeId<GuideEntryPrototype>]
+        private const string DefaultSpeciesGuidebook = "Species";
+
+        public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IPrototypeManager prototypeManager, IConfigurationManager configurationManager)
         {
             RobustXamlLoader.Load(this);
             _prototypeManager = prototypeManager;
-            _entMan = entityManager;
             _preferencesManager = preferencesManager;
-            _configurationManager = configurationManager;
             _markingManager = IoCManager.Resolve<MarkingManager>();
+            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
+            controller.PreviewDummyUpdated += OnDummyUpdate;
 
-            SpeciesInfoButton.ToolTip = Loc.GetString("humanoid-profile-editor-guidebook-button-tooltip");
+            _previewSpriteView.SetEntity(controller.GetPreviewDummy());
 
             #region Left
 
-            #region Randomize
-
-            #endregion Randomize
-
             #region Name
 
             _nameEdit.OnTextChanged += args => { SetName(args.Text); };
@@ -139,8 +111,6 @@ namespace Content.Client.Preferences.UI
 
             _tabContainer.SetTabTitle(0, Loc.GetString("humanoid-profile-editor-appearance-tab"));
 
-            ShowClothes.OnPressed += ToggleClothes;
-
             #region Sex
 
             _sexButton.OnItemSelected += args =>
@@ -318,33 +288,6 @@ namespace Content.Client.Preferences.UI
 
             #endregion Hair
 
-            #region Clothing
-
-            _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpsuit"), (int) ClothingPreference.Jumpsuit);
-            _clothingButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-jumpskirt"), (int) ClothingPreference.Jumpskirt);
-
-            _clothingButton.OnItemSelected += args =>
-            {
-                _clothingButton.SelectId(args.Id);
-                SetClothing((ClothingPreference) args.Id);
-            };
-
-            #endregion Clothing
-
-            #region Backpack
-
-            _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-backpack"), (int) BackpackPreference.Backpack);
-            _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-satchel"), (int) BackpackPreference.Satchel);
-            _backpackButton.AddItem(Loc.GetString("humanoid-profile-editor-preference-duffelbag"), (int) BackpackPreference.Duffelbag);
-
-            _backpackButton.OnItemSelected += args =>
-            {
-                _backpackButton.SelectId(args.Id);
-                SetBackpack((BackpackPreference) args.Id);
-            };
-
-            #endregion Backpack
-
             #region SpawnPriority
 
             foreach (var value in Enum.GetValues<SpawnPriorityPreference>())
@@ -399,40 +342,16 @@ namespace Content.Client.Preferences.UI
             _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();
 
             #endregion Jobs
 
-            #region Antags
-
             _tabContainer.SetTabTitle(2, Loc.GetString("humanoid-profile-editor-antags-tab"));
 
-            _antagPreferences = new List<AntagPreferenceSelector>();
-
-            foreach (var antag in prototypeManager.EnumeratePrototypes<AntagPrototype>().OrderBy(a => Loc.GetString(a.Name)))
-            {
-                if (!antag.SetPreference)
-                    continue;
-
-                var selector = new AntagPreferenceSelector(antag);
-                _antagList.AddChild(selector);
-                _antagPreferences.Add(selector);
-                if (selector.Disabled)
-                {
-                    Profile = Profile?.WithAntagPreference(antag.ID, false);
-                    IsDirty = true;
-                }
-
-                selector.PreferenceChanged += preference =>
-                {
-                    Profile = Profile?.WithAntagPreference(antag.ID, preference);
-                    IsDirty = true;
-                };
-            }
-
-            #endregion Antags
-
             #region Traits
 
             var traits = prototypeManager.EnumeratePrototypes<TraitPrototype>().OrderBy(t => Loc.GetString(t.Name)).ToList();
@@ -483,7 +402,7 @@ namespace Content.Client.Preferences.UI
 
             #region FlavorText
 
-            if (_configurationManager.GetCVar(CCVars.FlavorText))
+            if (configurationManager.GetCVar(CCVars.FlavorText))
             {
                 var flavorText = new FlavorText.FlavorText();
                 _tabContainer.AddChild(flavorText);
@@ -500,22 +419,14 @@ namespace Content.Client.Preferences.UI
             _previewRotateLeftButton.OnPressed += _ =>
             {
                 _previewRotation = _previewRotation.TurnCw();
-                _needUpdatePreview = true;
+                SetPreviewRotation(_previewRotation);
             };
             _previewRotateRightButton.OnPressed += _ =>
             {
                 _previewRotation = _previewRotation.TurnCcw();
-                _needUpdatePreview = true;
+                SetPreviewRotation(_previewRotation);
             };
 
-            var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
-            var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
-
-            if (_previewDummy != null)
-                _entMan.DeleteEntity(_previewDummy!.Value);
-
-            _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
-            _previewSpriteView.SetEntity(_previewDummy);
             #endregion Dummy
 
             #endregion Left
@@ -538,22 +449,54 @@ namespace Content.Client.Preferences.UI
         {
             var guidebookController = UserInterfaceManager.GetUIController<GuidebookUIController>();
             var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
-            var page = "Species";
+            var page = DefaultSpeciesGuidebook;
             if (_prototypeManager.HasIndex<GuideEntryPrototype>(species))
                 page = species;
 
-            if (_prototypeManager.TryIndex<GuideEntryPrototype>("Species", out var guideRoot))
+            if (_prototypeManager.TryIndex<GuideEntryPrototype>(DefaultSpeciesGuidebook, out var guideRoot))
             {
                 var dict = new Dictionary<string, GuideEntry>();
-                dict.Add("Species", guideRoot);
+                dict.Add(DefaultSpeciesGuidebook, guideRoot);
                 //TODO: Don't close the guidebook if its already open, just go to the correct page
                 guidebookController.ToggleGuidebook(dict, includeChildren:true, selected: page);
             }
         }
 
-        private void ToggleClothes(BaseButton.ButtonEventArgs obj)
+        private void OnDummyUpdate(EntityUid value)
         {
-            RebuildSpriteView();
+            _previewSpriteView.SetEntity(value);
+        }
+
+        private void UpdateAntagRequirements()
+        {
+            _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);
+                    IsDirty = true;
+                }
+
+                selector.PreferenceChanged += preference =>
+                {
+                    Profile = Profile?.WithAntagPreference(antag.ID, preference);
+                    IsDirty = true;
+                };
+            }
+
         }
 
         private void UpdateRoleRequirements()
@@ -614,10 +557,16 @@ namespace Content.Client.Preferences.UI
                     .Where(job => job.SetPreference)
                     .ToArray();
                 Array.Sort(jobs, JobUIComparer.Instance);
+                var jobLoadoutGroup = new ButtonGroup();
 
                 foreach (var job in jobs)
                 {
-                    var selector = new JobPrioritySelector(job, _prototypeManager);
+                    RoleLoadout? loadout = null;
+                    Profile?.Loadouts.TryGetValue(LoadoutSystem.GetJobPrototype(job.ID), out loadout);
+                    var selector = new JobPrioritySelector(loadout, job, jobLoadoutGroup, _prototypeManager)
+                    {
+                        Margin = new Thickness(3f, 3f, 3f, 0f),
+                    };
 
                     if (!_requirements.IsAllowed(job, out var reason))
                     {
@@ -627,6 +576,13 @@ namespace Content.Client.Preferences.UI
                     category.AddChild(selector);
                     _jobPriorities.Add(selector);
 
+                    selector.LoadoutUpdated += args =>
+                    {
+                        Profile?.SetLoadout(args);
+                        UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
+                        IsDirty = true;
+                    };
+
                     selector.PriorityChanged += priority =>
                     {
                         Profile = Profile?.WithJobPriority(job.ID, priority);
@@ -672,20 +628,10 @@ namespace Content.Client.Preferences.UI
                 return;
 
             Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings.GetForwardEnumerator().ToList()));
-            _needUpdatePreview = true;
+            UpdatePreview();
             IsDirty = true;
         }
 
-        private void OnMarkingColorChange(List<Marking> markings)
-        {
-            if (Profile is null)
-                return;
-
-            Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithMarkings(markings));
-            IsDirty = true;
-        }
-
-
         private void OnSkinColorOnValueChanged()
         {
             if (Profile is null) return;
@@ -745,33 +691,21 @@ namespace Content.Client.Preferences.UI
             if (!disposing)
                 return;
 
-            if (_previewDummy != null)
-                _entMan.DeleteEntity(_previewDummy.Value);
-
+            var controller = UserInterfaceManager.GetUIController<LobbyUIController>();
+            controller.PreviewDummyUpdated -= OnDummyUpdate;
+            _requirements.Updated -= UpdateAntagRequirements;
             _requirements.Updated -= UpdateRoleRequirements;
             _preferencesManager.OnServerDataLoaded -= LoadServerData;
         }
 
-        private void RebuildSpriteView()
-        {
-            var species = Profile?.Species ?? SharedHumanoidAppearanceSystem.DefaultSpecies;
-            var dollProto = _prototypeManager.Index<SpeciesPrototype>(species).DollPrototype;
-
-            if (_previewDummy != null)
-                _entMan.DeleteEntity(_previewDummy!.Value);
-
-            _previewDummy = _entMan.SpawnEntity(dollProto, MapCoordinates.Nullspace);
-            _previewSpriteView.SetEntity(_previewDummy);
-            _needUpdatePreview = true;
-        }
-
         private void LoadServerData()
         {
             Profile = (HumanoidCharacterProfile) _preferencesManager.Preferences!.SelectedCharacter;
             CharacterSlot = _preferencesManager.Preferences.SelectedCharacterIndex;
 
+            UpdateAntagRequirements();
+            UpdateRoleRequirements();
             UpdateControls();
-            _needUpdatePreview = true;
         }
 
         private void SetAge(int newAge)
@@ -813,10 +747,9 @@ namespace Content.Client.Preferences.UI
             OnSkinColorOnValueChanged(); // Species may have special color prefs, make sure to update it.
             CMarkings.SetSpecies(newSpecies); // Repopulate the markings tab as well.
             UpdateSexControls(); // update sex for new species
-            RebuildSpriteView(); // they might have different inv so we need a new dummy
             UpdateSpeciesGuidebookIcon();
             IsDirty = true;
-            _needUpdatePreview = true;
+            UpdatePreview();
         }
 
         private void SetName(string newName)
@@ -825,18 +758,6 @@ namespace Content.Client.Preferences.UI
             IsDirty = true;
         }
 
-        private void SetClothing(ClothingPreference newClothing)
-        {
-            Profile = Profile?.WithClothingPreference(newClothing);
-            IsDirty = true;
-        }
-
-        private void SetBackpack(BackpackPreference newBackpack)
-        {
-            Profile = Profile?.WithBackpackPreference(newBackpack);
-            IsDirty = true;
-        }
-
         private void SetSpawnPriority(SpawnPriorityPreference newSpawnPriority)
         {
             Profile = Profile?.WithSpawnPriorityPreference(newSpawnPriority);
@@ -847,12 +768,11 @@ namespace Content.Client.Preferences.UI
         {
             IsDirty = false;
 
-            if (Profile != null)
-            {
-                _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
-                OnProfileChanged?.Invoke(Profile, CharacterSlot);
-                _needUpdatePreview = true;
-            }
+            if (Profile == null)
+                return;
+
+            _preferencesManager.UpdateCharacter(Profile, CharacterSlot);
+            OnProfileChanged?.Invoke(Profile, CharacterSlot);
         }
 
         private bool IsDirty
@@ -861,7 +781,6 @@ namespace Content.Client.Preferences.UI
             set
             {
                 _isDirty = value;
-                _needUpdatePreview = true;
                 UpdateSaveButton();
             }
         }
@@ -981,7 +900,7 @@ namespace Content.Client.Preferences.UI
             if (!_prototypeManager.HasIndex<GuideEntryPrototype>(species))
                 return;
 
-            var style = speciesProto.GuideBookIcon;
+            const string style = "SpeciesInfoDefault";
             SpeciesInfoButton.StyleClasses.Add(style);
         }
 
@@ -1017,26 +936,6 @@ namespace Content.Client.Preferences.UI
             _genderButton.SelectId((int) Profile.Gender);
         }
 
-        private void UpdateClothingControls()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-
-            _clothingButton.SelectId((int) Profile.Clothing);
-        }
-
-        private void UpdateBackpackControls()
-        {
-            if (Profile == null)
-            {
-                return;
-            }
-
-            _backpackButton.SelectId((int) Profile.Backpack);
-        }
-
         private void UpdateSpawnPriorityControls()
         {
             if (Profile == null)
@@ -1166,13 +1065,13 @@ namespace Content.Client.Preferences.UI
             if (Profile is null)
                 return;
 
-            var humanoid = _entMan.System<HumanoidAppearanceSystem>();
-            humanoid.LoadProfile(_previewDummy!.Value, Profile);
-
-            if (ShowClothes.Pressed)
-                LobbyCharacterPreviewPanel.GiveDummyJobClothes(_previewDummy!.Value, Profile);
+            UserInterfaceManager.GetUIController<LobbyUIController>().UpdateCharacterUI();
+            SetPreviewRotation(_previewRotation);
+        }
 
-            _previewSpriteView.OverrideDirection = (Direction) ((int) _previewRotation % 4 * 2);
+        private void SetPreviewRotation(Direction direction)
+        {
+            _previewSpriteView.OverrideDirection = (Direction) ((int) direction % 4 * 2);
         }
 
         public void UpdateControls()
@@ -1184,17 +1083,15 @@ namespace Content.Client.Preferences.UI
             UpdateGenderControls();
             UpdateSkinColor();
             UpdateSpecies();
-            UpdateClothingControls();
-            UpdateBackpackControls();
             UpdateSpawnPriorityControls();
             UpdateAgeEdit();
             UpdateEyePickers();
             UpdateSaveButton();
+            UpdateLoadouts();
             UpdateJobPriorities();
             UpdateAntagPreferences();
             UpdateTraitPreferences();
             UpdateMarkings();
-            RebuildSpriteView();
             UpdateHairPickers();
             UpdateCMarkingsHair();
             UpdateCMarkingsFacialHair();
@@ -1202,17 +1099,6 @@ namespace Content.Client.Preferences.UI
             _preferenceUnavailableButton.SelectId((int) Profile.PreferenceUnavailable);
         }
 
-        protected override void FrameUpdate(FrameEventArgs args)
-        {
-            base.FrameUpdate(args);
-
-            if (_needUpdatePreview)
-            {
-                UpdatePreview();
-                _needUpdatePreview = false;
-            }
-        }
-
         private void UpdateJobPriorities()
         {
             foreach (var prioritySelector in _jobPriorities)
@@ -1225,143 +1111,11 @@ namespace Content.Client.Preferences.UI
             }
         }
 
-        private abstract class RequirementsSelector<T> : Control
+        private void UpdateLoadouts()
         {
-            public T Proto { get; }
-            public bool Disabled => _lockStripe.Visible;
-
-            protected readonly RadioOptions<int> Options;
-            private StripeBack _lockStripe;
-            private Label _requirementsLabel;
-
-            protected RequirementsSelector(T proto)
-            {
-                Proto = proto;
-
-                Options = new RadioOptions<int>(RadioOptionsLayout.Horizontal)
-                {
-                    FirstButtonStyle = StyleBase.ButtonOpenRight,
-                    ButtonStyle = StyleBase.ButtonOpenBoth,
-                    LastButtonStyle = StyleBase.ButtonOpenLeft
-                };
-                //Override default radio option button width
-                Options.GenerateItem = GenerateButton;
-
-                Options.OnItemSelected += args => Options.Select(args.Id);
-
-                _requirementsLabel = new Label()
-                {
-                    Text = Loc.GetString("role-timer-locked"),
-                    Visible = true,
-                    HorizontalAlignment = HAlignment.Center,
-                    StyleClasses = {StyleBase.StyleClassLabelSubText},
-                };
-
-                _lockStripe = new StripeBack()
-                {
-                    Visible = false,
-                    HorizontalExpand = true,
-                    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((string, int)[] items, string title, int titleSize, string? description, TextureRect? icon = null)
-            {
-                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
-                };
-
-                var container = new BoxContainer
-                {
-                    Orientation = LayoutOrientation.Horizontal,
-                };
-
-                if (icon != null)
-                    container.AddChild(icon);
-                container.AddChild(titleLabel);
-                container.AddChild(Options);
-                container.AddChild(_lockStripe);
-
-                AddChild(container);
-            }
-
-            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
-                };
-            }
-        }
-
-        private sealed class JobPrioritySelector : RequirementsSelector<JobPrototype>
-        {
-            public JobPriority Priority
-            {
-                get => (JobPriority) Options.SelectedValue;
-                set => Options.SelectByValue((int) value);
-            }
-
-            public event Action<JobPriority>? PriorityChanged;
-
-            public JobPrioritySelector(JobPrototype proto, IPrototypeManager protoMan)
-                : base(proto)
+            foreach (var prioritySelector in _jobPriorities)
             {
-                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(items, proto.LocalizedName, 200, proto.LocalizedDescription, icon);
+                prioritySelector.CloseLoadout();
             }
         }
 
@@ -1386,41 +1140,6 @@ namespace Content.Client.Preferences.UI
             }
         }
 
-        private 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)
-                : base(proto)
-            {
-                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);
-                Setup(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);
-                }
-            }
-        }
-
         private sealed class TraitPreferenceSelector : Control
         {
             public TraitPrototype Trait { get; }
diff --git a/Content.Client/Preferences/UI/JobPrioritySelector.cs b/Content.Client/Preferences/UI/JobPrioritySelector.cs
new file mode 100644 (file)
index 0000000..243c78f
--- /dev/null
@@ -0,0 +1,46 @@
+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/LoadoutContainer.xaml b/Content.Client/Preferences/UI/LoadoutContainer.xaml
new file mode 100644 (file)
index 0000000..a84a4a9
--- /dev/null
@@ -0,0 +1,15 @@
+<BoxContainer Name="Container" xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+         Orientation="Horizontal"
+         HorizontalExpand="True"
+         MouseFilter="Ignore"
+         Margin="0 0 0 5">
+    <Button Name="SelectButton" ToggleMode="True" Margin="0 0 5 0" HorizontalExpand="True"/>
+    <PanelContainer SetSize="64 64" HorizontalAlignment="Right">
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
+        </PanelContainer.PanelOverride>
+        <SpriteView Name="Sprite" Scale="4 4" MouseFilter="Stop"/>
+    </PanelContainer>
+</BoxContainer>
diff --git a/Content.Client/Preferences/UI/LoadoutContainer.xaml.cs b/Content.Client/Preferences/UI/LoadoutContainer.xaml.cs
new file mode 100644 (file)
index 0000000..45a982b
--- /dev/null
@@ -0,0 +1,74 @@
+using Content.Shared.Clothing;
+using Content.Shared.Preferences.Loadouts;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Map;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutContainer : BoxContainer
+{
+    [Dependency] private readonly IEntityManager _entManager = default!;
+    [Dependency] private readonly IPrototypeManager _protoManager = default!;
+
+    private readonly EntityUid? _entity;
+
+    public Button Select => SelectButton;
+
+    public LoadoutContainer(ProtoId<LoadoutPrototype> proto, bool disabled, FormattedMessage? reason)
+    {
+        RobustXamlLoader.Load(this);
+        IoCManager.InjectDependencies(this);
+
+        SelectButton.Disabled = disabled;
+
+        if (disabled && reason != null)
+        {
+            var tooltip = new Tooltip();
+            tooltip.SetMessage(reason);
+            SelectButton.TooltipSupplier = _ => tooltip;
+        }
+
+        if (_protoManager.TryIndex(proto, out var loadProto))
+        {
+            var ent = _entManager.System<LoadoutSystem>().GetFirstOrNull(loadProto);
+
+            if (ent != null)
+            {
+                _entity = _entManager.SpawnEntity(ent, MapCoordinates.Nullspace);
+                Sprite.SetEntity(_entity);
+
+                var spriteTooltip = new Tooltip();
+                spriteTooltip.SetMessage(FormattedMessage.FromUnformatted(_entManager.GetComponent<MetaDataComponent>(_entity.Value).EntityDescription));
+                Sprite.TooltipSupplier = _ => spriteTooltip;
+            }
+        }
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+
+        if (!disposing)
+            return;
+
+        _entManager.DeleteEntity(_entity);
+    }
+
+    public bool Pressed
+    {
+        get => SelectButton.Pressed;
+        set => SelectButton.Pressed = value;
+    }
+
+    public string? Text
+    {
+        get => SelectButton.Text;
+        set => SelectButton.Text = value;
+    }
+}
diff --git a/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml
new file mode 100644 (file)
index 0000000..1e3eb14
--- /dev/null
@@ -0,0 +1,10 @@
+<BoxContainer xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         Orientation="Vertical">
+    <PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
+        <BoxContainer Name="LoadoutsContainer" Orientation="Vertical"/>
+    </PanelContainer>
+    <!-- Buffer space so we have 10 margin between controls but also 10 to the borders -->
+    <Label Text="{Loc 'loadout-restrictions'}" Margin="5 0 5 5"/>
+    <BoxContainer Name="RestrictionsContainer" Orientation="Vertical" HorizontalExpand="True" />
+</BoxContainer>
diff --git a/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs b/Content.Client/Preferences/UI/LoadoutGroupContainer.xaml.cs
new file mode 100644 (file)
index 0000000..8dc1c40
--- /dev/null
@@ -0,0 +1,93 @@
+using System.Linq;
+using Content.Shared.Clothing;
+using Content.Shared.Preferences.Loadouts;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Preferences.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutGroupContainer : BoxContainer
+{
+    private readonly LoadoutGroupPrototype _groupProto;
+
+    public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
+    public event Action<ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
+
+    public LoadoutGroupContainer(RoleLoadout loadout, LoadoutGroupPrototype groupProto, ICommonSession session, IDependencyCollection collection)
+    {
+        RobustXamlLoader.Load(this);
+        _groupProto = groupProto;
+
+        RefreshLoadouts(loadout, session, collection);
+    }
+
+    /// <summary>
+    /// Updates button availabilities and buttons.
+    /// </summary>
+    public void RefreshLoadouts(RoleLoadout loadout, ICommonSession session, IDependencyCollection collection)
+    {
+        var protoMan = collection.Resolve<IPrototypeManager>();
+        var loadoutSystem = collection.Resolve<IEntityManager>().System<LoadoutSystem>();
+        RestrictionsContainer.DisposeAllChildren();
+
+        if (_groupProto.MinLimit > 0)
+        {
+            RestrictionsContainer.AddChild(new Label()
+            {
+                Text = Loc.GetString("loadouts-min-limit", ("count", _groupProto.MinLimit)),
+                Margin = new Thickness(5, 0, 5, 5),
+            });
+        }
+
+        if (_groupProto.MaxLimit > 0)
+        {
+            RestrictionsContainer.AddChild(new Label()
+            {
+                Text = Loc.GetString("loadouts-max-limit", ("count", _groupProto.MaxLimit)),
+                Margin = new Thickness(5, 0, 5, 5),
+            });
+        }
+
+        if (protoMan.TryIndex(loadout.Role, out var roleProto) && roleProto.Points != null && loadout.Points != null)
+        {
+            RestrictionsContainer.AddChild(new Label()
+            {
+                Text = Loc.GetString("loadouts-points-limit", ("count", loadout.Points.Value), ("max", roleProto.Points.Value)),
+                Margin = new Thickness(5, 0, 5, 5),
+            });
+        }
+
+        LoadoutsContainer.DisposeAllChildren();
+        // Didn't use options because this is more robust in future.
+
+        var selected = loadout.SelectedLoadouts[_groupProto.ID];
+
+        foreach (var loadoutProto in _groupProto.Loadouts)
+        {
+            if (!protoMan.TryIndex(loadoutProto, out var loadProto))
+                continue;
+
+            var matchingLoadout = selected.FirstOrDefault(e => e.Prototype == loadoutProto);
+            var pressed = matchingLoadout != null;
+
+            var enabled = loadout.IsValid(session, loadoutProto, collection, out var reason);
+            var loadoutContainer = new LoadoutContainer(loadoutProto, !enabled, reason);
+            loadoutContainer.Select.Pressed = pressed;
+            loadoutContainer.Text = loadoutSystem.GetName(loadProto);
+
+            loadoutContainer.Select.OnPressed += args =>
+            {
+                if (args.Button.Pressed)
+                    OnLoadoutPressed?.Invoke(loadoutProto);
+                else
+                    OnLoadoutUnpressed?.Invoke(loadoutProto);
+            };
+
+            LoadoutsContainer.AddChild(loadoutContainer);
+        }
+    }
+}
diff --git a/Content.Client/Preferences/UI/LoadoutWindow.xaml b/Content.Client/Preferences/UI/LoadoutWindow.xaml
new file mode 100644 (file)
index 0000000..afa783c
--- /dev/null
@@ -0,0 +1,10 @@
+<controls:FancyWindow xmlns="https://spacestation14.io"
+         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+         xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+         SetSize="800 800"
+         MinSize="800 64">
+        <VerticalTabContainer Name="LoadoutGroupsContainer"
+                      VerticalExpand="True"
+                      HorizontalExpand="True">
+        </VerticalTabContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Preferences/UI/LoadoutWindow.xaml.cs b/Content.Client/Preferences/UI/LoadoutWindow.xaml.cs
new file mode 100644 (file)
index 0000000..8e1ef0f
--- /dev/null
@@ -0,0 +1,60 @@
+using Content.Client.Lobby;
+using Content.Client.UserInterface.Controls;
+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;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutWindow : FancyWindow
+{
+    public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutPressed;
+    public event Action<ProtoId<LoadoutGroupPrototype>, ProtoId<LoadoutPrototype>>? OnLoadoutUnpressed;
+
+    private List<LoadoutGroupContainer> _groups = new();
+
+    public LoadoutWindow(RoleLoadout loadout, RoleLoadoutPrototype proto, ICommonSession session, IDependencyCollection collection)
+    {
+        RobustXamlLoader.Load(this);
+        var protoManager = collection.Resolve<IPrototypeManager>();
+
+        foreach (var group in proto.Groups)
+        {
+            if (!protoManager.TryIndex(group, out var groupProto))
+                continue;
+
+            var container = new LoadoutGroupContainer(loadout, protoManager.Index(group), session, collection);
+            LoadoutGroupsContainer.AddTab(container, Loc.GetString(groupProto.Name));
+            _groups.Add(container);
+
+            container.OnLoadoutPressed += args =>
+            {
+                OnLoadoutPressed?.Invoke(group, args);
+            };
+
+            container.OnLoadoutUnpressed += args =>
+            {
+                OnLoadoutUnpressed?.Invoke(group, args);
+            };
+        }
+    }
+
+    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);
+        }
+    }
+}
diff --git a/Content.Client/Preferences/UI/RequirementsSelector.cs b/Content.Client/Preferences/UI/RequirementsSelector.cs
new file mode 100644 (file)
index 0000000..97c75f3
--- /dev/null
@@ -0,0 +1,222 @@
+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.UpdateCharacterUI();
+                        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.UpdateCharacterUI();
+                        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 980559cc8174cc24ee9fcac4b21c6d2396124278..7bc62dfe2bc27eed952aecebf7798152064c0e3a 100644 (file)
@@ -19,7 +19,7 @@ public sealed partial class MindTests
         await using var pair = await PoolManager.GetServerClient(settings);
 
         // Client is connected with a valid entity & mind
-        Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+        Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
         Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
 
         // Delete **everything**
@@ -28,6 +28,12 @@ public sealed partial class MindTests
         await pair.RunTicksSync(5);
 
         Assert.That(pair.Server.EntMan.EntityCount, Is.EqualTo(0));
+
+        foreach (var ent in pair.Client.EntMan.GetEntities())
+        {
+            Console.WriteLine(pair.Client.EntMan.ToPrettyString(ent));
+        }
+
         Assert.That(pair.Client.EntMan.EntityCount, Is.EqualTo(0));
 
         // Create a new map.
@@ -36,7 +42,7 @@ public sealed partial class MindTests
         await pair.RunTicksSync(5);
 
         // Client is not attached to anything
-        Assert.That(pair.Client.Player?.ControlledEntity, Is.Null);
+        Assert.That(pair.Client.AttachedEntity, Is.Null);
         Assert.That(pair.PlayerData?.Mind, Is.Null);
 
         // Attempt to ghost
@@ -45,9 +51,9 @@ public sealed partial class MindTests
         await pair.RunTicksSync(10);
 
         // Client should be attached to a ghost placed on the new map.
-        Assert.That(pair.Client.EntMan.EntityExists(pair.Client.Player?.ControlledEntity));
+        Assert.That(pair.Client.EntMan.EntityExists(pair.Client.AttachedEntity));
         Assert.That(pair.Server.EntMan.EntityExists(pair.PlayerData?.Mind));
-        var xform = pair.Client.Transform(pair.Client.Player!.ControlledEntity!.Value);
+        var xform = pair.Client.Transform(pair.Client.AttachedEntity!.Value);
         Assert.That(xform.MapID, Is.EqualTo(new MapId(mapId)));
 
         await pair.CleanReturnAsync();
diff --git a/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs b/Content.IntegrationTests/Tests/Preferences/LoadoutTests.cs
new file mode 100644 (file)
index 0000000..72e35da
--- /dev/null
@@ -0,0 +1,44 @@
+using Content.Server.Station.Systems;
+using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Roles.Jobs;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Preferences;
+
+[TestFixture]
+[Ignore("HumanoidAppearance crashes upon loading default profiles.")]
+public sealed class LoadoutTests
+{
+    /// <summary>
+    /// Checks that an empty loadout still spawns with default gear and not naked.
+    /// </summary>
+    [Test]
+    public async Task TestEmptyLoadout()
+    {
+        var pair = await PoolManager.GetServerClient(new PoolSettings()
+        {
+            Dirty = true,
+        });
+        var server = pair.Server;
+
+        var entManager = server.ResolveDependency<IEntityManager>();
+
+        // Check that an empty role loadout spawns gear
+        var stationSystem = entManager.System<StationSpawningSystem>();
+        var testMap = await pair.CreateTestMap();
+
+        // That's right I can't even spawn a dummy profile without station spawning / humanoidappearance code crashing.
+        var profile = new HumanoidCharacterProfile();
+
+        profile.SetLoadout(new RoleLoadout("TestRoleLoadout"));
+
+        stationSystem.SpawnPlayerMob(testMap.GridCoords, job: new JobComponent()
+        {
+            // Sue me, there's so much involved in setting up jobs
+            Prototype = "CargoTechnician"
+        }, profile, station: null);
+
+        await pair.CleanReturnAsync();
+    }
+}
index c57504764dc2f0f7850d479bf88acb19a6f978d5..0fb9c6a361ba2adea367eae8a75d70124d27472e 100644 (file)
@@ -4,6 +4,8 @@ using Content.Server.Database;
 using Content.Shared.GameTicking;
 using Content.Shared.Humanoid;
 using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
 using Microsoft.Data.Sqlite;
 using Microsoft.EntityFrameworkCore;
 using Robust.Shared.Configuration;
@@ -53,8 +55,6 @@ namespace Content.IntegrationTests.Tests.Preferences
                     Color.Beige,
                     new ()
                 ),
-                ClothingPreference.Jumpskirt,
-                BackpackPreference.Backpack,
                 SpawnPriorityPreference.None,
                 new Dictionary<string, JobPriority>
                 {
@@ -62,7 +62,8 @@ namespace Content.IntegrationTests.Tests.Preferences
                 },
                 PreferenceUnavailableMode.StayInLobby,
                 new List<string> (),
-                new List<string>()
+                new List<string>(),
+                new Dictionary<string, RoleLoadout>()
             );
         }
 
diff --git a/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.Designer.cs
new file mode 100644 (file)
index 0000000..6a45e7a
--- /dev/null
@@ -0,0 +1,1838 @@
+// <auto-generated />
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    [DbContext(typeof(PostgresServerDbContext))]
+    [Migration("20240301130641_ClothingRemoval")]
+    partial class ClothingRemoval
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.0")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("text")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("boolean")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<short>("Impact")
+                        .HasColumnType("smallint")
+                        .HasColumnName("impact");
+
+                    b.Property<JsonDocument>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Message")
+                        .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+                    NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("integer")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_messages_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("boolean")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_notes_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("boolean")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_watchlists_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("antag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("assigned_user_id_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("smallint")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("job_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("integer")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("play_time_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("interval")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<IPAddress>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", null, t =>
+                        {
+                            t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("integer")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Age")
+                        .HasColumnType("integer")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_name");
+
+                    b.Property<JsonDocument>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("integer")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("integer")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("integer")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<string>("LoadoutName")
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("StartDate")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("timestamp with time zone")
+                        .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("integer")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_hit_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_role_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("role_unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("trait_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("bytea")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("integer")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("integer")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.cs b/Content.Server.Database/Migrations/Postgres/20240301130641_ClothingRemoval.cs
new file mode 100644 (file)
index 0000000..1ac30e4
--- /dev/null
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    /// <inheritdoc />
+    public partial class ClothingRemoval : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "backpack",
+                table: "profile");
+
+            migrationBuilder.DropColumn(
+                name: "clothing",
+                table: "profile");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "backpack",
+                table: "profile",
+                type: "text",
+                nullable: false,
+                defaultValue: "");
+
+            migrationBuilder.AddColumn<string>(
+                name: "clothing",
+                table: "profile",
+                type: "text",
+                nullable: false,
+                defaultValue: "");
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.Designer.cs
new file mode 100644 (file)
index 0000000..905d6b0
--- /dev/null
@@ -0,0 +1,1884 @@
+// <auto-generated />
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    [DbContext(typeof(PostgresServerDbContext))]
+    [Migration("20240403072242_Loadouts")]
+    partial class Loadouts
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder
+                .HasAnnotation("ProductVersion", "8.0.0")
+                .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("text")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("boolean")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<short>("Impact")
+                        .HasColumnType("smallint")
+                        .HasColumnName("impact");
+
+                    b.Property<JsonDocument>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("integer")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Message")
+                        .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+                    NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("integer")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_messages_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("boolean")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("boolean")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_notes_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("boolean")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("admin_watchlists_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("boolean")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("character varying(4096)")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("antag_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("assigned_user_id_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<IPAddress>("Address")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("smallint")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("job_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("integer")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("play_time_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("interval")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("player_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<IPAddress>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("inet")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", null, t =>
+                        {
+                            t.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("integer")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("Age")
+                        .HasColumnType("integer")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("hair_name");
+
+                    b.Property<JsonDocument>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("integer")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("integer")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("integer")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("integer")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("StartDate")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("timestamp with time zone")
+                        .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("boolean")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("integer")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("integer")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_ban_hit_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("integer")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("server_role_ban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<NpgsqlInet?>("Address")
+                        .HasColumnType("inet")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("bytea")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("boolean")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("uuid")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("interval")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("integer")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("integer")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("role_unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("unban_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("integer")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("uuid")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("trait_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("bytea")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("timestamp with time zone")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("uuid")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("integer")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("integer")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.cs b/Content.Server.Database/Migrations/Postgres/20240403072242_Loadouts.cs
new file mode 100644 (file)
index 0000000..bc512b6
--- /dev/null
@@ -0,0 +1,103 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+    /// <inheritdoc />
+    public partial class Loadouts : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "profile_role_loadout",
+                columns: table => new
+                {
+                    profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    profile_id = table.Column<int>(type: "integer", nullable: false),
+                    role_name = table.Column<string>(type: "text", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
+                    table.ForeignKey(
+                        name: "FK_profile_role_loadout_profile_profile_id",
+                        column: x => x.profile_id,
+                        principalTable: "profile",
+                        principalColumn: "profile_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "profile_loadout_group",
+                columns: table => new
+                {
+                    profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    profile_role_loadout_id = table.Column<int>(type: "integer", nullable: false),
+                    group_name = table.Column<string>(type: "text", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
+                    table.ForeignKey(
+                        name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loa~",
+                        column: x => x.profile_role_loadout_id,
+                        principalTable: "profile_role_loadout",
+                        principalColumn: "profile_role_loadout_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "profile_loadout",
+                columns: table => new
+                {
+                    profile_loadout_id = table.Column<int>(type: "integer", nullable: false)
+                        .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+                    profile_loadout_group_id = table.Column<int>(type: "integer", nullable: false),
+                    loadout_name = table.Column<string>(type: "text", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
+                    table.ForeignKey(
+                        name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group~",
+                        column: x => x.profile_loadout_group_id,
+                        principalTable: "profile_loadout_group",
+                        principalColumn: "profile_loadout_group_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_loadout_profile_loadout_group_id",
+                table: "profile_loadout",
+                column: "profile_loadout_group_id");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_loadout_group_profile_role_loadout_id",
+                table: "profile_loadout_group",
+                column: "profile_role_loadout_id");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_role_loadout_profile_id",
+                table: "profile_role_loadout",
+                column: "profile_id");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "profile_loadout");
+
+            migrationBuilder.DropTable(
+                name: "profile_loadout_group");
+
+            migrationBuilder.DropTable(
+                name: "profile_role_loadout");
+        }
+    }
+}
index 8376407dcf89a7616102834f23352e47984ead51..bbbd64d874a1d300d21ecf6e205824b12b92c0c2 100644 (file)
@@ -735,21 +735,11 @@ namespace Content.Server.Database.Migrations.Postgres
                         .HasColumnType("integer")
                         .HasColumnName("age");
 
-                    b.Property<string>("Backpack")
-                        .IsRequired()
-                        .HasColumnType("text")
-                        .HasColumnName("backpack");
-
                     b.Property<string>("CharacterName")
                         .IsRequired()
                         .HasColumnType("text")
                         .HasColumnName("char_name");
 
-                    b.Property<string>("Clothing")
-                        .IsRequired()
-                        .HasColumnType("text")
-                        .HasColumnName("clothing");
-
                     b.Property<string>("EyeColor")
                         .IsRequired()
                         .HasColumnType("text")
@@ -832,6 +822,84 @@ namespace Content.Server.Database.Migrations.Postgres
                     b.ToTable("profile", (string)null);
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("integer")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("text")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.Property<int>("Id")
@@ -1519,6 +1587,42 @@ namespace Content.Server.Database.Migrations.Postgres
                     b.Navigation("Preference");
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.HasOne("Content.Server.Database.Server", "Server")
@@ -1731,9 +1835,21 @@ namespace Content.Server.Database.Migrations.Postgres
 
                     b.Navigation("Jobs");
 
+                    b.Navigation("Loadouts");
+
                     b.Navigation("Traits");
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.Navigation("AdminLogs");
diff --git a/Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.Designer.cs
new file mode 100644 (file)
index 0000000..1d933d0
--- /dev/null
@@ -0,0 +1,1765 @@
+// <auto-generated />
+using System;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    [DbContext(typeof(SqliteServerDbContext))]
+    [Migration("20240301130602_ClothingRemoval")]
+    partial class ClothingRemoval
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_flag_id");
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<sbyte>("Impact")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("impact");
+
+                    b.Property<string>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_messages_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_notes_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_watchlists_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("antag_id");
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("assigned_user_id_id");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_log_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("job_id");
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("play_time_id");
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("player_id");
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<string>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<int>("Age")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_name");
+
+                    b.Property<byte[]>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<string>("LoadoutName")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("StartDate")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_hit_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_role_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("role_unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("trait_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("BLOB")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.cs b/Content.Server.Database/Migrations/Sqlite/20240301130602_ClothingRemoval.cs
new file mode 100644 (file)
index 0000000..dd69ef2
--- /dev/null
@@ -0,0 +1,40 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    /// <inheritdoc />
+    public partial class ClothingRemoval : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropColumn(
+                name: "backpack",
+                table: "profile");
+
+            migrationBuilder.DropColumn(
+                name: "clothing",
+                table: "profile");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.AddColumn<string>(
+                name: "backpack",
+                table: "profile",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: "");
+
+            migrationBuilder.AddColumn<string>(
+                name: "clothing",
+                table: "profile",
+                type: "TEXT",
+                nullable: false,
+                defaultValue: "");
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.Designer.cs
new file mode 100644 (file)
index 0000000..06698bd
--- /dev/null
@@ -0,0 +1,1809 @@
+// <auto-generated />
+using System;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    [DbContext(typeof(SqliteServerDbContext))]
+    [Migration("20240403072258_Loadouts")]
+    partial class Loadouts
+    {
+        /// <inheritdoc />
+        protected override void BuildTargetModel(ModelBuilder modelBuilder)
+        {
+#pragma warning disable 612, 618
+            modelBuilder.HasAnnotation("ProductVersion", "8.0.0");
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int?>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Title")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("title");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_admin");
+
+                    b.HasIndex("AdminRankId")
+                        .HasDatabaseName("IX_admin_admin_rank_id");
+
+                    b.ToTable("admin", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_flag_id");
+
+                    b.Property<Guid>("AdminId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.Property<bool>("Negative")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("negative");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_flag");
+
+                    b.HasIndex("AdminId")
+                        .HasDatabaseName("IX_admin_flag_admin_id");
+
+                    b.HasIndex("Flag", "AdminId")
+                        .IsUnique();
+
+                    b.ToTable("admin_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Id")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_log_id");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<sbyte>("Impact")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("impact");
+
+                    b.Property<string>("Json")
+                        .IsRequired()
+                        .HasColumnType("jsonb")
+                        .HasColumnName("json");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<int>("Type")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("type");
+
+                    b.HasKey("RoundId", "Id")
+                        .HasName("PK_admin_log");
+
+                    b.HasIndex("Date");
+
+                    b.HasIndex("Type")
+                        .HasDatabaseName("IX_admin_log_type");
+
+                    b.ToTable("admin_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.Property<int>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("LogId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("log_id");
+
+                    b.Property<Guid>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.HasKey("RoundId", "LogId", "PlayerUserId")
+                        .HasName("PK_admin_log_player");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+                    b.ToTable("admin_log_player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_messages_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<bool>("Dismissed")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("dismissed");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Seen")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("seen");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_messages");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_messages_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_messages_round_id");
+
+                    b.ToTable("admin_messages", null, t =>
+                        {
+                            t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_notes_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<bool>("Secret")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("secret");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_notes");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_notes_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_notes_round_id");
+
+                    b.ToTable("admin_notes", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank");
+
+                    b.ToTable("admin_rank", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_flag_id");
+
+                    b.Property<int>("AdminRankId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_rank_id");
+
+                    b.Property<string>("Flag")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flag");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_rank_flag");
+
+                    b.HasIndex("AdminRankId");
+
+                    b.HasIndex("Flag", "AdminRankId")
+                        .IsUnique();
+
+                    b.ToTable("admin_rank_flag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("admin_watchlists_id");
+
+                    b.Property<DateTime>("CreatedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_at");
+
+                    b.Property<Guid?>("CreatedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("created_by_id");
+
+                    b.Property<bool>("Deleted")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("deleted");
+
+                    b.Property<DateTime?>("DeletedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_at");
+
+                    b.Property<Guid?>("DeletedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("deleted_by_id");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<string>("Message")
+                        .IsRequired()
+                        .HasMaxLength(4096)
+                        .HasColumnType("TEXT")
+                        .HasColumnName("message");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_admin_watchlists");
+
+                    b.HasIndex("CreatedById");
+
+                    b.HasIndex("DeletedById");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_admin_watchlists_round_id");
+
+                    b.ToTable("admin_watchlists", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("antag_id");
+
+                    b.Property<string>("AntagName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("antag_name");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_antag");
+
+                    b.HasIndex("ProfileId", "AntagName")
+                        .IsUnique();
+
+                    b.ToTable("antag", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("assigned_user_id_id");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_assigned_user_id");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.HasIndex("UserName")
+                        .IsUnique();
+
+                    b.ToTable("assigned_user_id", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_log_id");
+
+                    b.Property<string>("Address")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<byte?>("Denied")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("denied");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<int>("ServerId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasDefaultValue(0)
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("Time")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<string>("UserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_connection_log");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_connection_log_server_id");
+
+                    b.HasIndex("UserId");
+
+                    b.ToTable("connection_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("job_id");
+
+                    b.Property<string>("JobName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("job_name");
+
+                    b.Property<int>("Priority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("priority");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_job");
+
+                    b.HasIndex("ProfileId");
+
+                    b.HasIndex("ProfileId", "JobName")
+                        .IsUnique();
+
+                    b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+                        .IsUnique()
+                        .HasFilter("priority = 3");
+
+                    b.ToTable("job", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("play_time_id");
+
+                    b.Property<Guid>("PlayerId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_id");
+
+                    b.Property<TimeSpan>("TimeSpent")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("time_spent");
+
+                    b.Property<string>("Tracker")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("tracker");
+
+                    b.HasKey("Id")
+                        .HasName("PK_play_time");
+
+                    b.HasIndex("PlayerId", "Tracker")
+                        .IsUnique();
+
+                    b.ToTable("play_time", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("player_id");
+
+                    b.Property<DateTime>("FirstSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("first_seen_time");
+
+                    b.Property<DateTime?>("LastReadRules")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_read_rules");
+
+                    b.Property<string>("LastSeenAddress")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_address");
+
+                    b.Property<byte[]>("LastSeenHWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("last_seen_hwid");
+
+                    b.Property<DateTime>("LastSeenTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_time");
+
+                    b.Property<string>("LastSeenUserName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_seen_user_name");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_player");
+
+                    b.HasAlternateKey("UserId")
+                        .HasName("ak_player_user_id");
+
+                    b.HasIndex("LastSeenUserName");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("player", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<string>("AdminOOCColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("admin_ooc_color");
+
+                    b.Property<int>("SelectedCharacterSlot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("selected_character_slot");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_preference");
+
+                    b.HasIndex("UserId")
+                        .IsUnique();
+
+                    b.ToTable("preference", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<int>("Age")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("age");
+
+                    b.Property<string>("CharacterName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("char_name");
+
+                    b.Property<string>("EyeColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("eye_color");
+
+                    b.Property<string>("FacialHairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_color");
+
+                    b.Property<string>("FacialHairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("facial_hair_name");
+
+                    b.Property<string>("FlavorText")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("flavor_text");
+
+                    b.Property<string>("Gender")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("gender");
+
+                    b.Property<string>("HairColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_color");
+
+                    b.Property<string>("HairName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("hair_name");
+
+                    b.Property<byte[]>("Markings")
+                        .HasColumnType("jsonb")
+                        .HasColumnName("markings");
+
+                    b.Property<int>("PreferenceId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("preference_id");
+
+                    b.Property<int>("PreferenceUnavailable")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("pref_unavailable");
+
+                    b.Property<string>("Sex")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("sex");
+
+                    b.Property<string>("SkinColor")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("skin_color");
+
+                    b.Property<int>("Slot")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("slot");
+
+                    b.Property<int>("SpawnPriority")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("spawn_priority");
+
+                    b.Property<string>("Species")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("species");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile");
+
+                    b.HasIndex("PreferenceId")
+                        .HasDatabaseName("IX_profile_preference_id");
+
+                    b.HasIndex("Slot", "PreferenceId")
+                        .IsUnique();
+
+                    b.ToTable("profile", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_id");
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("ServerId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<DateTime>("StartDate")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasDefaultValue(new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
+                        .HasColumnName("start_date");
+
+                    b.HasKey("Id")
+                        .HasName("PK_round");
+
+                    b.HasIndex("ServerId")
+                        .HasDatabaseName("IX_round_server_id");
+
+                    b.HasIndex("StartDate");
+
+                    b.ToTable("round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_id");
+
+                    b.Property<string>("Name")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server");
+
+                    b.ToTable("server", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<bool>("AutoDelete")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("auto_delete");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<int>("ExemptFlags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("exempt_flags");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_ban_round_id");
+
+                    b.ToTable("server_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.Property<int>("Flags")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("flags");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_server_ban_exemption");
+
+                    b.ToTable("server_ban_exemption", null, t =>
+                        {
+                            t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_ban_hit_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<int>("ConnectionId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("connection_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_ban_hit");
+
+                    b.HasIndex("BanId")
+                        .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+                    b.HasIndex("ConnectionId")
+                        .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+                    b.ToTable("server_ban_hit", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("server_role_ban_id");
+
+                    b.Property<string>("Address")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("address");
+
+                    b.Property<DateTime>("BanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("ban_time");
+
+                    b.Property<Guid?>("BanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("banning_admin");
+
+                    b.Property<DateTime?>("ExpirationTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("expiration_time");
+
+                    b.Property<byte[]>("HWId")
+                        .HasColumnType("BLOB")
+                        .HasColumnName("hwid");
+
+                    b.Property<bool>("Hidden")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("hidden");
+
+                    b.Property<DateTime?>("LastEditedAt")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_at");
+
+                    b.Property<Guid?>("LastEditedById")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("last_edited_by_id");
+
+                    b.Property<Guid?>("PlayerUserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("player_user_id");
+
+                    b.Property<TimeSpan>("PlaytimeAtNote")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("playtime_at_note");
+
+                    b.Property<string>("Reason")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("reason");
+
+                    b.Property<string>("RoleId")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_id");
+
+                    b.Property<int?>("RoundId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("round_id");
+
+                    b.Property<int>("Severity")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("severity");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_ban");
+
+                    b.HasIndex("Address");
+
+                    b.HasIndex("BanningAdmin");
+
+                    b.HasIndex("LastEditedById");
+
+                    b.HasIndex("PlayerUserId")
+                        .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+                    b.HasIndex("RoundId")
+                        .HasDatabaseName("IX_server_role_ban_round_id");
+
+                    b.ToTable("server_role_ban", null, t =>
+                        {
+                            t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+                        });
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("role_unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_role_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_role_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("unban_id");
+
+                    b.Property<int>("BanId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("ban_id");
+
+                    b.Property<DateTime>("UnbanTime")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unban_time");
+
+                    b.Property<Guid?>("UnbanningAdmin")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("unbanning_admin");
+
+                    b.HasKey("Id")
+                        .HasName("PK_server_unban");
+
+                    b.HasIndex("BanId")
+                        .IsUnique();
+
+                    b.ToTable("server_unban", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("trait_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("TraitName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("trait_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_trait");
+
+                    b.HasIndex("ProfileId", "TraitName")
+                        .IsUnique();
+
+                    b.ToTable("trait", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("uploaded_resource_log_id");
+
+                    b.Property<byte[]>("Data")
+                        .IsRequired()
+                        .HasColumnType("BLOB")
+                        .HasColumnName("data");
+
+                    b.Property<DateTime>("Date")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("date");
+
+                    b.Property<string>("Path")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("path");
+
+                    b.Property<Guid>("UserId")
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_uploaded_resource_log");
+
+                    b.ToTable("uploaded_resource_log", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+                {
+                    b.Property<Guid>("UserId")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("user_id");
+
+                    b.HasKey("UserId")
+                        .HasName("PK_whitelist");
+
+                    b.ToTable("whitelist", (string)null);
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.Property<int>("PlayersId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("players_id");
+
+                    b.Property<int>("RoundsId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("rounds_id");
+
+                    b.HasKey("PlayersId", "RoundsId")
+                        .HasName("PK_player_round");
+
+                    b.HasIndex("RoundsId")
+                        .HasDatabaseName("IX_player_round_rounds_id");
+
+                    b.ToTable("player_round", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+                        .WithMany("Admins")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+                    b.Navigation("AdminRank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Admin", "Admin")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+                    b.Navigation("Admin");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("RoundId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_round_round_id");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminLogs")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.AdminLog", "Log")
+                        .WithMany("Players")
+                        .HasForeignKey("RoundId", "LogId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+                    b.Navigation("Log");
+
+                    b.Navigation("Player");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminMessagesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminMessagesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminMessagesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminMessagesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_messages_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminNotesCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminNotesDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminNotesLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminNotesReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_notes_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+                {
+                    b.HasOne("Content.Server.Database.AdminRank", "Rank")
+                        .WithMany("Flags")
+                        .HasForeignKey("AdminRankId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+                    b.Navigation("Rank");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminWatchlistsCreated")
+                        .HasForeignKey("CreatedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "DeletedBy")
+                        .WithMany("AdminWatchlistsDeleted")
+                        .HasForeignKey("DeletedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminWatchlistsLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Player", "Player")
+                        .WithMany("AdminWatchlistsReceived")
+                        .HasForeignKey("PlayerUserId")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("DeletedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Player");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Antag", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Antags")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_antag_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("ConnectionLogs")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .IsRequired()
+                        .HasConstraintName("FK_connection_log_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Job", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Jobs")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_job_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.HasOne("Content.Server.Database.Preference", "Preference")
+                        .WithMany("Profiles")
+                        .HasForeignKey("PreferenceId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_preference_preference_id");
+
+                    b.Navigation("Preference");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.HasOne("Content.Server.Database.Server", "Server")
+                        .WithMany("Rounds")
+                        .HasForeignKey("ServerId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_round_server_server_id");
+
+                    b.Navigation("Server");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithMany("BanHits")
+                        .HasForeignKey("BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+                    b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+                        .WithMany("BanHits")
+                        .HasForeignKey("ConnectionId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+                    b.Navigation("Ban");
+
+                    b.Navigation("Connection");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", "CreatedBy")
+                        .WithMany("AdminServerRoleBansCreated")
+                        .HasForeignKey("BanningAdmin")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+                    b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+                        .WithMany("AdminServerRoleBansLastEdited")
+                        .HasForeignKey("LastEditedById")
+                        .HasPrincipalKey("UserId")
+                        .OnDelete(DeleteBehavior.SetNull)
+                        .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+                    b.HasOne("Content.Server.Database.Round", "Round")
+                        .WithMany()
+                        .HasForeignKey("RoundId")
+                        .HasConstraintName("FK_server_role_ban_round_round_id");
+
+                    b.Navigation("CreatedBy");
+
+                    b.Navigation("LastEditedBy");
+
+                    b.Navigation("Round");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+                {
+                    b.HasOne("Content.Server.Database.ServerBan", "Ban")
+                        .WithOne("Unban")
+                        .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+                    b.Navigation("Ban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Trait", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Traits")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_trait_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
+            modelBuilder.Entity("PlayerRound", b =>
+                {
+                    b.HasOne("Content.Server.Database.Player", null)
+                        .WithMany()
+                        .HasForeignKey("PlayersId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_player_players_id");
+
+                    b.HasOne("Content.Server.Database.Round", null)
+                        .WithMany()
+                        .HasForeignKey("RoundsId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_player_round_round_rounds_id");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Admin", b =>
+                {
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+                {
+                    b.Navigation("Players");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+                {
+                    b.Navigation("Admins");
+
+                    b.Navigation("Flags");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+                {
+                    b.Navigation("BanHits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Player", b =>
+                {
+                    b.Navigation("AdminLogs");
+
+                    b.Navigation("AdminMessagesCreated");
+
+                    b.Navigation("AdminMessagesDeleted");
+
+                    b.Navigation("AdminMessagesLastEdited");
+
+                    b.Navigation("AdminMessagesReceived");
+
+                    b.Navigation("AdminNotesCreated");
+
+                    b.Navigation("AdminNotesDeleted");
+
+                    b.Navigation("AdminNotesLastEdited");
+
+                    b.Navigation("AdminNotesReceived");
+
+                    b.Navigation("AdminServerBansCreated");
+
+                    b.Navigation("AdminServerBansLastEdited");
+
+                    b.Navigation("AdminServerRoleBansCreated");
+
+                    b.Navigation("AdminServerRoleBansLastEdited");
+
+                    b.Navigation("AdminWatchlistsCreated");
+
+                    b.Navigation("AdminWatchlistsDeleted");
+
+                    b.Navigation("AdminWatchlistsLastEdited");
+
+                    b.Navigation("AdminWatchlistsReceived");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Preference", b =>
+                {
+                    b.Navigation("Profiles");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Profile", b =>
+                {
+                    b.Navigation("Antags");
+
+                    b.Navigation("Jobs");
+
+                    b.Navigation("Loadouts");
+
+                    b.Navigation("Traits");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Round", b =>
+                {
+                    b.Navigation("AdminLogs");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.Server", b =>
+                {
+                    b.Navigation("ConnectionLogs");
+
+                    b.Navigation("Rounds");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+                {
+                    b.Navigation("BanHits");
+
+                    b.Navigation("Unban");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+                {
+                    b.Navigation("Unban");
+                });
+#pragma warning restore 612, 618
+        }
+    }
+}
diff --git a/Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.cs b/Content.Server.Database/Migrations/Sqlite/20240403072258_Loadouts.cs
new file mode 100644 (file)
index 0000000..c262ed1
--- /dev/null
@@ -0,0 +1,102 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+    /// <inheritdoc />
+    public partial class Loadouts : Migration
+    {
+        /// <inheritdoc />
+        protected override void Up(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.CreateTable(
+                name: "profile_role_loadout",
+                columns: table => new
+                {
+                    profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    profile_id = table.Column<int>(type: "INTEGER", nullable: false),
+                    role_name = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_role_loadout", x => x.profile_role_loadout_id);
+                    table.ForeignKey(
+                        name: "FK_profile_role_loadout_profile_profile_id",
+                        column: x => x.profile_id,
+                        principalTable: "profile",
+                        principalColumn: "profile_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "profile_loadout_group",
+                columns: table => new
+                {
+                    profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    profile_role_loadout_id = table.Column<int>(type: "INTEGER", nullable: false),
+                    group_name = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_loadout_group", x => x.profile_loadout_group_id);
+                    table.ForeignKey(
+                        name: "FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id",
+                        column: x => x.profile_role_loadout_id,
+                        principalTable: "profile_role_loadout",
+                        principalColumn: "profile_role_loadout_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateTable(
+                name: "profile_loadout",
+                columns: table => new
+                {
+                    profile_loadout_id = table.Column<int>(type: "INTEGER", nullable: false)
+                        .Annotation("Sqlite:Autoincrement", true),
+                    profile_loadout_group_id = table.Column<int>(type: "INTEGER", nullable: false),
+                    loadout_name = table.Column<string>(type: "TEXT", nullable: false)
+                },
+                constraints: table =>
+                {
+                    table.PrimaryKey("PK_profile_loadout", x => x.profile_loadout_id);
+                    table.ForeignKey(
+                        name: "FK_profile_loadout_profile_loadout_group_profile_loadout_group_id",
+                        column: x => x.profile_loadout_group_id,
+                        principalTable: "profile_loadout_group",
+                        principalColumn: "profile_loadout_group_id",
+                        onDelete: ReferentialAction.Cascade);
+                });
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_loadout_profile_loadout_group_id",
+                table: "profile_loadout",
+                column: "profile_loadout_group_id");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_loadout_group_profile_role_loadout_id",
+                table: "profile_loadout_group",
+                column: "profile_role_loadout_id");
+
+            migrationBuilder.CreateIndex(
+                name: "IX_profile_role_loadout_profile_id",
+                table: "profile_role_loadout",
+                column: "profile_id");
+        }
+
+        /// <inheritdoc />
+        protected override void Down(MigrationBuilder migrationBuilder)
+        {
+            migrationBuilder.DropTable(
+                name: "profile_loadout");
+
+            migrationBuilder.DropTable(
+                name: "profile_loadout_group");
+
+            migrationBuilder.DropTable(
+                name: "profile_role_loadout");
+        }
+    }
+}
index 80c374c785d984c400accf49128112da8bb13837..d343f49f416d9b500ef2fa4468698a5201d1cd4e 100644 (file)
@@ -688,21 +688,11 @@ namespace Content.Server.Database.Migrations.Sqlite
                         .HasColumnType("INTEGER")
                         .HasColumnName("age");
 
-                    b.Property<string>("Backpack")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasColumnName("backpack");
-
                     b.Property<string>("CharacterName")
                         .IsRequired()
                         .HasColumnType("TEXT")
                         .HasColumnName("char_name");
 
-                    b.Property<string>("Clothing")
-                        .IsRequired()
-                        .HasColumnType("TEXT")
-                        .HasColumnName("clothing");
-
                     b.Property<string>("EyeColor")
                         .IsRequired()
                         .HasColumnType("TEXT")
@@ -785,6 +775,78 @@ namespace Content.Server.Database.Migrations.Sqlite
                     b.ToTable("profile", (string)null);
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_id");
+
+                    b.Property<string>("LoadoutName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("loadout_name");
+
+                    b.Property<int>("ProfileLoadoutGroupId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout");
+
+                    b.HasIndex("ProfileLoadoutGroupId");
+
+                    b.ToTable("profile_loadout", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_loadout_group_id");
+
+                    b.Property<string>("GroupName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("group_name");
+
+                    b.Property<int>("ProfileRoleLoadoutId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_loadout_group");
+
+                    b.HasIndex("ProfileRoleLoadoutId");
+
+                    b.ToTable("profile_loadout_group", (string)null);
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Property<int>("Id")
+                        .ValueGeneratedOnAdd()
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_role_loadout_id");
+
+                    b.Property<int>("ProfileId")
+                        .HasColumnType("INTEGER")
+                        .HasColumnName("profile_id");
+
+                    b.Property<string>("RoleName")
+                        .IsRequired()
+                        .HasColumnType("TEXT")
+                        .HasColumnName("role_name");
+
+                    b.HasKey("Id")
+                        .HasName("PK_profile_role_loadout");
+
+                    b.HasIndex("ProfileId");
+
+                    b.ToTable("profile_role_loadout", (string)null);
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.Property<int>("Id")
@@ -1450,6 +1512,42 @@ namespace Content.Server.Database.Migrations.Sqlite
                     b.Navigation("Preference");
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileLoadoutGroupId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group_id");
+
+                    b.Navigation("ProfileLoadoutGroup");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+                        .WithMany("Groups")
+                        .HasForeignKey("ProfileRoleLoadoutId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loadout_id");
+
+                    b.Navigation("ProfileRoleLoadout");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.HasOne("Content.Server.Database.Profile", "Profile")
+                        .WithMany("Loadouts")
+                        .HasForeignKey("ProfileId")
+                        .OnDelete(DeleteBehavior.Cascade)
+                        .IsRequired()
+                        .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+                    b.Navigation("Profile");
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.HasOne("Content.Server.Database.Server", "Server")
@@ -1662,9 +1760,21 @@ namespace Content.Server.Database.Migrations.Sqlite
 
                     b.Navigation("Jobs");
 
+                    b.Navigation("Loadouts");
+
                     b.Navigation("Traits");
                 });
 
+            modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+                {
+                    b.Navigation("Loadouts");
+                });
+
+            modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+                {
+                    b.Navigation("Groups");
+                });
+
             modelBuilder.Entity("Content.Server.Database.Round", b =>
                 {
                     b.Navigation("AdminLogs");
index 2e13c2a2f63c55e01e69f6d70e370b874edb70db..4a85f60ec6a0ad5179966538483d8c03f00aaeb9 100644 (file)
@@ -56,8 +56,26 @@ namespace Content.Server.Database
                 .IsUnique();
 
             modelBuilder.Entity<Trait>()
-                        .HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
-                        .IsUnique();
+                .HasIndex(p => new {HumanoidProfileId = p.ProfileId, p.TraitName})
+                .IsUnique();
+
+            modelBuilder.Entity<ProfileRoleLoadout>()
+                .HasOne(e => e.Profile)
+                .WithMany(e => e.Loadouts)
+                .HasForeignKey(e => e.ProfileId)
+                .IsRequired();
+
+            modelBuilder.Entity<ProfileLoadoutGroup>()
+                .HasOne(e => e.ProfileRoleLoadout)
+                .WithMany(e => e.Groups)
+                .HasForeignKey(e => e.ProfileRoleLoadoutId)
+                .IsRequired();
+
+            modelBuilder.Entity<ProfileLoadout>()
+                .HasOne(e => e.ProfileLoadoutGroup)
+                .WithMany(e => e.Loadouts)
+                .HasForeignKey(e => e.ProfileLoadoutGroupId)
+                .IsRequired();
 
             modelBuilder.Entity<Job>()
                 .HasIndex(j => j.ProfileId);
@@ -337,13 +355,13 @@ namespace Content.Server.Database
         public string FacialHairColor { get; set; } = null!;
         public string EyeColor { get; set; } = null!;
         public string SkinColor { get; set; } = null!;
-        public string Clothing { get; set; } = null!;
-        public string Backpack { get; set; } = null!;
         public int SpawnPriority { get; set; } = 0;
         public List<Job> Jobs { get; } = new();
         public List<Antag> Antags { get; } = new();
         public List<Trait> Traits { get; } = new();
 
+        public List<ProfileRoleLoadout> Loadouts { get; } = new();
+
         [Column("pref_unavailable")] public DbPreferenceUnavailableMode PreferenceUnavailable { get; set; }
 
         public int PreferenceId { get; set; }
@@ -387,6 +405,79 @@ namespace Content.Server.Database
         public string TraitName { get; set; } = null!;
     }
 
+    #region Loadouts
+
+    /// <summary>
+    /// Corresponds to a single role's loadout inside the DB.
+    /// </summary>
+    public class ProfileRoleLoadout
+    {
+        public int Id { get; set; }
+
+        public int ProfileId { get; set; }
+
+        public Profile Profile { get; set; } = null!;
+
+        /// <summary>
+        /// The corresponding role prototype on the profile.
+        /// </summary>
+        public string RoleName { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Store the saved loadout groups. These may get validated and removed when loaded at runtime.
+        /// </summary>
+        public List<ProfileLoadoutGroup> Groups { get; set; } = new();
+    }
+
+    /// <summary>
+    /// Corresponds to a loadout group prototype with the specified loadouts attached.
+    /// </summary>
+    public class ProfileLoadoutGroup
+    {
+        public int Id { get; set; }
+
+        public int ProfileRoleLoadoutId { get; set; }
+
+        /// <summary>
+        /// The corresponding RoleLoadout that owns this.
+        /// </summary>
+        public ProfileRoleLoadout ProfileRoleLoadout { get; set; } = null!;
+
+        /// <summary>
+        /// The corresponding group prototype.
+        /// </summary>
+        public string GroupName { get; set; } = string.Empty;
+
+        /// <summary>
+        /// Selected loadout prototype. Null if none is set.
+        /// May get validated at runtime and updated to to the default.
+        /// </summary>
+        public List<ProfileLoadout> Loadouts { get; set; } = new();
+    }
+
+    /// <summary>
+    /// Corresponds to a selected loadout.
+    /// </summary>
+    public class ProfileLoadout
+    {
+        public int Id { get; set; }
+
+        public int ProfileLoadoutGroupId { get; set; }
+
+        public ProfileLoadoutGroup ProfileLoadoutGroup { get; set; } = null!;
+
+        /// <summary>
+        /// Corresponding loadout prototype.
+        /// </summary>
+        public string LoadoutName { get; set; } = string.Empty;
+
+        /*
+         * Insert extra data here like custom descriptions or colors or whatever.
+         */
+    }
+
+    #endregion
+
     public enum DbPreferenceUnavailableMode
     {
         // These enum values HAVE to match the ones in PreferenceUnavailableMode in Shared.
index 79e73ce3d972207f7af521074f0fd40c03586af8..15d8d4afb7b7ebb354bcb5498369eba498c52670 100644 (file)
@@ -97,7 +97,7 @@ namespace Content.Server.Administration.Commands
                 foreach (var slot in slots)
                 {
                     invSystem.TryUnequip(target, slot.Name, true, true, false, inventoryComponent);
-                    var gearStr = startingGear.GetGear(slot.Name, profile);
+                    var gearStr = startingGear.GetGear(slot.Name);
                     if (gearStr == string.Empty)
                     {
                         continue;
index 67b29209a34704f80f293c105ec8c05ccf472bc3..9d40d9400f395fcc425491cb36a6a9a0a059243b 100644 (file)
@@ -13,6 +13,8 @@ using Content.Shared.Database;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Markings;
 using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
 using Microsoft.EntityFrameworkCore;
 using Robust.Shared.Enums;
 using Robust.Shared.Network;
@@ -40,6 +42,10 @@ namespace Content.Server.Database
                 .Include(p => p.Profiles).ThenInclude(h => h.Jobs)
                 .Include(p => p.Profiles).ThenInclude(h => h.Antags)
                 .Include(p => p.Profiles).ThenInclude(h => h.Traits)
+                .Include(p => p.Profiles)
+                    .ThenInclude(h => h.Loadouts)
+                    .ThenInclude(l => l.Groups)
+                    .ThenInclude(group => group.Loadouts)
                 .AsSingleQuery()
                 .SingleOrDefaultAsync(p => p.UserId == userId.UserId);
 
@@ -88,6 +94,9 @@ namespace Content.Server.Database
                 .Include(p => p.Jobs)
                 .Include(p => p.Antags)
                 .Include(p => p.Traits)
+                .Include(p => p.Loadouts)
+                    .ThenInclude(l => l.Groups)
+                    .ThenInclude(group => group.Loadouts)
                 .AsSplitQuery()
                 .SingleOrDefault(h => h.Slot == slot);
 
@@ -179,14 +188,6 @@ namespace Content.Server.Database
             if (Enum.TryParse<Sex>(profile.Sex, true, out var sexVal))
                 sex = sexVal;
 
-            var clothing = ClothingPreference.Jumpsuit;
-            if (Enum.TryParse<ClothingPreference>(profile.Clothing, true, out var clothingVal))
-                clothing = clothingVal;
-
-            var backpack = BackpackPreference.Backpack;
-            if (Enum.TryParse<BackpackPreference>(profile.Backpack, true, out var backpackVal))
-                backpack = backpackVal;
-
             var spawnPriority = (SpawnPriorityPreference) profile.SpawnPriority;
 
             var gender = sex == Sex.Male ? Gender.Male : Gender.Female;
@@ -209,6 +210,27 @@ namespace Content.Server.Database
                 }
             }
 
+            var loadouts = new Dictionary<string, RoleLoadout>();
+
+            foreach (var role in profile.Loadouts)
+            {
+                var loadout = new RoleLoadout(role.RoleName);
+
+                foreach (var group in role.Groups)
+                {
+                    var groupLoadouts = loadout.SelectedLoadouts.GetOrNew(group.GroupName);
+                    foreach (var profLoadout in group.Loadouts)
+                    {
+                        groupLoadouts.Add(new Loadout()
+                        {
+                            Prototype = profLoadout.LoadoutName,
+                        });
+                    }
+                }
+
+                loadouts[role.RoleName] = loadout;
+            }
+
             return new HumanoidCharacterProfile(
                 profile.CharacterName,
                 profile.FlavorText,
@@ -226,13 +248,12 @@ namespace Content.Server.Database
                     Color.FromHex(profile.SkinColor),
                     markings
                 ),
-                clothing,
-                backpack,
                 spawnPriority,
                 jobs,
                 (PreferenceUnavailableMode) profile.PreferenceUnavailable,
                 antags.ToList(),
-                traits.ToList()
+                traits.ToList(),
+                loadouts
             );
         }
 
@@ -259,8 +280,6 @@ namespace Content.Server.Database
             profile.FacialHairColor = appearance.FacialHairColor.ToHex();
             profile.EyeColor = appearance.EyeColor.ToHex();
             profile.SkinColor = appearance.SkinColor.ToHex();
-            profile.Clothing = humanoid.Clothing.ToString();
-            profile.Backpack = humanoid.Backpack.ToString();
             profile.SpawnPriority = (int) humanoid.SpawnPriority;
             profile.Markings = markings;
             profile.Slot = slot;
@@ -285,6 +304,36 @@ namespace Content.Server.Database
                         .Select(t => new Trait {TraitName = t})
             );
 
+            profile.Loadouts.Clear();
+
+            foreach (var (role, loadouts) in humanoid.Loadouts)
+            {
+                var dz = new ProfileRoleLoadout()
+                {
+                    RoleName = role,
+                };
+
+                foreach (var (group, groupLoadouts) in loadouts.SelectedLoadouts)
+                {
+                    var profileGroup = new ProfileLoadoutGroup()
+                    {
+                        GroupName = group,
+                    };
+
+                    foreach (var loadout in groupLoadouts)
+                    {
+                        profileGroup.Loadouts.Add(new ProfileLoadout()
+                        {
+                            LoadoutName = loadout.Prototype,
+                        });
+                    }
+
+                    dz.Groups.Add(profileGroup);
+                }
+
+                profile.Loadouts.Add(dz);
+            }
+
             return profile;
         }
         #endregion
index 886af60987cca1053011ad8ca92bff498ec59aa1..d521e26396a1ba8163488d07611883f171d7f46b 100644 (file)
@@ -709,7 +709,7 @@ public sealed class NukeopsRuleSystem : GameRuleSystem<NukeopsRuleComponent>
             _humanoid.LoadProfile(mob, profile);
 
         var gear = _prototypeManager.Index(spawnDetails.GearProto);
-        _stationSpawning.EquipStartingGear(mob, gear, profile);
+        _stationSpawning.EquipStartingGear(mob, gear);
 
         _npcFaction.RemoveFaction(mob, "NanoTrasen", false);
         _npcFaction.AddFaction(mob, "Syndicate");
index 900554d82410e1a4f10b0fd54d4723fe4fad1eeb..818d3c6e94ce7f18e0bd6e39fb97905362eb5008 100644 (file)
@@ -249,7 +249,7 @@ public sealed class PiratesRuleSystem : GameRuleSystem<PiratesRuleComponent>
 
                 _mindSystem.TransferTo(newMind, mob);
                 var profile = _prefs.GetPreferences(session.UserId).SelectedCharacter as HumanoidCharacterProfile;
-                _stationSpawningSystem.EquipStartingGear(mob, pirateGear, profile);
+                _stationSpawningSystem.EquipStartingGear(mob, pirateGear);
 
                 _npcFaction.RemoveFaction(mob, EnemyFactionId, false);
                 _npcFaction.AddFaction(mob, PirateFactionId);
index 25bb1072a52c9ee8a19a91c27d865b2ca08a8f4b..f6800f72890d55663846728366ee036953ca889b 100644 (file)
@@ -22,6 +22,7 @@ using Content.Server.Worldgen.Tools;
 using Content.Shared.Administration.Logs;
 using Content.Shared.Administration.Managers;
 using Content.Shared.Kitchen;
+using Content.Shared.Players.PlayTimeTracking;
 
 namespace Content.Server.IoC
 {
@@ -58,6 +59,7 @@ namespace Content.Server.IoC
             IoCManager.Register<PoissonDiskSampler>();
             IoCManager.Register<DiscordWebhook>();
             IoCManager.Register<ServerDbEntryManager>();
+            IoCManager.Register<ISharedPlaytimeManager, PlayTimeTrackingManager>();
             IoCManager.Register<ServerApi>();
         }
     }
index 5949d57197c7f8d440f212ba42f41d4efcce2a5f..4d2b01f76307fc17fc021c1b550bcb9461f717b8 100644 (file)
@@ -54,7 +54,7 @@ public delegate void CalcPlayTimeTrackersCallback(ICommonSession player, HashSet
 /// Operations like refreshing and sending play time info to clients are deferred until the next frame (note: not tick).
 /// </para>
 /// </remarks>
-public sealed class PlayTimeTrackingManager
+public sealed class PlayTimeTrackingManager : ISharedPlaytimeManager
 {
     [Dependency] private readonly IServerDbManager _db = default!;
     [Dependency] private readonly IServerNetManager _net = default!;
@@ -201,6 +201,11 @@ public sealed class PlayTimeTrackingManager
         }
     }
 
+    public IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session)
+    {
+        return GetTrackerTimes(session);
+    }
+
     private void SendPlayTimes(ICommonSession pSession)
     {
         var roles = GetTrackerTimes(pSession);
index e489ae28d586757a71148a62cfee0d9005aebda9..0f8cb83f10c80c9b9135b9c53d896f828cac14ef 100644 (file)
@@ -8,6 +8,7 @@ using Content.Shared.CCVar;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.Preferences;
 using Content.Shared.Roles;
+using Robust.Server.Player;
 using Robust.Shared.Configuration;
 using Robust.Shared.Network;
 using Robust.Shared.Player;
@@ -25,6 +26,7 @@ namespace Content.Server.Preferences.Managers
         [Dependency] private readonly IServerNetManager _netManager = default!;
         [Dependency] private readonly IConfigurationManager _cfg = default!;
         [Dependency] private readonly IServerDbManager _db = default!;
+        [Dependency] private readonly IPlayerManager _playerManager = default!;
         [Dependency] private readonly IPrototypeManager _protos = default!;
 
         // Cache player prefs on the server so we don't need as much async hell related to them.
@@ -98,8 +100,10 @@ namespace Content.Server.Preferences.Managers
             }
 
             var curPrefs = prefsData.Prefs!;
+            var session = _playerManager.GetSessionById(userId);
+            var collection = IoCManager.Instance!;
 
-            profile.EnsureValid(_cfg, _protos);
+            profile.EnsureValid(session, collection);
 
             var profiles = new Dictionary<int, ICharacterProfile>(curPrefs.Characters)
             {
@@ -260,17 +264,20 @@ namespace Content.Server.Preferences.Managers
                 return await _db.InitPrefsAsync(userId, HumanoidCharacterProfile.Random());
             }
 
-            return SanitizePreferences(prefs);
+            var session = _playerManager.GetSessionById(userId);
+            var collection = IoCManager.Instance!;
+
+            return SanitizePreferences(session, prefs, collection);
         }
 
-        private PlayerPreferences SanitizePreferences(PlayerPreferences prefs)
+        private PlayerPreferences SanitizePreferences(ICommonSession session, PlayerPreferences prefs, IDependencyCollection collection)
         {
             // Clean up preferences in case of changes to the game,
             // such as removed jobs still being selected.
 
             return new PlayerPreferences(prefs.Characters.Select(p =>
             {
-                return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(_cfg, _protos));
+                return new KeyValuePair<int, ICharacterProfile>(p.Key, p.Value.Validated(session, collection));
             }), prefs.SelectedCharacterIndex, prefs.AdminOOCColor);
         }
 
index b98e04844b9f141b6e2ef087717f99fd8fc618eb..5be4ff10f4f40cd547b6dc4792f8020703e7fdca 100644 (file)
@@ -51,7 +51,7 @@ public sealed class SpawnPointSystem : EntitySystem
             // TODO: Refactor gameticker spawning code so we don't have to do this!
             var points2 = EntityQueryEnumerator<SpawnPointComponent, TransformComponent>();
 
-            if (points2.MoveNext(out var uid, out var spawnPoint, out var xform))
+            if (points2.MoveNext(out var spawnPoint, out var xform))
             {
                 possiblePositions.Add(xform.Coordinates);
             }
index d0ebae9a0e64523add1ea27dfa8e36888f272865..3a3ab31d44bff8328282f9818858b976962066cf 100644 (file)
@@ -1,3 +1,4 @@
+using System.Linq;
 using Content.Server.Access.Systems;
 using Content.Server.DetailExaminable;
 using Content.Server.Humanoid;
@@ -10,10 +11,12 @@ using Content.Server.Station.Components;
 using Content.Shared.Access.Components;
 using Content.Shared.Access.Systems;
 using Content.Shared.CCVar;
+using Content.Shared.Clothing;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
 using Content.Shared.PDA;
 using Content.Shared.Preferences;
+using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Random;
 using Content.Shared.Random.Helpers;
 using Content.Shared.Roles;
@@ -86,7 +89,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
 
         if (station != null && profile != null)
         {
-            /// Try to call the character's preferred spawner first.
+            // Try to call the character's preferred spawner first.
             if (_spawnerCallbacks.TryGetValue(profile.SpawnPriority, out var preferredSpawner))
             {
                 preferredSpawner(ev);
@@ -101,9 +104,11 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
             }
             else
             {
-                /// Call all of them in the typical order.
+                // Call all of them in the typical order.
                 foreach (var typicalSpawner in _spawnerCallbacks.Values)
+                {
                     typicalSpawner(ev);
+                }
             }
         }
 
@@ -134,7 +139,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
         EntityUid? station,
         EntityUid? entity = null)
     {
-        _prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out JobPrototype? prototype);
+        _prototypeManager.TryIndex(job?.Prototype ?? string.Empty, out var prototype);
 
         // If we're not spawning a humanoid, we're gonna exit early without doing all the humanoid stuff.
         if (prototype?.JobEntity != null)
@@ -176,11 +181,38 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
         if (prototype?.StartingGear != null)
         {
             var startingGear = _prototypeManager.Index<StartingGearPrototype>(prototype.StartingGear);
-            EquipStartingGear(entity.Value, startingGear, profile);
+            EquipStartingGear(entity.Value, startingGear);
             if (profile != null)
                 EquipIdCard(entity.Value, profile.Name, prototype, station);
         }
 
+        // Run loadouts after so stuff like storage loadouts can get
+        var jobLoadout = LoadoutSystem.GetJobPrototype(prototype?.ID);
+
+        if (_prototypeManager.TryIndex(jobLoadout, out RoleLoadoutPrototype? loadoutProto))
+        {
+            RoleLoadout? loadout = null;
+            profile?.Loadouts.TryGetValue(jobLoadout, out loadout);
+
+            // Set to default if not present
+            if (loadout == null)
+            {
+                loadout = new RoleLoadout(jobLoadout);
+                loadout.SetDefault(_prototypeManager);
+            }
+
+            // Order loadout selections by the order they appear on the prototype.
+            foreach (var group in loadout.SelectedLoadouts.OrderBy(x => loadoutProto.Groups.FindIndex(e => e == x.Key)))
+            {
+                foreach (var items in group.Value)
+                {
+                    // Handle any extra data here.
+                    var startingGear = _prototypeManager.Index<StartingGearPrototype>(items.Prototype);
+                    EquipStartingGear(entity.Value, startingGear);
+                }
+            }
+        }
+
         if (profile != null)
         {
             _humanoidSystem.LoadProfile(entity.Value, profile);
index bb93e9e38c84ad1356bdda62854614f403354f30..897228553ce12eeb4bdf094dd4d085da9b82b74d 100644 (file)
@@ -1,4 +1,6 @@
+using System.Linq;
 using Content.Shared.Clothing.Components;
+using Content.Shared.Preferences.Loadouts;
 using Content.Shared.Roles;
 using Content.Shared.Station;
 using Robust.Shared.Prototypes;
@@ -24,12 +26,94 @@ public sealed class LoadoutSystem : EntitySystem
         SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
     }
 
+    public static string GetJobPrototype(string? loadout)
+    {
+        if (string.IsNullOrEmpty(loadout))
+            return string.Empty;
+
+        return "Job" + loadout;
+    }
+
+    /// <summary>
+    /// Tries to get the first entity prototype for operations such as sprite drawing.
+    /// </summary>
+    public EntProtoId? GetFirstOrNull(LoadoutPrototype loadout)
+    {
+        if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
+            return null;
+
+        var count = gear.Equipment.Count + gear.Inhand.Count + gear.Storage.Values.Sum(x => x.Count);
+
+        if (count == 1)
+        {
+            if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
+            {
+                return proto.ID;
+            }
+
+            if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
+            {
+                return proto.ID;
+            }
+
+            // Storage moment
+            foreach (var ents in gear.Storage.Values)
+            {
+                foreach (var ent in ents)
+                {
+                    return ent;
+                }
+            }
+        }
+
+        return null;
+    }
+
+    /// <summary>
+    /// Tries to get the name of a loadout.
+    /// </summary>
+    public string GetName(LoadoutPrototype loadout)
+    {
+        if (!_protoMan.TryIndex(loadout.Equipment, out var gear))
+            return Loc.GetString("loadout-unknown");
+
+        var count = gear.Equipment.Count + gear.Storage.Values.Sum(o => o.Count) + gear.Inhand.Count;
+
+        if (count == 1)
+        {
+            if (gear.Equipment.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Equipment.Values.First(), out var proto))
+            {
+                return proto.Name;
+            }
+
+            if (gear.Inhand.Count == 1 && _protoMan.TryIndex<EntityPrototype>(gear.Inhand[0], out proto))
+            {
+                return proto.Name;
+            }
+
+            foreach (var values in gear.Storage.Values)
+            {
+                if (values.Count != 1)
+                    continue;
+
+                if (_protoMan.TryIndex<EntityPrototype>(values[0], out proto))
+                {
+                    return proto.Name;
+                }
+
+                break;
+            }
+        }
+
+        return Loc.GetString($"loadout-{loadout.ID}");
+    }
+
     private void OnMapInit(EntityUid uid, LoadoutComponent component, MapInitEvent args)
     {
         if (component.Prototypes == null)
             return;
 
         var proto = _protoMan.Index<StartingGearPrototype>(_random.Pick(component.Prototypes));
-        _station.EquipStartingGear(uid, proto, null);
+        _station.EquipStartingGear(uid, proto);
     }
 }
index 3896839a06cdaaf1de6ec797d785b5efb9b71f60..4c1483ac485bcf4bbb93d5ec50738f95df3e4c13 100644 (file)
@@ -66,14 +66,14 @@ public sealed partial class SpeciesPrototype : IPrototype
     /// <summary>
     ///     Humanoid species variant used by this entity.
     /// </summary>
-    [DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string Prototype { get; private set; } = default!;
+    [DataField(required: true)]
+    public EntProtoId Prototype { get; private set; } = default!;
 
     /// <summary>
     /// Prototype used by the species for the dress-up doll in various menus.
     /// </summary>
-    [DataField(required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
-    public string DollPrototype { get; private set; } = default!;
+    [DataField(required: true)]
+    public EntProtoId DollPrototype { get; private set; } = default!;
 
     /// <summary>
     /// Method of skin coloration used by the species.
@@ -120,12 +120,6 @@ public sealed partial class SpeciesPrototype : IPrototype
     /// </summary>
     [DataField]
     public int MaxAge = 120;
-
-    /// <summary>
-    ///     The Style used for the guidebook info link in the character profile editor
-    /// </summary>
-    [DataField]
-    public string GuideBookIcon = "SpeciesInfoDefault";
 }
 
 public enum SpeciesNaming : byte
diff --git a/Content.Shared/Players/PlayTimeTracking/ISharedPlaytimeManager.cs b/Content.Shared/Players/PlayTimeTracking/ISharedPlaytimeManager.cs
new file mode 100644 (file)
index 0000000..d2839ea
--- /dev/null
@@ -0,0 +1,12 @@
+using Robust.Shared.Player;
+
+namespace Content.Shared.Players.PlayTimeTracking;
+
+public interface ISharedPlaytimeManager
+{
+    /// <summary>
+    /// Gets the playtimes for the session or an empty dictionary if none found.
+    /// </summary>
+    IReadOnlyDictionary<string, TimeSpan> GetPlayTimes(ICommonSession session);
+}
+
diff --git a/Content.Shared/Preferences/BackpackPreference.cs b/Content.Shared/Preferences/BackpackPreference.cs
deleted file mode 100644 (file)
index da2c4b8..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-namespace Content.Shared.Preferences
-{
-    /// <summary>
-    /// The backpack preference for a profile. Stored in database!
-    /// </summary>
-    public enum BackpackPreference
-    {
-        Backpack,
-        Satchel,
-        Duffelbag
-    }
-}
diff --git a/Content.Shared/Preferences/ClothingPreference.cs b/Content.Shared/Preferences/ClothingPreference.cs
deleted file mode 100644 (file)
index 24a4b20..0000000
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace Content.Shared.Preferences
-{
-    /// <summary>
-    /// The clothing preference for a profile. Stored in database!
-    /// </summary>
-    public enum ClothingPreference
-    {
-        Jumpsuit,
-        Jumpskirt
-    }
-}
index 9cf45aaefd56dd322211e50a2bd5b7d39aa3bdb1..3f3521fda6ad5bb12496657e3c0521e79bd2863f 100644 (file)
@@ -1,15 +1,17 @@
 using System.Linq;
-using System.Globalization;
 using System.Text.RegularExpressions;
 using Content.Shared.CCVar;
 using Content.Shared.GameTicking;
 using Content.Shared.Humanoid;
 using Content.Shared.Humanoid.Prototypes;
-using Content.Shared.Random.Helpers;
+using Content.Shared.Preferences.Loadouts;
+using Content.Shared.Preferences.Loadouts.Effects;
 using Content.Shared.Roles;
 using Content.Shared.Traits;
+using Robust.Shared.Collections;
 using Robust.Shared.Configuration;
 using Robust.Shared.Enums;
+using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 using Robust.Shared.Serialization;
@@ -31,6 +33,11 @@ namespace Content.Shared.Preferences
         private readonly List<string> _antagPreferences;
         private readonly List<string> _traitPreferences;
 
+        public IReadOnlyDictionary<string, RoleLoadout> Loadouts => _loadouts;
+
+        private Dictionary<string, RoleLoadout> _loadouts;
+
+        // What in the lord is happening here.
         private HumanoidCharacterProfile(
             string name,
             string flavortext,
@@ -39,13 +46,12 @@ namespace Content.Shared.Preferences
             Sex sex,
             Gender gender,
             HumanoidCharacterAppearance appearance,
-            ClothingPreference clothing,
-            BackpackPreference backpack,
             SpawnPriorityPreference spawnPriority,
             Dictionary<string, JobPriority> jobPriorities,
             PreferenceUnavailableMode preferenceUnavailable,
             List<string> antagPreferences,
-            List<string> traitPreferences)
+            List<string> traitPreferences,
+            Dictionary<string, RoleLoadout> loadouts)
         {
             Name = name;
             FlavorText = flavortext;
@@ -54,13 +60,12 @@ namespace Content.Shared.Preferences
             Sex = sex;
             Gender = gender;
             Appearance = appearance;
-            Clothing = clothing;
-            Backpack = backpack;
             SpawnPriority = spawnPriority;
             _jobPriorities = jobPriorities;
             PreferenceUnavailable = preferenceUnavailable;
             _antagPreferences = antagPreferences;
             _traitPreferences = traitPreferences;
+            _loadouts = loadouts;
         }
 
         /// <summary>Copy constructor but with overridable references (to prevent useless copies)</summary>
@@ -68,15 +73,16 @@ namespace Content.Shared.Preferences
             HumanoidCharacterProfile other,
             Dictionary<string, JobPriority> jobPriorities,
             List<string> antagPreferences,
-            List<string> traitPreferences)
-            : this(other.Name, other.FlavorText, other.Species, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, other.SpawnPriority,
-                jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences)
+            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))
+            : this(other, new Dictionary<string, JobPriority>(other.JobPriorities), new List<string>(other.AntagPreferences), new List<string>(other.TraitPreferences), new Dictionary<string, RoleLoadout>(other.Loadouts))
         {
         }
 
@@ -88,15 +94,14 @@ namespace Content.Shared.Preferences
             Sex sex,
             Gender gender,
             HumanoidCharacterAppearance appearance,
-            ClothingPreference clothing,
-            BackpackPreference backpack,
             SpawnPriorityPreference spawnPriority,
             IReadOnlyDictionary<string, JobPriority> jobPriorities,
             PreferenceUnavailableMode preferenceUnavailable,
             IReadOnlyList<string> antagPreferences,
-            IReadOnlyList<string> traitPreferences)
-            : this(name, flavortext, species, age, sex, gender, appearance, clothing, backpack, spawnPriority, new Dictionary<string, JobPriority>(jobPriorities),
-                preferenceUnavailable, new List<string>(antagPreferences), new List<string>(traitPreferences))
+            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))
         {
         }
 
@@ -113,8 +118,6 @@ namespace Content.Shared.Preferences
             Sex.Male,
             Gender.Male,
             new HumanoidCharacterAppearance(),
-            ClothingPreference.Jumpsuit,
-            BackpackPreference.Backpack,
             SpawnPriorityPreference.None,
             new Dictionary<string, JobPriority>
             {
@@ -122,7 +125,8 @@ namespace Content.Shared.Preferences
             },
             PreferenceUnavailableMode.SpawnAsOverflow,
             new List<string>(),
-            new List<string>())
+            new List<string>(),
+            new Dictionary<string, RoleLoadout>())
         {
         }
 
@@ -141,8 +145,6 @@ namespace Content.Shared.Preferences
                 Sex.Male,
                 Gender.Male,
                 HumanoidCharacterAppearance.DefaultWithSpecies(species),
-                ClothingPreference.Jumpsuit,
-                BackpackPreference.Backpack,
                 SpawnPriorityPreference.None,
                 new Dictionary<string, JobPriority>
                 {
@@ -150,7 +152,8 @@ namespace Content.Shared.Preferences
                 },
                 PreferenceUnavailableMode.SpawnAsOverflow,
                 new List<string>(),
-                new List<string>());
+                new List<string>(),
+                new Dictionary<string, RoleLoadout>());
         }
 
         // TODO: This should eventually not be a visual change only.
@@ -195,11 +198,11 @@ namespace Content.Shared.Preferences
 
             var name = GetName(species, gender);
 
-            return new HumanoidCharacterProfile(name, "", species, age, sex, gender, HumanoidCharacterAppearance.Random(species, sex), ClothingPreference.Jumpsuit, BackpackPreference.Backpack, SpawnPriorityPreference.None,
+            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>());
+                }, PreferenceUnavailableMode.StayInLobby, new List<string>(), new List<string>(), new Dictionary<string, RoleLoadout>());
         }
 
         public string Name { get; private set; }
@@ -219,8 +222,6 @@ namespace Content.Shared.Preferences
 
         [DataField("appearance")]
         public HumanoidCharacterAppearance Appearance { get; private set; }
-        public ClothingPreference Clothing { get; private set; }
-        public BackpackPreference Backpack { get; private set; }
         public SpawnPriorityPreference SpawnPriority { get; private set; }
         public IReadOnlyDictionary<string, JobPriority> JobPriorities => _jobPriorities;
         public IReadOnlyList<string> AntagPreferences => _antagPreferences;
@@ -263,21 +264,14 @@ namespace Content.Shared.Preferences
             return new(this) { Appearance = appearance };
         }
 
-        public HumanoidCharacterProfile WithClothingPreference(ClothingPreference clothing)
-        {
-            return new(this) { Clothing = clothing };
-        }
-        public HumanoidCharacterProfile WithBackpackPreference(BackpackPreference backpack)
-        {
-            return new(this) { Backpack = backpack };
-        }
         public HumanoidCharacterProfile WithSpawnPriorityPreference(SpawnPriorityPreference spawnPriority)
         {
             return new(this) { SpawnPriority = spawnPriority };
         }
+
         public HumanoidCharacterProfile WithJobPriorities(IEnumerable<KeyValuePair<string, JobPriority>> jobPriorities)
         {
-            return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences);
+            return new(this, new Dictionary<string, JobPriority>(jobPriorities), _antagPreferences, _traitPreferences, _loadouts);
         }
 
         public HumanoidCharacterProfile WithJobPriority(string jobId, JobPriority priority)
@@ -291,7 +285,7 @@ namespace Content.Shared.Preferences
             {
                 dictionary[jobId] = priority;
             }
-            return new(this, dictionary, _antagPreferences, _traitPreferences);
+            return new(this, dictionary, _antagPreferences, _traitPreferences, _loadouts);
         }
 
         public HumanoidCharacterProfile WithPreferenceUnavailable(PreferenceUnavailableMode mode)
@@ -301,7 +295,7 @@ namespace Content.Shared.Preferences
 
         public HumanoidCharacterProfile WithAntagPreferences(IEnumerable<string> antagPreferences)
         {
-            return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences);
+            return new(this, _jobPriorities, new List<string>(antagPreferences), _traitPreferences, _loadouts);
         }
 
         public HumanoidCharacterProfile WithAntagPreference(string antagId, bool pref)
@@ -321,7 +315,7 @@ namespace Content.Shared.Preferences
                     list.Remove(antagId);
                 }
             }
-            return new(this, _jobPriorities, list, _traitPreferences);
+            return new(this, _jobPriorities, list, _traitPreferences, _loadouts);
         }
 
         public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref)
@@ -343,7 +337,7 @@ namespace Content.Shared.Preferences
                     list.Remove(traitId);
                 }
             }
-            return new(this, _jobPriorities, _antagPreferences, list);
+            return new(this, _jobPriorities, _antagPreferences, list, _loadouts);
         }
 
         public string Summary =>
@@ -362,17 +356,19 @@ namespace Content.Shared.Preferences
             if (Sex != other.Sex) return false;
             if (Gender != other.Gender) return false;
             if (PreferenceUnavailable != other.PreferenceUnavailable) return false;
-            if (Clothing != other.Clothing) return false;
-            if (Backpack != other.Backpack) return false;
             if (SpawnPriority != other.SpawnPriority) return false;
             if (!_jobPriorities.SequenceEqual(other._jobPriorities)) return false;
             if (!_antagPreferences.SequenceEqual(other._antagPreferences)) return false;
             if (!_traitPreferences.SequenceEqual(other._traitPreferences)) return false;
+            if (!Loadouts.SequenceEqual(other.Loadouts)) return false;
             return Appearance.MemberwiseEquals(other.Appearance);
         }
 
-        public void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager)
+        public void EnsureValid(ICommonSession session, IDependencyCollection collection)
         {
+            var configManager = collection.Resolve<IConfigurationManager>();
+            var prototypeManager = collection.Resolve<IPrototypeManager>();
+
             if (!prototypeManager.TryIndex<SpeciesPrototype>(Species, out var speciesPrototype) || speciesPrototype.RoundStart == false)
             {
                 Species = SharedHumanoidAppearanceSystem.DefaultSpecies;
@@ -455,21 +451,6 @@ namespace Content.Shared.Preferences
                 _ => PreferenceUnavailableMode.StayInLobby // Invalid enum values.
             };
 
-            var clothing = Clothing switch
-            {
-                ClothingPreference.Jumpsuit => ClothingPreference.Jumpsuit,
-                ClothingPreference.Jumpskirt => ClothingPreference.Jumpskirt,
-                _ => ClothingPreference.Jumpsuit // Invalid enum values.
-            };
-
-            var backpack = Backpack switch
-            {
-                BackpackPreference.Backpack => BackpackPreference.Backpack,
-                BackpackPreference.Satchel => BackpackPreference.Satchel,
-                BackpackPreference.Duffelbag => BackpackPreference.Duffelbag,
-                _ => BackpackPreference.Backpack // Invalid enum values.
-            };
-
             var spawnPriority = SpawnPriority switch
             {
                 SpawnPriorityPreference.None => SpawnPriorityPreference.None,
@@ -502,8 +483,6 @@ namespace Content.Shared.Preferences
             Sex = sex;
             Gender = gender;
             Appearance = appearance;
-            Clothing = clothing;
-            Backpack = backpack;
             SpawnPriority = spawnPriority;
 
             _jobPriorities.Clear();
@@ -520,12 +499,31 @@ namespace Content.Shared.Preferences
 
             _traitPreferences.Clear();
             _traitPreferences.AddRange(traits);
+
+            // Checks prototypes exist for all loadouts and dump / set to default if not.
+            var toRemove = new ValueList<string>();
+
+            foreach (var (roleName, loadouts) in _loadouts)
+            {
+                if (!prototypeManager.HasIndex<RoleLoadoutPrototype>(roleName))
+                {
+                    toRemove.Add(roleName);
+                    continue;
+                }
+
+                loadouts.EnsureValid(session, collection);
+            }
+
+            foreach (var value in toRemove)
+            {
+                _loadouts.Remove(value);
+            }
         }
 
-        public ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager)
+        public ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection)
         {
             var profile = new HumanoidCharacterProfile(this);
-            profile.EnsureValid(configManager, prototypeManager);
+            profile.EnsureValid(session, collection);
             return profile;
         }
 
@@ -551,16 +549,32 @@ namespace Content.Shared.Preferences
                     Age,
                     Sex,
                     Gender,
-                    Appearance,
-                    Clothing,
-                    Backpack
+                    Appearance
                 ),
                 SpawnPriority,
                 PreferenceUnavailable,
                 _jobPriorities,
                 _antagPreferences,
-                _traitPreferences
+                _traitPreferences,
+                _loadouts
             );
         }
+
+        public void SetLoadout(RoleLoadout loadout)
+        {
+            _loadouts[loadout.Role.Id] = loadout;
+        }
+
+        public RoleLoadout GetLoadoutOrDefault(string id, IEntityManager entManager, IPrototypeManager protoManager)
+        {
+            if (!_loadouts.TryGetValue(id, out var loadout))
+            {
+                loadout = new RoleLoadout(id);
+                loadout.SetDefault(protoManager, force: true);
+            }
+
+            loadout.SetDefault(protoManager);
+            return loadout;
+        }
     }
 }
index a9d30639bb35929badb60f576006eba9b80c04e0..3e7dbc41588f0b5f723d392316bc9018cd224f76 100644 (file)
@@ -1,5 +1,6 @@
 using Content.Shared.Humanoid;
 using Robust.Shared.Configuration;
+using Robust.Shared.Player;
 using Robust.Shared.Prototypes;
 
 namespace Content.Shared.Preferences
@@ -15,11 +16,11 @@ namespace Content.Shared.Preferences
         /// <summary>
         ///     Makes this profile valid so there's no bad data like negative ages.
         /// </summary>
-        void EnsureValid(IConfigurationManager configManager, IPrototypeManager prototypeManager);
+        void EnsureValid(ICommonSession session, IDependencyCollection collection);
 
         /// <summary>
         /// Gets a copy of this profile that has <see cref="EnsureValid"/> applied, i.e. no invalid data.
         /// </summary>
-        ICharacterProfile Validated(IConfigurationManager configManager, IPrototypeManager prototypeManager);
+        ICharacterProfile Validated(ICommonSession session, IDependencyCollection collection);
     }
 }
diff --git a/Content.Shared/Preferences/Loadouts/Effects/GroupLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/GroupLoadoutEffect.cs
new file mode 100644 (file)
index 0000000..5a2cd87
--- /dev/null
@@ -0,0 +1,29 @@
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Preferences.Loadouts.Effects;
+
+/// <summary>
+/// Uses a <see cref="LoadoutEffectGroupPrototype"/> prototype as a singular effect that can be re-used.
+/// </summary>
+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)
+    {
+        var effectsProto = collection.Resolve<IPrototypeManager>().Index(Proto);
+
+        foreach (var effect in effectsProto.Effects)
+        {
+            if (!effect.Validate(loadout, session, collection, out reason))
+                return false;
+        }
+
+        reason = null;
+        return true;
+    }
+}
diff --git a/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/JobRequirementLoadoutEffect.cs
new file mode 100644 (file)
index 0000000..4a750a5
--- /dev/null
@@ -0,0 +1,26 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Players.PlayTimeTracking;
+using Content.Shared.Roles;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Preferences.Loadouts.Effects;
+
+/// <summary>
+/// Checks for a job requirement to be met such as playtime.
+/// </summary>
+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)
+    {
+        var manager = collection.Resolve<ISharedPlaytimeManager>();
+        var playtimes = manager.GetPlayTimes(session);
+        return JobRequirements.TryRequirementMet(Requirement, playtimes, out reason,
+            collection.Resolve<IEntityManager>(),
+            collection.Resolve<IPrototypeManager>());
+    }
+}
diff --git a/Content.Shared/Preferences/Loadouts/Effects/LoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/LoadoutEffect.cs
new file mode 100644 (file)
index 0000000..65694d5
--- /dev/null
@@ -0,0 +1,20 @@
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Player;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Preferences.Loadouts.Effects;
+
+[ImplicitDataDefinitionForInheritors]
+public abstract partial class LoadoutEffect
+{
+    /// <summary>
+    /// Tries to validate the effect.
+    /// </summary>
+    public abstract bool Validate(
+        RoleLoadout loadout,
+        ICommonSession session,
+        IDependencyCollection collection,
+        [NotNullWhen(false)] out FormattedMessage? reason);
+
+    public virtual void Apply(RoleLoadout loadout) {}
+}
diff --git a/Content.Shared/Preferences/Loadouts/Effects/LoadoutEffectGroupPrototype.cs b/Content.Shared/Preferences/Loadouts/Effects/LoadoutEffectGroupPrototype.cs
new file mode 100644 (file)
index 0000000..0851be8
--- /dev/null
@@ -0,0 +1,16 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Preferences.Loadouts.Effects;
+
+/// <summary>
+/// Stores a group of loadout effects in a prototype for re-use.
+/// </summary>
+[Prototype]
+public sealed class LoadoutEffectGroupPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    [DataField(required: true)]
+    public List<LoadoutEffect> Effects = new();
+}
diff --git a/Content.Shared/Preferences/Loadouts/Effects/PointsCostLoadoutEffect.cs b/Content.Shared/Preferences/Loadouts/Effects/PointsCostLoadoutEffect.cs
new file mode 100644 (file)
index 0000000..3146ff6
--- /dev/null
@@ -0,0 +1,40 @@
+using System.Diagnostics.CodeAnalysis;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Preferences.Loadouts.Effects;
+
+public sealed partial class PointsCostLoadoutEffect : LoadoutEffect
+{
+    [DataField(required: true)]
+    public int Cost = 1;
+
+    public override bool Validate(
+        RoleLoadout loadout,
+        ICommonSession session,
+        IDependencyCollection collection,
+        [NotNullWhen(false)] out FormattedMessage? reason)
+    {
+        reason = null;
+        var protoManager = collection.Resolve<IPrototypeManager>();
+
+        if (!protoManager.TryIndex(loadout.Role, out var roleProto) || roleProto.Points == null)
+        {
+            return true;
+        }
+
+        if (loadout.Points <= Cost)
+        {
+            reason = FormattedMessage.FromUnformatted("loadout-group-points-insufficient");
+            return false;
+        }
+
+        return true;
+    }
+
+    public override void Apply(RoleLoadout loadout)
+    {
+        loadout.Points -= Cost;
+    }
+}
diff --git a/Content.Shared/Preferences/Loadouts/Loadout.cs b/Content.Shared/Preferences/Loadouts/Loadout.cs
new file mode 100644 (file)
index 0000000..6a4373b
--- /dev/null
@@ -0,0 +1,13 @@
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Preferences.Loadouts;
+
+/// <summary>
+/// Specifies the selected prototype and custom data for a loadout.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class Loadout
+{
+    public ProtoId<LoadoutPrototype> Prototype;
+}
diff --git a/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs b/Content.Shared/Preferences/Loadouts/LoadoutGroupPrototype.cs
new file mode 100644 (file)
index 0000000..63f3b73
--- /dev/null
@@ -0,0 +1,34 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Preferences.Loadouts;
+
+/// <summary>
+/// Corresponds to a set of loadouts for a particular slot.
+/// </summary>
+[Prototype("loadoutGroup")]
+public sealed class LoadoutGroupPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    /// <summary>
+    /// User-friendly name for the group.
+    /// </summary>
+    [DataField(required: true)]
+    public LocId Name;
+
+    /// <summary>
+    /// Minimum number of loadouts that need to be specified for this category.
+    /// </summary>
+    [DataField]
+    public int MinLimit = 1;
+
+    /// <summary>
+    /// Maximum limit for the category.
+    /// </summary>
+    [DataField]
+    public int MaxLimit = 1;
+
+    [DataField(required: true)]
+    public List<ProtoId<LoadoutPrototype>> Loadouts = new();
+}
diff --git a/Content.Shared/Preferences/Loadouts/LoadoutPrototype.cs b/Content.Shared/Preferences/Loadouts/LoadoutPrototype.cs
new file mode 100644 (file)
index 0000000..43aedd8
--- /dev/null
@@ -0,0 +1,25 @@
+using Content.Shared.Preferences.Loadouts.Effects;
+using Content.Shared.Roles;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Preferences.Loadouts;
+
+/// <summary>
+/// Individual loadout item to be applied.
+/// </summary>
+[Prototype]
+public sealed class LoadoutPrototype : IPrototype
+{
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    [DataField(required: true)]
+    public ProtoId<StartingGearPrototype> Equipment;
+
+    /// <summary>
+    /// Effects to be applied when the loadout is applied.
+    /// These can also return true or false for validation purposes.
+    /// </summary>
+    [DataField]
+    public List<LoadoutEffect> Effects = new();
+}
diff --git a/Content.Shared/Preferences/Loadouts/RoleLoadout.cs b/Content.Shared/Preferences/Loadouts/RoleLoadout.cs
new file mode 100644 (file)
index 0000000..78cd376
--- /dev/null
@@ -0,0 +1,248 @@
+using System.Diagnostics.CodeAnalysis;
+using Content.Shared.Random;
+using Robust.Shared.Collections;
+using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Serialization;
+using Robust.Shared.Utility;
+
+namespace Content.Shared.Preferences.Loadouts;
+
+/// <summary>
+/// Contains all of the selected data for a role's loadout.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class RoleLoadout
+{
+    public readonly ProtoId<RoleLoadoutPrototype> Role;
+
+    public Dictionary<ProtoId<LoadoutGroupPrototype>, List<Loadout>> SelectedLoadouts = new();
+
+    /*
+     * Loadout-specific data used for validation.
+     */
+
+    public int? Points;
+
+    public RoleLoadout(ProtoId<RoleLoadoutPrototype> role)
+    {
+        Role = role;
+    }
+
+    /// <summary>
+    /// Ensures all prototypes exist and effects can be applied.
+    /// </summary>
+    public void EnsureValid(ICommonSession session, IDependencyCollection collection)
+    {
+        var groupRemove = new ValueList<string>();
+        var protoManager = collection.Resolve<IPrototypeManager>();
+
+        if (!protoManager.TryIndex(Role, out var roleProto))
+        {
+            SelectedLoadouts.Clear();
+            return;
+        }
+
+        // Reset points to recalculate.
+        Points = roleProto.Points;
+
+        foreach (var (group, groupLoadouts) in SelectedLoadouts)
+        {
+            // Dump if Group doesn't exist
+            if (!protoManager.TryIndex(group, out var groupProto))
+            {
+                groupRemove.Add(group);
+                continue;
+            }
+
+            var loadouts = groupLoadouts[..Math.Min(groupLoadouts.Count, groupProto.MaxLimit)];
+
+            // Validate first
+            for (var i = loadouts.Count - 1; i >= 0; i--)
+            {
+                var loadout = loadouts[i];
+
+                if (!protoManager.TryIndex(loadout.Prototype, out var loadoutProto))
+                {
+                    loadouts.RemoveAt(i);
+                    continue;
+                }
+
+                // Validate the loadout can be applied (e.g. points).
+                if (!IsValid(session, loadout.Prototype, collection, out _))
+                {
+                    loadouts.RemoveAt(i);
+                    continue;
+                }
+
+                Apply(loadoutProto);
+            }
+
+            // Apply defaults if required
+            // Technically it's possible for someone to game themselves into loadouts they shouldn't have
+            // If you put invalid ones first but that's your fault for not using sensible defaults
+            if (loadouts.Count < groupProto.MinLimit)
+            {
+                for (var i = 0; i < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); i++)
+                {
+                    if (!protoManager.TryIndex(groupProto.Loadouts[i], out var loadoutProto))
+                        continue;
+
+                    var defaultLoadout = new Loadout()
+                    {
+                        Prototype = loadoutProto.ID,
+                    };
+
+                    if (loadouts.Contains(defaultLoadout))
+                        continue;
+
+                    // Still need to apply the effects even if validation is ignored.
+                    loadouts.Add(defaultLoadout);
+                    Apply(loadoutProto);
+                }
+            }
+
+            SelectedLoadouts[group] = loadouts;
+        }
+
+        foreach (var value in groupRemove)
+        {
+            SelectedLoadouts.Remove(value);
+        }
+    }
+
+    private void Apply(LoadoutPrototype loadoutProto)
+    {
+        foreach (var effect in loadoutProto.Effects)
+        {
+            effect.Apply(this);
+        }
+    }
+
+    /// <summary>
+    /// Resets the selected loadouts to default if no data is present.
+    /// </summary>
+    public void SetDefault(IPrototypeManager protoManager, bool force = false)
+    {
+        if (force)
+            SelectedLoadouts.Clear();
+
+        var roleProto = protoManager.Index(Role);
+
+        for (var i = roleProto.Groups.Count - 1; i >= 0; i--)
+        {
+            var group = roleProto.Groups[i];
+
+            if (!protoManager.TryIndex(group, out var groupProto))
+                continue;
+
+            if (SelectedLoadouts.ContainsKey(group))
+                continue;
+
+            SelectedLoadouts[group] = new List<Loadout>();
+
+            if (groupProto.MinLimit > 0)
+            {
+                // Apply any loadouts we can.
+                for (var j = 0; j < Math.Min(groupProto.MinLimit, groupProto.Loadouts.Count); j++)
+                {
+                    AddLoadout(group, groupProto.Loadouts[j], protoManager);
+                }
+            }
+        }
+    }
+
+    /// <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)
+    {
+        reason = null;
+
+        var protoManager = collection.Resolve<IPrototypeManager>();
+
+        if (!protoManager.TryIndex(loadout, out var loadoutProto))
+        {
+            // Uhh
+            reason = FormattedMessage.FromMarkup("");
+            return false;
+        }
+
+        if (!protoManager.TryIndex(Role, out var roleProto))
+        {
+            reason = FormattedMessage.FromUnformatted("loadouts-prototype-missing");
+            return false;
+        }
+
+        var valid = true;
+
+        foreach (var effect in loadoutProto.Effects)
+        {
+            valid = valid && effect.Validate(this, session, collection, out reason);
+        }
+
+        return valid;
+    }
+
+    /// <summary>
+    /// Applies the specified loadout to this group.
+    /// </summary>
+    public bool AddLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
+    {
+        var groupLoadouts = SelectedLoadouts[selectedGroup];
+
+        // Need to unselect existing ones if we're at or above limit
+        var limit = Math.Max(0, groupLoadouts.Count + 1 - protoManager.Index(selectedGroup).MaxLimit);
+
+        for (var i = 0; i < groupLoadouts.Count; i++)
+        {
+            var loadout = groupLoadouts[i];
+
+            if (loadout.Prototype != selectedLoadout)
+            {
+                // Remove any other loadouts that might push it above the limit.
+                if (limit > 0)
+                {
+                    limit--;
+                    groupLoadouts.RemoveAt(i);
+                    i--;
+                }
+
+                continue;
+            }
+
+            DebugTools.Assert(false);
+            return false;
+        }
+
+        groupLoadouts.Add(new Loadout()
+        {
+            Prototype = selectedLoadout,
+        });
+
+        return true;
+    }
+
+    /// <summary>
+    /// Removed the specified loadout from this group.
+    /// </summary>
+    public bool RemoveLoadout(ProtoId<LoadoutGroupPrototype> selectedGroup, ProtoId<LoadoutPrototype> selectedLoadout, IPrototypeManager protoManager)
+    {
+        // Although this may bring us below minimum we'll let EnsureValid handle it.
+
+        var groupLoadouts = SelectedLoadouts[selectedGroup];
+
+        for (var i = 0; i < groupLoadouts.Count; i++)
+        {
+            var loadout = groupLoadouts[i];
+
+            if (loadout.Prototype != selectedLoadout)
+                continue;
+
+            groupLoadouts.RemoveAt(i);
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs b/Content.Shared/Preferences/Loadouts/RoleLoadoutPrototype.cs
new file mode 100644 (file)
index 0000000..58e0279
--- /dev/null
@@ -0,0 +1,29 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Preferences.Loadouts;
+
+/// <summary>
+/// Corresponds to a Job / Antag prototype and specifies loadouts
+/// </summary>
+[Prototype]
+public sealed class RoleLoadoutPrototype : IPrototype
+{
+    /*
+     * Separate to JobPrototype / AntagPrototype as they are turning into messy god classes.
+     */
+
+    [IdDataField]
+    public string ID { get; } = string.Empty;
+
+    /// <summary>
+    /// Groups that comprise this role loadout.
+    /// </summary>
+    [DataField(required: true)]
+    public List<ProtoId<LoadoutGroupPrototype>> Groups = new();
+
+    /// <summary>
+    /// How many points are allotted for this role loadout prototype.
+    /// </summary>
+    [DataField]
+    public int? Points;
+}
index b1d5f43ede328cfac89b5c5b7f2df2b5f41c59d3..ba559fadd5cd9aa399893c6b418e34d1d0dc3594 100644 (file)
@@ -96,7 +96,7 @@ namespace Content.Shared.Roles
         /// </summary>
         public static bool TryRequirementMet(
             JobRequirement requirement,
-            Dictionary<string, TimeSpan> playTimes,
+            IReadOnlyDictionary<string, TimeSpan> playTimes,
             [NotNullWhen(false)] out FormattedMessage? reason,
             IEntityManager entManager,
             IPrototypeManager prototypes)
@@ -162,7 +162,7 @@ namespace Content.Shared.Roles
                             return true;
 
                         reason = FormattedMessage.FromMarkup(Loc.GetString(
-                              "role-timer-overall-insufficient", 
+                              "role-timer-overall-insufficient",
                               ("time", Math.Ceiling(overallDiff))));
                         return false;
                     }
index 0d4122b6466e20223fdd1cb7aa960fe6dc7a5b11..fc9ecec7afe023df36b5881707c34bf5e86bb569 100644 (file)
@@ -9,37 +9,21 @@ namespace Content.Shared.Roles
         [DataField]
         public Dictionary<string, EntProtoId> Equipment = new();
 
-        /// <summary>
-        /// if empty, there is no skirt override - instead the uniform provided in equipment is added.
-        /// </summary>
-        [DataField]
-        public EntProtoId? InnerClothingSkirt;
-
-        [DataField]
-        public EntProtoId? Satchel;
-
         [DataField]
-        public EntProtoId? Duffelbag;
+        public List<EntProtoId> Inhand = new(0);
 
+        /// <summary>
+        /// Inserts entities into the specified slot's storage (if it does have storage).
+        /// </summary>
         [DataField]
-        public List<EntProtoId> Inhand = new(0);
+        public Dictionary<string, List<EntProtoId>> Storage = new();
 
         [ViewVariables]
         [IdDataField]
         public string ID { get; private set; } = string.Empty;
 
-        public string GetGear(string slot, HumanoidCharacterProfile? profile)
+        public string GetGear(string slot)
         {
-            if (profile != null)
-            {
-                if (slot == "jumpsuit" && profile.Clothing == ClothingPreference.Jumpskirt && !string.IsNullOrEmpty(InnerClothingSkirt))
-                    return InnerClothingSkirt;
-                if (slot == "back" && profile.Backpack == BackpackPreference.Satchel && !string.IsNullOrEmpty(Satchel))
-                    return Satchel;
-                if (slot == "back" && profile.Backpack == BackpackPreference.Duffelbag && !string.IsNullOrEmpty(Duffelbag))
-                    return Duffelbag;
-            }
-
             return Equipment.TryGetValue(slot, out var equipment) ? equipment : string.Empty;
         }
     }
index 8cdcff883b15468b9c634515077ad6632d95dfdc..49ef8509db236637d4371d7627815da6e267e5c0 100644 (file)
@@ -3,6 +3,9 @@ using Content.Shared.Hands.EntitySystems;
 using Content.Shared.Inventory;
 using Content.Shared.Preferences;
 using Content.Shared.Roles;
+using Content.Shared.Storage;
+using Content.Shared.Storage.EntitySystems;
+using Robust.Shared.Collections;
 
 namespace Content.Shared.Station;
 
@@ -10,40 +13,69 @@ public abstract class SharedStationSpawningSystem : EntitySystem
 {
     [Dependency] protected readonly InventorySystem InventorySystem = default!;
     [Dependency] private   readonly SharedHandsSystem _handsSystem = default!;
+    [Dependency] private   readonly SharedStorageSystem _storage = default!;
+    [Dependency] private   readonly SharedTransformSystem _xformSystem = default!;
 
     /// <summary>
     /// Equips starting gear onto the given entity.
     /// </summary>
     /// <param name="entity">Entity to load out.</param>
     /// <param name="startingGear">Starting gear to use.</param>
-    /// <param name="profile">Character profile to use, if any.</param>
-    public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear, HumanoidCharacterProfile? profile)
+    public void EquipStartingGear(EntityUid entity, StartingGearPrototype startingGear)
     {
         if (InventorySystem.TryGetSlots(entity, out var slotDefinitions))
         {
             foreach (var slot in slotDefinitions)
             {
-                var equipmentStr = startingGear.GetGear(slot.Name, profile);
+                var equipmentStr = startingGear.GetGear(slot.Name);
                 if (!string.IsNullOrEmpty(equipmentStr))
                 {
                     var equipmentEntity = EntityManager.SpawnEntity(equipmentStr, EntityManager.GetComponent<TransformComponent>(entity).Coordinates);
-                    InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, true, force:true);
+                    InventorySystem.TryEquip(entity, equipmentEntity, slot.Name, silent: true, force:true);
                 }
             }
         }
 
-        if (!TryComp(entity, out HandsComponent? handsComponent))
-            return;
+        if (TryComp(entity, out HandsComponent? handsComponent))
+        {
+            var inhand = startingGear.Inhand;
+            var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
+            foreach (var prototype in inhand)
+            {
+                var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
+
+                if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
+                {
+                    _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
+                }
+            }
+        }
 
-        var inhand = startingGear.Inhand;
-        var coords = EntityManager.GetComponent<TransformComponent>(entity).Coordinates;
-        foreach (var prototype in inhand)
+        if (startingGear.Storage.Count > 0)
         {
-            var inhandEntity = EntityManager.SpawnEntity(prototype, coords);
+            var coords = _xformSystem.GetMapCoordinates(entity);
+            var ents = new ValueList<EntityUid>();
+            TryComp(entity, out InventoryComponent? inventoryComp);
 
-            if (_handsSystem.TryGetEmptyHand(entity, out var emptyHand, handsComponent))
+            foreach (var (slot, entProtos) in startingGear.Storage)
             {
-                _handsSystem.TryPickup(entity, inhandEntity, emptyHand, checkActionBlocker: false, handsComp: handsComponent);
+                if (entProtos.Count == 0)
+                    continue;
+
+                foreach (var ent in entProtos)
+                {
+                    ents.Add(Spawn(ent, coords));
+                }
+
+                if (inventoryComp != null &&
+                    InventorySystem.TryGetSlotEntity(entity, slot, out var slotEnt, inventoryComponent: inventoryComp) &&
+                    TryComp(slotEnt, out StorageComponent? storage))
+                {
+                    foreach (var ent in ents)
+                    {
+                        _storage.Insert(slotEnt.Value, ent, out _, storageComp: storage, playSound: false);
+                    }
+                }
             }
         }
     }
diff --git a/Resources/Locale/en-US/job/loadouts.ftl b/Resources/Locale/en-US/job/loadouts.ftl
new file mode 100644 (file)
index 0000000..6e1f074
--- /dev/null
@@ -0,0 +1,2 @@
+loadout-window = Loadout
+loadout-none = None
diff --git a/Resources/Locale/en-US/preferences/loadout-groups.ftl b/Resources/Locale/en-US/preferences/loadout-groups.ftl
new file mode 100644 (file)
index 0000000..f0bb3ad
--- /dev/null
@@ -0,0 +1,159 @@
+# Miscellaneous
+loadout-group-trinkets = Trinkets
+
+# Command
+loadout-group-captain-head = Captain head
+loadout-group-captain-jumpsuit = Captain jumpsuit
+loadout-group-captain-neck = Captain neck
+loadout-group-captain-backpack = Captain backpack
+
+loadout-group-hop-head = Head of Personnel head
+loadout-group-hop-jumpsuit = Head of Personnel jumpsuit
+loadout-group-hop-neck = Head of Personnel neck
+loadout-group-hop-backpack = Head of Personnel backpack
+
+
+# Civilian
+loadout-group-passenger-jumpsuit = Passenger jumpsuit
+loadout-group-passenger-mask = Passenger mask
+loadout-group-passenger-gloves = Passenger gloves
+loadout-group-passenger-backpack = Passenger backpack
+
+loadout-group-bartender-head = Bartender head
+loadout-group-bartender-jumpsuit = Bartender jumpsuit
+loadout-group-bartender-outerclothing = Bartender outer clothing
+
+loadout-group-chef-head = Chef head
+loadout-group-chef-mask = Chef mask
+loadout-group-chef-jumpsuit = Chef jumpsuit
+loadout-group-chef-outerclothing = Chef outer clothing
+
+loadout-group-librarian-jumpsuit = Librarian jumpsuit
+
+loadout-group-lawyer-jumpsuit = Lawyer jumpsuit
+loadout-group-lawyer-neck = Lawyer neck
+
+loadout-group-chaplain-head = Chaplain head
+loadout-group-chaplain-mask = Chaplain mask
+loadout-group-chaplain-jumpsuit = Chaplain jumpsuit
+loadout-group-chaplain-backpack = Chaplain backpack
+loadout-group-chaplain-outerclothing = Chaplain outer clothing
+loadout-group-chaplain-neck = Chaplain neck
+
+loadout-group-janitor-head = Janitor head
+loadout-group-janitor-jumpsuit = Janitor jumpsuit
+
+loadout-group-botanist-head = Botanist head
+loadout-group-botanist-jumpsuit = Botanist jumpsuit
+loadout-group-botanist-backpack = Botanist backpack
+loadout-group-botanist-outerclothing = Botanist outer clothing
+
+loadout-group-clown-head = Clown head
+loadout-group-clown-jumpsuit = Clown jumpsuit
+loadout-group-clown-backpack = Clown backpack
+loadout-group-clown-shoes = Clown shoes
+
+loadout-group-mime-head = Mime head
+loadout-group-mime-mask = Mime mask
+loadout-group-mime-jumpsuit = Mime jumpsuit
+loadout-group-mime-backpack = Mime backpack
+
+loadout-group-musician-backpack = Musician backpack
+
+# Cargo
+loadout-group-quartermaster-head = Quartermaster head
+loadout-group-quartermaster-jumpsuit = Quartermaster jumpsuit
+loadout-group-quartermaster-backpack = Quartermaster backpack
+loadout-group-quartermaster-neck = Quartermaster neck
+
+loadout-group-cargo-technician-head = Technician head
+loadout-group-cargo-technician-jumpsuit = Technician jumpsuit
+loadout-group-cargo-technician-backpack = Technician backpack
+
+loadout-group-salvage-specialist-backpack = Salvage Specialist backpack
+
+# Engineering
+loadout-group-chief-engineer-head = Chief Engineer head
+loadout-group-chief-engineer-jumpsuit = Chief Engineer jumpsuit
+loadout-group-chief-engineer-backpack = Chief Engineer backpack
+loadout-group-chief-engineer-neck = Chief Engineer neck
+
+loadout-group-technical-assistant-jumpsuit = Technical Assistant jumpsuit
+
+loadout-group-station-engineer-head = Station Engineer head
+loadout-group-station-engineer-jumpsuit = Station Engineer jumpsuit
+loadout-group-station-engineer-backpack = Station Engineer backpack
+loadout-group-station-engineer-outerclothing = Station Engineer outer clothing
+loadout-group-station-engineer-id = Station Engineer ID
+
+loadout-group-atmospheric-technician-jumpsuit = Atmospheric Technician jumpsuit
+loadout-group-atmospheric-technician-backpack = Atmospheric Technician backpack
+
+# Science
+loadout-group-research-director-head = Research Director head
+loadout-group-research-director-neck = Research Director neck
+loadout-group-research-director-jumpsuit = Research Director jumpsuit
+loadout-group-research-director-backpack = Research Director backpack
+loadout-group-research-director-outerclothing = Research Director outer clothing
+
+loadout-group-scientist-head = Scientist head
+loadout-group-scientist-neck = Scientist neck
+loadout-group-scientist-jumpsuit = Scientist jumpsuit
+loadout-group-scientist-backpack = Scientist backpack
+loadout-group-scientist-outerclothing = Scientist outer clothing
+loadout-group-scientist-id = Scientist ID
+
+loadout-group-research-assistant-jumpsuit = Research Assistant jumpsuit
+
+# Security
+loadout-group-head-of-security-head = Head of Security head
+loadout-group-head-of-security-jumpsuit = Head of Security jumpsuit
+loadout-group-head-of-security-neck = Head of Security neck
+loadout-group-head-of-security-outerclothing = Head of Security outer clothing
+
+loadout-group-warden-head = Warden head
+loadout-group-warden-jumpsuit = Warden jumpsuit
+loadout-group-warden-outerclothing = Warden outer clothing
+
+loadout-group-security-head = Security head
+loadout-group-security-jumpsuit = Security jumpsuit
+loadout-group-security-backpack = Security backpack
+loadout-group-security-id = Security ID
+
+loadout-group-detective-head = Detective head
+loadout-group-detective-neck = Detective neck
+loadout-group-detective-jumpsuit = Detective jumpsuit
+loadout-group-detective-backpack = Detective backpack
+loadout-group-detective-outerclothing = Detective outer clothing
+
+loadout-group-security-cadet-jumpsuit = Security cadet jumpsuit
+
+# Medical
+loadout-group-chief-medical-officer-head = Chief Medical Officer head
+loadout-group-chief-medical-officer-jumpsuit = Chief Medical Officer jumpsuit
+loadout-group-chief-medical-officer-outerclothing = Chief Medical Officer outer clothing
+loadout-group-chief-medical-officer-backpack = Chief Medical Officer backpack
+loadout-group-chief-medical-officer-neck = Chief Medical Officer neck
+
+loadout-group-medical-doctor-head = Medical Doctor head
+loadout-group-medical-doctor-jumpsuit = Medical Doctor jumpsuit
+loadout-group-medical-doctor-outerclothing = Medical Doctor outer clothing
+loadout-group-medical-doctor-backpack = Medical Doctor backpack
+loadout-group-medical-doctor-id = Medical Doctor ID
+
+loadout-group-medical-intern-jumpsuit = Medical intern jumpsuit
+
+loadout-group-chemist-jumpsuit = Chemist jumpsuit
+loadout-group-chemist-outerclothing = Chemist outer clothing
+loadout-group-chemist-backpack = Chemist backpack
+
+loadout-group-paramedic-head = Paramedic head
+loadout-group-paramedic-jumpsuit = Paramedic jumpsuit
+loadout-group-paramedic-outerclothing = Paramedic outer clothing
+loadout-group-paramedic-backpack = Paramedic backpack
+
+# Wildcards
+loadout-group-reporter-jumpsuit = Reporter jumpsuit
+
+loadout-group-boxer-jumpsuit = Boxer jumpsuit
+loadout-group-boxer-gloves = Boxer gloves
diff --git a/Resources/Locale/en-US/preferences/loadouts.ftl b/Resources/Locale/en-US/preferences/loadouts.ftl
new file mode 100644 (file)
index 0000000..b6953c7
--- /dev/null
@@ -0,0 +1,7 @@
+# Restrictions
+loadout-restrictions = Restrictions
+loadouts-min-limit = Min count: {$count}
+loadouts-max-limit = Max count: {$count}
+loadouts-points-limit = Points: {$count} / {$max}
+
+loadouts-points-restriction = Insufficient points
index b1a695dbedc170e2e8f702c7915763b7db3df5a1..139d222f797fd8894bffc34de8b3677d63983246 100644 (file)
@@ -19,8 +19,6 @@ 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-clothing-label = Clothing:
-humanoid-profile-editor-backpack-label = Backpack:
 humanoid-profile-editor-spawn-priority-label = Spawn priority:
 humanoid-profile-editor-eyes-label = Eye color:
 humanoid-profile-editor-jobs-tab = Jobs
index bbea09e9210ee22ffc2d972139cfb31b39208bb6..71c679a2aa5227a3ea79d419cf3f18995e792def 100644 (file)
       - id: Flash
       #- id: TelescopicBaton
 
+- type: entity
+  noSpawn: true
+  parent: ClothingBackpackIan
+  id: ClothingBackpackHOPIanFilled
+  components:
+  - type: StorageFill
+    contents:
+      - id: BoxSurvival
+      - id: Flash
+      #- id: TelescopicBaton
+
 - type: entity
   noSpawn: true
   parent: ClothingBackpackMedical
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/cargo_technician.yml
new file mode 100644 (file)
index 0000000..147a53e
--- /dev/null
@@ -0,0 +1,56 @@
+# Head
+- type: loadout
+  id: CargoTechnicianHead
+  equipment: CargoTechnicianHead
+
+- type: startingGear
+  id: CargoTechnicianHead
+  equipment:
+    head: ClothingHeadHatCargosoft
+
+# Jumpsuit
+- type: loadout
+  id: CargoTechnicianJumpsuit
+  equipment: CargoTechnicianJumpsuit
+
+- type: startingGear
+  id: CargoTechnicianJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitCargo
+
+- type: loadout
+  id: CargoTechnicianJumpskirt
+  equipment: CargoTechnicianJumpskirt
+
+- type: startingGear
+  id: CargoTechnicianJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtCargo
+
+# Back
+- type: loadout
+  id: CargoTechnicianBackpack
+  equipment: CargoTechnicianBackpack
+
+- type: startingGear
+  id: CargoTechnicianBackpack
+  equipment:
+    back: ClothingBackpackCargoFilled
+
+- type: loadout
+  id: CargoTechnicianSatchel
+  equipment: CargoTechnicianSatchel
+
+- type: startingGear
+  id: CargoTechnicianSatchel
+  equipment:
+    back: ClothingBackpackSatchelCargoFilled
+
+- type: loadout
+  id: CargoTechnicianDuffel
+  equipment: CargoTechnicianDuffel
+
+- type: startingGear
+  id: CargoTechnicianDuffel
+  equipment:
+    back: ClothingBackpackDuffelCargoFilled
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/quartermaster.yml
new file mode 100644 (file)
index 0000000..64971d9
--- /dev/null
@@ -0,0 +1,111 @@
+# Jumpsuit
+- type: loadout
+  id: QuartermasterJumpsuit
+  equipment: QuartermasterJumpsuit
+
+- type: startingGear
+  id: QuartermasterJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitQM
+
+- type: loadout
+  id: QuartermasterJumpskirt
+  equipment: QuartermasterJumpskirt
+
+- type: startingGear
+  id: QuartermasterJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtQM
+
+- type: loadout
+  id: QuartermasterTurtleneck
+  equipment: QuartermasterTurtleneck
+
+- type: startingGear
+  id: QuartermasterTurtleneck
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitQMTurtleneck
+
+- type: loadout
+  id: QuartermasterTurtleneckSkirt
+  equipment: QuartermasterTurtleneckSkirt
+
+- type: startingGear
+  id: QuartermasterTurtleneckSkirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtQMTurtleneck
+
+- type: loadout
+  id: QuartermasterFormalSuit
+  equipment: QuartermasterFormalSuit
+
+- type: startingGear
+  id: QuartermasterFormalSuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitQMFormal
+
+# Head
+- type: loadout
+  id: QuartermasterHead
+  equipment: QuartermasterHead
+
+- type: startingGear
+  id: QuartermasterHead
+  equipment:
+    head: ClothingHeadHatQMsoft
+
+- type: loadout
+  id: QuartermasterBeret
+  equipment: QuartermasterBeret
+
+- type: startingGear
+  id: QuartermasterBeret
+  equipment:
+    head: ClothingHeadHatBeretQM
+
+# Neck
+- type: loadout
+  id: QuartermasterCloak
+  equipment: QuartermasterCloak
+
+- type: startingGear
+  id: QuartermasterCloak
+  equipment:
+    neck: ClothingNeckCloakQm
+
+- type: loadout
+  id: QuartermasterMantle
+  equipment: QuartermasterMantle
+
+- type: startingGear
+  id: QuartermasterMantle
+  equipment:
+    neck: ClothingNeckMantleQM
+
+# Back
+- type: loadout
+  id: QuartermasterBackpack
+  equipment: QuartermasterBackpack
+
+- type: startingGear
+  id: QuartermasterBackpack
+  equipment:
+    back: ClothingBackpackQuartermasterFilled
+
+- type: loadout
+  id: QuartermasterSatchel
+  equipment: QuartermasterSatchel
+
+- type: startingGear
+  id: QuartermasterSatchel
+  equipment:
+    back: ClothingBackpackSatchelQuartermasterFilled
+
+- type: loadout
+  id: QuartermasterDuffel
+  equipment: QuartermasterDuffel
+
+- type: startingGear
+  id: QuartermasterDuffel
+  equipment:
+    back: ClothingBackpackDuffelQuartermasterFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Cargo/salvage_specialist.yml b/Resources/Prototypes/Loadouts/Jobs/Cargo/salvage_specialist.yml
new file mode 100644 (file)
index 0000000..e31393f
--- /dev/null
@@ -0,0 +1,27 @@
+# Back
+- type: loadout
+  id: SalvageSpecialistBackpack
+  equipment: SalvageSpecialistBackpack
+
+- type: startingGear
+  id: SalvageSpecialistBackpack
+  equipment:
+    back: ClothingBackpackSalvageFilled
+
+- type: loadout
+  id: SalvageSpecialistSatchel
+  equipment: SalvageSpecialistSatchel
+
+- type: startingGear
+  id: SalvageSpecialistSatchel
+  equipment:
+    back: ClothingBackpackSatchelSalvageFilled
+
+- type: loadout
+  id: SalvageSpecialistDuffel
+  equipment: SalvageSpecialistDuffel
+
+- type: startingGear
+  id: SalvageSpecialistDuffel
+  equipment:
+    back: ClothingBackpackDuffelSalvageFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/bartender.yml
new file mode 100644 (file)
index 0000000..a711df3
--- /dev/null
@@ -0,0 +1,65 @@
+# Head
+- type: loadout
+  id: BartenderHead
+  equipment: BartenderHead
+
+- type: startingGear
+  id: BartenderHead
+  equipment:
+    head: ClothingHeadHatTophat
+
+- type: loadout
+  id: BartenderBowler
+  equipment: BartenderBowler
+
+- type: startingGear
+  id: BartenderBowler
+  equipment:
+    head: ClothingHeadHatBowlerHat
+
+# Jumpsuit
+- type: loadout
+  id: BartenderJumpsuit
+  equipment: BartenderJumpsuit
+
+- type: startingGear
+  id: BartenderJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitBartender
+
+- type: loadout
+  id: BartenderJumpskirt
+  equipment: BartenderJumpskirt
+
+- type: startingGear
+  id: BartenderJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtBartender
+
+- type: loadout
+  id: BartenderJumpsuitPurple
+  equipment: BartenderJumpsuitPurple
+
+- type: startingGear
+  id: BartenderJumpsuitPurple
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitBartenderPurple
+
+# Outer clothing
+- type: loadout
+  id: BartenderApron
+  equipment: BartenderApron
+
+- type: startingGear
+  id: BartenderApron
+  equipment:
+    outerClothing: ClothingOuterApronBar
+
+- type: loadout
+  id: BartenderVest
+  equipment: BartenderVest
+
+- type: startingGear
+  id: BartenderVest
+  equipment:
+    outerClothing: ClothingOuterVest
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/botanist.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/botanist.yml
new file mode 100644 (file)
index 0000000..6202fa4
--- /dev/null
@@ -0,0 +1,84 @@
+# Head
+- type: loadout
+  id: BotanistHead
+  equipment: BotanistHead
+
+- type: startingGear
+  id: BotanistHead
+  equipment:
+    head: ClothingHeadHatTrucker
+
+- type: loadout
+  id: BotanistBandana
+  equipment: BotanistBandana
+
+- type: startingGear
+  id: BotanistBandana
+  equipment:
+    head: ClothingHeadBandBotany
+
+# Jumpsuit
+- type: loadout
+  id: BotanistJumpsuit
+  equipment: BotanistJumpsuit
+
+- type: startingGear
+  id: BotanistJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitHydroponics
+
+- type: loadout
+  id: BotanistJumpskirt
+  equipment: BotanistJumpskirt
+
+- type: startingGear
+  id: BotanistJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtHydroponics
+
+- type: loadout
+  id: BotanistOveralls
+  equipment: BotanistOveralls
+
+- type: startingGear
+  id: BotanistOveralls
+  equipment:
+    jumpsuit: ClothingUniformOveralls
+
+# Back
+- type: loadout
+  id: BotanistBackpack
+  equipment: BotanistBackpack
+
+- type: startingGear
+  id: BotanistBackpack
+  equipment:
+    back: ClothingBackpackHydroponicsFilled
+
+- type: loadout
+  id: BotanistSatchel
+  equipment: BotanistSatchel
+
+- type: startingGear
+  id: BotanistSatchel
+  equipment:
+    back: ClothingBackpackSatchelHydroponicsFilled
+
+- type: loadout
+  id: BotanistDuffel
+  equipment: BotanistDuffel
+
+- type: startingGear
+  id: BotanistDuffel
+  equipment:
+    back: ClothingBackpackDuffelHydroponicsFilled
+
+# Outer clothing
+- type: loadout
+  id: BotanistApron
+  equipment: BotanistApron
+
+- type: startingGear
+  id: BotanistApron
+  equipment:
+    outerClothing: ClothingOuterApronBotanist
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/chaplain.yml
new file mode 100644 (file)
index 0000000..51aa61a
--- /dev/null
@@ -0,0 +1,158 @@
+# Head
+- type: loadout
+  id: ChaplainHead
+  equipment: ChaplainHead
+
+- type: startingGear
+  id: ChaplainHead
+  equipment:
+    head: ClothingHeadHatFez
+
+- type: loadout
+  id: ChaplainPlagueHat
+  equipment: ChaplainPlagueHat
+
+- type: startingGear
+  id: ChaplainPlagueHat
+  equipment:
+    head: ClothingHeadHatPlaguedoctor
+
+- type: loadout
+  id: ChaplainWitchHat
+  equipment: ChaplainWitchHat
+
+- type: startingGear
+  id: ChaplainWitchHat
+  equipment:
+    head: ClothingHeadHatWitch
+
+- type: loadout
+  id: ChaplainWitchHatAlt
+  equipment: ChaplainWitchHatAlt
+
+- type: startingGear
+  id: ChaplainWitchHatAlt
+  equipment:
+    head: ClothingHeadHatWitch1
+
+# Mask
+- type: loadout
+  id: ChaplainMask
+  equipment: ChaplainMask
+
+- type: startingGear
+  id: ChaplainMask
+  equipment:
+    mask: ClothingMaskPlague
+
+# Jumpsuit
+- type: loadout
+  id: ChaplainJumpsuit
+  equipment: ChaplainJumpsuit
+
+- type: startingGear
+  id: ChaplainJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitChaplain
+
+- type: loadout
+  id: ChaplainJumpskirt
+  equipment: ChaplainJumpskirt
+
+- type: startingGear
+  id: ChaplainJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtChaplain
+
+- type: loadout
+  id: ChaplainRobesDark
+  equipment: ChaplainRobesDark
+
+- type: startingGear
+  id: ChaplainRobesDark
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitMonasticRobeDark
+
+- type: loadout
+  id: ChaplainRobesLight
+  equipment: ChaplainRobesLight
+
+- type: startingGear
+  id: ChaplainRobesLight
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitMonasticRobeLight
+
+# Back
+- type: loadout
+  id: ChaplainBackpack
+  equipment: ChaplainBackpack
+
+- type: startingGear
+  id: ChaplainBackpack
+  equipment:
+    back: ClothingBackpackChaplainFilled
+
+- type: loadout
+  id: ChaplainSatchel
+  equipment: ChaplainSatchel
+
+- type: startingGear
+  id: ChaplainSatchel
+  equipment:
+    back: ClothingBackpackSatchelChaplainFilled
+
+- type: loadout
+  id: ChaplainDuffel
+  equipment: ChaplainDuffel
+
+- type: startingGear
+  id: ChaplainDuffel
+  equipment:
+    back: ClothingBackpackDuffelChaplainFilled
+
+# Neck
+- type: loadout
+  id: ChaplainNeck
+  equipment: ChaplainNeck
+
+- type: startingGear
+  id: ChaplainNeck
+  equipment:
+    neck: ClothingNeckStoleChaplain
+
+# Outer clothing
+- type: loadout
+  id: ChaplainPlagueSuit
+  equipment: ChaplainPlagueSuit
+
+- type: startingGear
+  id: ChaplainPlagueSuit
+  equipment:
+    outerClothing: ClothingOuterPlagueSuit
+
+- type: loadout
+  id: ChaplainNunRobe
+  equipment: ChaplainNunRobe
+
+- type: startingGear
+  id: ChaplainNunRobe
+  equipment:
+    outerClothing: ClothingOuterNunRobe
+
+- type: loadout
+  id: ChaplainBlackHoodie
+  equipment: ChaplainBlackHoodie
+
+- type: startingGear
+  id: ChaplainBlackHoodie
+  equipment:
+    outerClothing: ClothingOuterHoodieBlack
+
+- type: loadout
+  id: ChaplainHoodie
+  equipment: ChaplainHoodie
+
+- type: startingGear
+  id: ChaplainHoodie
+  equipment:
+    outerClothing: ClothingOuterHoodieChaplain
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/chef.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/chef.yml
new file mode 100644 (file)
index 0000000..1a2b756
--- /dev/null
@@ -0,0 +1,57 @@
+# Head
+- type: loadout
+  id: ChefHead
+  equipment: ChefHead
+
+- type: startingGear
+  id: ChefHead
+  equipment:
+    head: ClothingHeadHatChef
+
+# Mask
+- type: loadout
+  id: ChefMask
+  equipment: ChefMask
+
+- type: startingGear
+  id: ChefMask
+  equipment:
+    mask: ClothingMaskItalianMoustache
+
+# Jumpsuit
+- type: loadout
+  id: ChefJumpsuit
+  equipment: ChefJumpsuit
+
+- type: startingGear
+  id: ChefJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitChef
+
+- type: loadout
+  id: ChefJumpskirt
+  equipment: ChefJumpskirt
+
+- type: startingGear
+  id: ChefJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtChef
+
+# Outer clothing
+- type: loadout
+  id: ChefApron
+  equipment: ChefApron
+
+- type: startingGear
+  id: ChefApron
+  equipment:
+    outerClothing: ClothingOuterApronChef
+
+- type: loadout
+  id: ChefJacket
+  equipment: ChefJacket
+
+- type: startingGear
+  id: ChefJacket
+  equipment:
+    outerClothing: ClothingOuterJacketChef
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/clown.yml
new file mode 100644 (file)
index 0000000..896d1a0
--- /dev/null
@@ -0,0 +1,75 @@
+# Head
+- type: loadout
+  id: JesterHat
+  equipment: JesterHat
+
+- type: startingGear
+  id: JesterHat
+  equipment:
+    head: ClothingHeadHatJesterAlt
+
+# Jumpsuit
+- type: loadout
+  id: ClownSuit
+  equipment: ClownSuit
+
+- type: startingGear
+  id: ClownSuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitClown
+
+- type: loadout
+  id: JesterSuit
+  equipment: JesterSuit
+
+- type: startingGear
+  id: JesterSuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitJesterAlt
+
+# Back
+- type: loadout
+  id: ClownBackpack
+  equipment: ClownBackpack
+
+- type: startingGear
+  id: ClownBackpack
+  equipment:
+    back: ClothingBackpackClownFilled
+
+- type: loadout
+  id: ClownSatchel
+  equipment: ClownSatchel
+
+- type: startingGear
+  id: ClownSatchel
+  equipment:
+    back: ClothingBackpackSatchelClownFilled
+
+- type: loadout
+  id: ClownDuffel
+  equipment: ClownDuffel
+
+- type: startingGear
+  id: ClownDuffel
+  equipment:
+    back: ClothingBackpackDuffelClownFilled
+
+# Shoes
+- type: loadout
+  id: ClownShoes
+  equipment: ClownShoes
+
+- type: startingGear
+  id: ClownShoes
+  equipment:
+    shoes: ClothingShoesClown
+
+- type: loadout
+  id: JesterShoes
+  equipment: JesterShoes
+
+- type: startingGear
+  id: JesterShoes
+  equipment:
+    shoes: ClothingShoesJester
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/janitor.yml
new file mode 100644 (file)
index 0000000..910cb49
--- /dev/null
@@ -0,0 +1,28 @@
+# Head
+- type: loadout
+  id: JanitorHead
+  equipment: JanitorHead
+
+- type: startingGear
+  id: JanitorHead
+  equipment:
+    head: ClothingHeadHatPurplesoft
+
+# Jumpsuit
+- type: loadout
+  id: JanitorJumpsuit
+  equipment: JanitorJumpsuit
+
+- type: startingGear
+  id: JanitorJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitJanitor
+
+- type: loadout
+  id: JanitorJumpskirt
+  equipment: JanitorJumpskirt
+
+- type: startingGear
+  id: JanitorJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtJanitor
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/lawyer.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/lawyer.yml
new file mode 100644 (file)
index 0000000..00be124
--- /dev/null
@@ -0,0 +1,100 @@
+# Jumpsuit
+- type: loadout
+  id: LawyerJumpsuit
+  equipment: LawyerJumpsuit
+
+- type: startingGear
+  id: LawyerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLawyerBlack
+
+- type: loadout
+  id: LawyerJumpskirt
+  equipment: LawyerJumpskirt
+
+- type: startingGear
+  id: LawyerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLawyerBlack
+
+- type: loadout
+  id: LawyerJumpsuitBlue
+  equipment: LawyerJumpsuitBlue
+
+- type: startingGear
+  id: LawyerJumpsuitBlue
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLawyerBlue
+
+- type: loadout
+  id: LawyerJumpskirtBlue
+  equipment: LawyerJumpskirtBlue
+
+- type: startingGear
+  id: LawyerJumpskirtBlue
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLawyerBlue
+
+- type: loadout
+  id: LawyerJumpsuitPurple
+  equipment: LawyerJumpsuitPurple
+
+- type: startingGear
+  id: LawyerJumpsuitPurple
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLawyerPurple
+
+- type: loadout
+  id: LawyerJumpskirtPurple
+  equipment: LawyerJumpskirtPurple
+
+- type: startingGear
+  id: LawyerJumpskirtPurple
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLawyerPurple
+
+- type: loadout
+  id: LawyerJumpsuitRed
+  equipment: LawyerJumpsuitRed
+
+- type: startingGear
+  id: LawyerJumpsuitRed
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLawyerRed
+
+- type: loadout
+  id: LawyerJumpskirtRed
+  equipment: LawyerJumpskirtRed
+
+- type: startingGear
+  id: LawyerJumpskirtRed
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLawyerRed
+
+- type: loadout
+  id: LawyerJumpsuitGood
+  equipment: LawyerJumpsuitGood
+
+- type: startingGear
+  id: LawyerJumpsuitGood
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLawyerGood
+
+- type: loadout
+  id: LawyerJumpskirtGood
+  equipment: LawyerJumpskirtGood
+
+- type: startingGear
+  id: LawyerJumpskirtGood
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLawyerGood
+
+# Neck
+- type: loadout
+  id: LawyerNeck
+  equipment: LawyerNeck
+
+- type: startingGear
+  id: LawyerNeck
+  equipment:
+    neck: ClothingNeckLawyerbadge
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/librarian.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/librarian.yml
new file mode 100644 (file)
index 0000000..f5d92b8
--- /dev/null
@@ -0,0 +1,36 @@
+# Jumpsuit
+- type: loadout
+  id: LibrarianJumpsuit
+  equipment: LibrarianJumpsuit
+
+- type: startingGear
+  id: LibrarianJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitLibrarian
+
+- type: loadout
+  id: LibrarianJumpskirt
+  equipment: LibrarianJumpskirt
+
+- type: startingGear
+  id: LibrarianJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtLibrarian
+
+- type: loadout
+  id: CuratorJumpsuit
+  equipment: CuratorJumpsuit
+
+- type: startingGear
+  id: CuratorJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitCurator
+
+- type: loadout
+  id: CuratorJumpskirt
+  equipment: CuratorJumpskirt
+
+- type: startingGear
+  id: CuratorJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtCurator
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/mime.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/mime.yml
new file mode 100644 (file)
index 0000000..d78e309
--- /dev/null
@@ -0,0 +1,102 @@
+# Head
+- type: loadout
+  id: MimeHead
+  equipment: MimeHead
+
+- type: startingGear
+  id: MimeHead
+  equipment:
+    head: ClothingHeadHatBeret
+
+- type: loadout
+  id: MimeFrenchBeret
+  equipment: MimeFrenchBeret
+
+- type: startingGear
+  id: MimeFrenchBeret
+  equipment:
+    head: ClothingHeadHatBeretFrench
+
+- type: loadout
+  id: MimeCap
+  equipment: MimeCap
+
+- type: startingGear
+  id: MimeCap
+  equipment:
+    head: ClothingHeadHatMimesoft
+
+# Mask
+- type: loadout
+  id: MimeMask
+  equipment: MimeMask
+
+- type: startingGear
+  id: MimeMask
+  equipment:
+    mask: ClothingMaskMime
+
+- type: loadout
+  id: MimeMaskSad
+  equipment: MimeMaskSad
+
+- type: startingGear
+  id: MimeMaskSad
+  equipment:
+    mask: ClothingMaskSadMime
+
+- type: loadout
+  id: MimeMaskScared
+  equipment: MimeMaskScared
+
+- type: startingGear
+  id: MimeMaskScared
+  equipment:
+    mask: ClothingMaskScaredMime
+
+# Jumpsuit
+- type: loadout
+  id: MimeJumpsuit
+  equipment: MimeJumpsuit
+
+- type: startingGear
+  id: MimeJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitMime
+
+- type: loadout
+  id: MimeJumpskirt
+  equipment: MimeJumpskirt
+
+- type: startingGear
+  id: MimeJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtMime
+
+# Back
+- type: loadout
+  id: MimeBackpack
+  equipment: MimeBackpack
+
+- type: startingGear
+  id: MimeBackpack
+  equipment:
+    back: ClothingBackpackMimeFilled
+
+- type: loadout
+  id: MimeSatchel
+  equipment: MimeSatchel
+
+- type: startingGear
+  id: MimeSatchel
+  equipment:
+    back: ClothingBackpackSatchelMimeFilled
+
+- type: loadout
+  id: MimeDuffel
+  equipment: MimeDuffel
+
+- type: startingGear
+  id: MimeDuffel
+  equipment:
+    back: ClothingBackpackDuffelMimeFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/musician.yml
new file mode 100644 (file)
index 0000000..484eb7b
--- /dev/null
@@ -0,0 +1,27 @@
+# Back
+- type: loadout
+  id: MusicianBackpack
+  equipment: MusicianBackpack
+
+- type: startingGear
+  id: MusicianBackpack
+  equipment:
+    back: ClothingBackpackMusicianFilled
+
+- type: loadout
+  id: MusicianSatchel
+  equipment: MusicianSatchel
+
+- type: startingGear
+  id: MusicianSatchel
+  equipment:
+    back: ClothingBackpackSatchelMusicianFilled
+
+- type: loadout
+  id: MusicianDuffel
+  equipment: MusicianDuffel
+
+- type: startingGear
+  id: MusicianDuffel
+  equipment:
+    back: ClothingBackpackDuffelMusicianFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml b/Resources/Prototypes/Loadouts/Jobs/Civilian/passenger.yml
new file mode 100644 (file)
index 0000000..65bfc1c
--- /dev/null
@@ -0,0 +1,82 @@
+# Greytide Time
+- type: loadoutEffectGroup
+  id: GreyTider
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobPassenger
+      time: 36000 #10 hrs, silly reward for people who play passenger a lot
+
+# Face
+- type: loadout
+  id: PassengerFace
+  equipment: GasMask
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: GreyTider
+
+- type: startingGear
+  id: GasMask
+  equipment:
+    mask: ClothingMaskGas
+
+# Jumpsuit
+- type: loadout
+  id: GreyJumpsuit
+  equipment: GreyJumpsuit
+
+- type: startingGear
+  id: GreyJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitColorGrey
+
+- type: loadout
+  id: GreyJumpskirt
+  equipment: GreyJumpskirt
+
+- type: startingGear
+  id: GreyJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtColorGrey
+
+# Back
+- type: loadout
+  id: CommonBackpack
+  equipment: CommonBackpack
+
+- type: startingGear
+  id: CommonBackpack
+  equipment:
+    back: ClothingBackpackFilled
+
+- type: loadout
+  id: CommonSatchel
+  equipment: CommonSatchel
+
+- type: startingGear
+  id: CommonSatchel
+  equipment:
+    back: ClothingBackpackSatchelFilled
+
+- type: loadout
+  id: CommonDuffel
+  equipment: CommonDuffel
+
+- type: startingGear
+  id: CommonDuffel
+  equipment:
+    back: ClothingBackpackDuffelFilled
+
+# Gloves
+- type: loadout
+  id: PassengerGloves
+  equipment: FingerlessInsulatedGloves
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: GreyTider
+
+- type: startingGear
+  id: FingerlessInsulatedGloves
+  equipment:
+    gloves: ClothingHandsGlovesFingerlessInsulated
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml b/Resources/Prototypes/Loadouts/Jobs/Command/captain.yml
new file mode 100644 (file)
index 0000000..e1b9abf
--- /dev/null
@@ -0,0 +1,111 @@
+# Jumpsuit
+- type: loadout
+  id: CaptainJumpsuit
+  equipment: CaptainJumpsuit
+
+- type: startingGear
+  id: CaptainJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitCaptain
+
+- type: loadout
+  id: CaptainJumpskirt
+  equipment: CaptainJumpskirt
+
+- type: startingGear
+  id: CaptainJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtCaptain
+
+- type: loadout
+  id: CaptainFormalSuit
+  equipment: CaptainFormalSuit
+
+- type: startingGear
+  id: CaptainFormalSuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitCapFormal
+
+- type: loadout
+  id: CaptainFormalSkirt
+  equipment: CaptainFormalSkirt
+
+- type: startingGear
+  id: CaptainFormalSkirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtCapFormalDress
+
+# Head
+- type: loadout
+  id: CaptainHead
+  equipment: CaptainHead
+
+- type: startingGear
+  id: CaptainHead
+  equipment:
+    head: ClothingHeadHatCaptain
+
+- type: loadout
+  id: CaptainCap
+  equipment: CaptainCap
+
+- type: startingGear
+  id: CaptainCap
+  equipment:
+    head: ClothingHeadHatCapcap
+
+# Neck
+- type: loadout
+  id: CaptainCloak
+  equipment: CaptainCloak
+
+- type: startingGear
+  id: CaptainCloak
+  equipment:
+    neck: ClothingNeckCloakCap
+  
+- type: loadout
+  id: CaptainCloakFormal
+  equipment: CaptainCloakFormal
+
+- type: startingGear
+  id: CaptainCloakFormal
+  equipment:
+    neck: ClothingNeckCloakCapFormal
+
+- type: loadout
+  id: CaptainMantle
+  equipment: CaptainMantle
+
+- type: startingGear
+  id: CaptainMantle
+  equipment:
+    neck: ClothingNeckMantleCap
+
+# Back
+- type: loadout
+  id: CaptainBackpack
+  equipment: CaptainBackpack
+
+- type: startingGear
+  id: CaptainBackpack
+  equipment:
+    back: ClothingBackpackCaptainFilled
+
+- type: loadout
+  id: CaptainSatchel
+  equipment: CaptainSatchel
+
+- type: startingGear
+  id: CaptainSatchel
+  equipment:
+    back: ClothingBackpackSatchelCaptainFilled
+
+- type: loadout
+  id: CaptainDuffel
+  equipment: CaptainDuffel
+
+- type: startingGear
+  id: CaptainDuffel
+  equipment:
+    back: ClothingBackpackDuffelCaptainFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml b/Resources/Prototypes/Loadouts/Jobs/Command/head_of_personnel.yml
new file mode 100644 (file)
index 0000000..26ca685
--- /dev/null
@@ -0,0 +1,97 @@
+# Professional HoP Time
+- type: loadoutEffectGroup
+  id: ProfessionalHoP
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobHeadOfPersonnel
+      time: 54000 #15 hrs, special reward for HoP mains
+
+# Jumpsuit
+- type: loadout
+  id: HoPJumpsuit
+  equipment: HoPJumpsuit
+
+- type: startingGear
+  id: HoPJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitHoP
+
+- type: loadout
+  id: HoPJumpskirt
+  equipment: HoPJumpskirt
+
+- type: startingGear
+  id: HoPJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtHoP
+
+# Head
+- type: loadout
+  id: HoPHead
+  equipment: HoPHead
+
+- type: startingGear
+  id: HoPHead
+  equipment:
+    head: ClothingHeadHatHopcap
+
+# Neck
+- type: loadout
+  id: HoPCloak
+  equipment: HoPCloak
+
+- type: startingGear
+  id: HoPCloak
+  equipment:
+    neck: ClothingNeckCloakHop
+
+- type: loadout
+  id: HoPMantle
+  equipment: HoPMantle
+
+- type: startingGear
+  id: HoPMantle
+  equipment:
+    neck: ClothingNeckMantleHOP
+
+# Back
+- type: loadout
+  id: HoPBackpack
+  equipment: HoPBackpack
+
+- type: startingGear
+  id: HoPBackpack
+  equipment:
+    back: ClothingBackpackHOPFilled
+
+- type: loadout
+  id: HoPSatchel
+  equipment: HoPSatchel
+
+- type: startingGear
+  id: HoPSatchel
+  equipment:
+    back: ClothingBackpackSatchelHOPFilled
+
+- type: loadout
+  id: HoPDuffel
+  equipment: HoPDuffel
+
+- type: startingGear
+  id: HoPDuffel
+  equipment:
+    back: ClothingBackpackDuffelHOPFilled
+
+- type: loadout
+  id: HoPBackpackIan
+  equipment: HoPBackpackIan
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: ProfessionalHoP
+
+- type: startingGear
+  id: HoPBackpackIan
+  equipment:
+    back: ClothingBackpackHOPIanFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/atmospheric_technician.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/atmospheric_technician.yml
new file mode 100644 (file)
index 0000000..16a2ef8
--- /dev/null
@@ -0,0 +1,55 @@
+# Jumpsuit
+- type: loadout
+  id: AtmosphericTechnicianJumpsuit
+  equipment: AtmosphericTechnicianJumpsuit
+
+- type: startingGear
+  id: AtmosphericTechnicianJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitAtmos
+
+- type: loadout
+  id: AtmosphericTechnicianJumpskirt
+  equipment: AtmosphericTechnicianJumpskirt
+
+- type: startingGear
+  id: AtmosphericTechnicianJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtAtmos
+
+- type: loadout
+  id: AtmosphericTechnicianJumpsuitCasual
+  equipment: AtmosphericTechnicianJumpsuitCasual
+
+- type: startingGear
+  id: AtmosphericTechnicianJumpsuitCasual
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitAtmosCasual
+
+# Back
+- type: loadout
+  id: AtmosphericTechnicianBackpack
+  equipment: AtmosphericTechnicianBackpack
+
+- type: startingGear
+  id: AtmosphericTechnicianBackpack
+  equipment:
+    back: ClothingBackpackAtmosphericsFilled
+
+- type: loadout
+  id: AtmosphericTechnicianSatchel
+  equipment: AtmosphericTechnicianSatchel
+
+- type: startingGear
+  id: AtmosphericTechnicianSatchel
+  equipment:
+    back: ClothingBackpackSatchelEngineeringFilled
+
+- type: loadout
+  id: AtmosphericTechnicianDuffel
+  equipment: AtmosphericTechnicianDuffel
+
+- type: startingGear
+  id: AtmosphericTechnicianDuffel
+  equipment:
+    back: ClothingBackpackDuffelAtmosphericsFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/chief_engineer.yml
new file mode 100644 (file)
index 0000000..f498ab0
--- /dev/null
@@ -0,0 +1,97 @@
+# Jumpsuit
+- type: loadout
+  id: ChiefEngineerJumpsuit
+  equipment: ChiefEngineerJumpsuit
+
+- type: startingGear
+  id: ChiefEngineerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitChiefEngineer
+
+- type: loadout
+  id: ChiefEngineerJumpskirt
+  equipment: ChiefEngineerJumpskirt
+
+- type: startingGear
+  id: ChiefEngineerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtChiefEngineer
+
+- type: loadout
+  id: ChiefEngineerTurtleneck
+  equipment: ChiefEngineerTurtleneck
+
+- type: startingGear
+  id: ChiefEngineerTurtleneck
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitChiefEngineerTurtle
+
+- type: loadout
+  id: ChiefEngineerTurtleneckSkirt
+  equipment: ChiefEngineerTurtleneckSkirt
+
+- type: startingGear
+  id: ChiefEngineerTurtleneckSkirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtChiefEngineerTurtle
+
+# Head
+- type: loadout
+  id: ChiefEngineerHead
+  equipment: ChiefEngineerHead
+
+- type: startingGear
+  id: ChiefEngineerHead
+  equipment:
+    head: ClothingHeadHatHardhatWhite
+
+- type: loadout
+  id: ChiefEngineerBeret
+  equipment: EngineeringBeret
+
+# Neck
+- type: loadout
+  id: ChiefEngineerCloak
+  equipment: ChiefEngineerCloak
+
+- type: startingGear
+  id: ChiefEngineerCloak
+  equipment:
+    neck: ClothingNeckCloakCe
+
+- type: loadout
+  id: ChiefEngineerMantle
+  equipment: ChiefEngineerMantle
+
+- type: startingGear
+  id: ChiefEngineerMantle
+  equipment:
+    neck: ClothingNeckMantleCE
+
+# Back
+- type: loadout
+  id: ChiefEngineerBackpack
+  equipment: ChiefEngineerBackpack
+
+- type: startingGear
+  id: ChiefEngineerBackpack
+  equipment:
+    back: ClothingBackpackChiefEngineerFilled
+
+- type: loadout
+  id: ChiefEngineerSatchel
+  equipment: ChiefEngineerSatchel
+
+- type: startingGear
+  id: ChiefEngineerSatchel
+  equipment:
+    back: ClothingBackpackSatchelChiefEngineerFilled
+
+- type: loadout
+  id: ChiefEngineerDuffel
+  equipment: ChiefEngineerDuffel
+
+- type: startingGear
+  id: ChiefEngineerDuffel
+  equipment:
+    back: ClothingBackpackDuffelChiefEngineerFilled
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/station_engineer.yml
new file mode 100644 (file)
index 0000000..619a043
--- /dev/null
@@ -0,0 +1,189 @@
+# Senior times
+- type: loadoutEffectGroup
+  id: SeniorEngineering
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobAtmosphericTechnician
+      time: 21600 #6 hrs
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobStationEngineer
+      time: 21600 #6 hrs
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:DepartmentTimeRequirement
+      department: Engineering
+      time: 216000 # 60 hrs
+
+# Head
+- type: loadout
+  id: StationEngineerHead
+  equipment: StationEngineerHead
+
+- type: startingGear
+  id: StationEngineerHead
+  equipment:
+    head: ClothingHeadHatHardhatYellow
+
+- type: loadout
+  id: SeniorEngineerBeret
+  equipment: EngineeringBeret
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: EngineeringBeret
+  equipment:
+    head: ClothingHeadHatBeretEngineering
+
+# Jumpsuit
+- type: loadout
+  id: StationEngineerJumpsuit
+  equipment: StationEngineerJumpsuit
+
+- type: startingGear
+  id: StationEngineerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitEngineering
+
+- type: loadout
+  id: StationEngineerJumpskirt
+  equipment: StationEngineerJumpskirt
+
+- type: startingGear
+  id: StationEngineerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtEngineering
+
+- type: loadout
+  id: StationEngineerHazardsuit
+  equipment: StationEngineerHazardsuit
+
+- type: startingGear
+  id: StationEngineerHazardsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitEngineeringHazard
+
+- type: loadout
+  id: SeniorEngineerJumpsuit
+  equipment: SeniorEngineerJumpsuit
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitSeniorEngineer
+
+- type: loadout
+  id: SeniorEngineerJumpskirt
+  equipment: SeniorEngineerJumpskirt
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtSeniorEngineer
+
+# Back
+- type: loadout
+  id: StationEngineerBackpack
+  equipment: StationEngineerBackpack
+
+- type: startingGear
+  id: StationEngineerBackpack
+  equipment:
+    back: ClothingBackpackEngineeringFilled
+
+- type: loadout
+  id: StationEngineerSatchel
+  equipment: StationEngineerSatchel
+
+- type: startingGear
+  id: StationEngineerSatchel
+  equipment:
+    back: ClothingBackpackSatchelEngineeringFilled
+
+- type: loadout
+  id: StationEngineerDuffel
+  equipment: StationEngineerDuffel
+
+- type: startingGear
+  id: StationEngineerDuffel
+  equipment:
+    back: ClothingBackpackDuffelEngineeringFilled
+
+- type: loadout
+  id: SeniorEngineerBackpack
+  equipment: SeniorEngineerBackpack
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerBackpack
+  equipment:
+    back: ClothingBackpackEngineeringFilled
+
+- type: loadout
+  id: SeniorEngineerSatchel
+  equipment: SeniorEngineerSatchel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerSatchel
+  equipment:
+    back: ClothingBackpackSatchelEngineeringFilled
+
+- type: loadout
+  id: SeniorEngineerDuffel
+  equipment: SeniorEngineerDuffel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerDuffel
+  equipment:
+    back: ClothingBackpackDuffelEngineeringFilled
+
+# OuterClothing
+- type: loadout
+  id: StationEngineerOuterVest
+  equipment: StationEngineerOuterVest
+
+- type: startingGear
+  id: StationEngineerOuterVest
+  equipment:
+    outerClothing: ClothingOuterVestHazard
+
+# ID
+- type: loadout
+  id: StationEngineerPDA
+  equipment: StationEngineerPDA
+
+- type: startingGear
+  id: StationEngineerPDA
+  equipment:
+    id: EngineerPDA
+
+- type: loadout
+  id: SeniorEngineerPDA
+  equipment: SeniorEngineerPDA
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorEngineering
+
+- type: startingGear
+  id: SeniorEngineerPDA
+  equipment:
+    id: SeniorEngineerPDA
diff --git a/Resources/Prototypes/Loadouts/Jobs/Engineering/technical_assistant.yml b/Resources/Prototypes/Loadouts/Jobs/Engineering/technical_assistant.yml
new file mode 100644 (file)
index 0000000..b7689da
--- /dev/null
@@ -0,0 +1,18 @@
+# Jumpsuit
+- type: loadout
+  id: YellowJumpsuit
+  equipment: YellowJumpsuit
+
+- type: startingGear
+  id: YellowJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitColorYellow
+
+- type: loadout
+  id: YellowJumpskirt
+  equipment: YellowJumpskirt
+
+- type: startingGear
+  id: YellowJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtColorYellow
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/chemist.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/chemist.yml
new file mode 100644 (file)
index 0000000..a1502c2
--- /dev/null
@@ -0,0 +1,56 @@
+# Jumpsuit
+- type: loadout
+  id: ChemistJumpsuit
+  equipment: ChemistJumpsuit
+
+- type: startingGear
+  id: ChemistJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitChemistry
+
+- type: loadout
+  id: ChemistJumpskirt
+  equipment: ChemistJumpskirt
+
+- type: startingGear
+  id: ChemistJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtChemistry
+
+# Back
+- type: loadout
+  id: ChemistBackpack
+  equipment: ChemistBackpack
+
+- type: startingGear
+  id: ChemistBackpack
+  equipment:
+    back: ClothingBackpackChemistryFilled
+
+- type: loadout
+  id: ChemistSatchel
+  equipment: ChemistSatchel
+
+- type: startingGear
+  id: ChemistSatchel
+  equipment:
+    back: ClothingBackpackSatchelChemistryFilled
+
+- type: loadout
+  id: ChemistDuffel
+  equipment: ChemistDuffel
+
+- type: startingGear
+  id: ChemistDuffel
+  equipment:
+    back: ClothingBackpackDuffelChemistryFilled
+
+# Outer clothing
+- type: loadout
+  id: ChemistLabCoat
+  equipment: ChemistLabCoat
+
+- type: startingGear
+  id: ChemistLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatLabChem
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/chief_medical_officer.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/chief_medical_officer.yml
new file mode 100644 (file)
index 0000000..df9040e
--- /dev/null
@@ -0,0 +1,85 @@
+# Jumpsuit
+- type: loadout
+  id: ChiefMedicalOfficerJumpsuit
+  equipment: ChiefMedicalOfficerJumpsuit
+
+- type: startingGear
+  id: ChiefMedicalOfficerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitCMO
+
+- type: loadout
+  id: ChiefMedicalOfficerJumpskirt
+  equipment: ChiefMedicalOfficerJumpskirt
+
+- type: startingGear
+  id: ChiefMedicalOfficerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtCMO
+
+# Head
+- type: loadout
+  id: ChiefMedicalOfficerBeret
+  equipment: ChiefMedicalOfficerBeret
+
+- type: startingGear
+  id: ChiefMedicalOfficerBeret
+  equipment:
+    head: ClothingHeadHatBeretCmo
+
+# Neck
+- type: loadout
+  id: ChiefMedicalOfficerCloak
+  equipment: ChiefMedicalOfficerCloak
+
+- type: startingGear
+  id: ChiefMedicalOfficerCloak
+  equipment:
+    neck: ClothingCloakCmo
+
+- type: loadout
+  id: ChiefMedicalOfficerMantle
+  equipment: ChiefMedicalOfficerMantle
+
+- type: startingGear
+  id: ChiefMedicalOfficerMantle
+  equipment:
+    neck: ClothingNeckMantleCMO
+
+# Back
+- type: loadout
+  id: ChiefMedicalOfficerBackpack
+  equipment: ChiefMedicalOfficerBackpack
+
+- type: startingGear
+  id: ChiefMedicalOfficerBackpack
+  equipment:
+    back: ClothingBackpackCMOFilled
+
+- type: loadout
+  id: ChiefMedicalOfficerSatchel
+  equipment: ChiefMedicalOfficerSatchel
+
+- type: startingGear
+  id: ChiefMedicalOfficerSatchel
+  equipment:
+    back: ClothingBackpackSatchelCMOFilled
+
+- type: loadout
+  id: ChiefMedicalOfficerDuffel
+  equipment: ChiefMedicalOfficerDuffel
+
+- type: startingGear
+  id: ChiefMedicalOfficerDuffel
+  equipment:
+    back: ClothingBackpackDuffelCMOFilled
+
+# Outer clothing
+- type: loadout
+  id: ChiefMedicalOfficerLabCoat
+  equipment: ChiefMedicalOfficerLabCoat
+
+- type: startingGear
+  id: ChiefMedicalOfficerLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatLabCmo
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/medical_doctor.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/medical_doctor.yml
new file mode 100644 (file)
index 0000000..2e1d29b
--- /dev/null
@@ -0,0 +1,239 @@
+# Senior Time
+- type: loadoutEffectGroup
+  id: SeniorPhysician
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobChemist
+      time: 21600 #6 hrs
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobMedicalDoctor
+      time: 21600 #6 hrs
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:DepartmentTimeRequirement
+      department: Medical
+      time: 216000 # 60 hrs
+
+# Head
+
+- type: loadout
+  id: SeniorPhysicianBeret
+  equipment: SeniorPhysicianBeret
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianBeret
+  equipment:
+    head: ClothingHeadHatBeretSeniorPhysician
+
+- type: loadout
+  id: MedicalBeret
+  equipment: MedicalBeret
+
+- type: startingGear
+  id: MedicalBeret
+  equipment:
+    head: ClothingHeadHatBeretMedic
+
+- type: loadout
+  id: BlueSurgeryCap
+  equipment: BlueSurgeryCap
+
+- type: startingGear
+  id: BlueSurgeryCap
+  equipment:
+    head: ClothingHeadHatSurgcapBlue
+
+- type: loadout
+  id: GreenSurgeryCap
+  equipment: GreenSurgeryCap
+
+- type: startingGear
+  id: GreenSurgeryCap
+  equipment:
+    head: ClothingHeadHatSurgcapGreen
+
+- type: loadout
+  id: PurpleSurgeryCap
+  equipment: PurpleSurgeryCap
+
+- type: startingGear
+  id: PurpleSurgeryCap
+  equipment:
+    head: ClothingHeadHatSurgcapPurple
+
+# Jumpsuit
+- type: loadout
+  id: MedicalDoctorJumpsuit
+  equipment: MedicalDoctorJumpsuit
+
+- type: startingGear
+  id: MedicalDoctorJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitMedicalDoctor
+
+- type: loadout
+  id: MedicalDoctorJumpskirt
+  equipment: MedicalDoctorJumpskirt
+
+- type: startingGear
+  id: MedicalDoctorJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtMedicalDoctor
+
+- type: loadout
+  id: SeniorPhysicianJumpsuit
+  equipment: SeniorPhysicianJumpsuit
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitSeniorPhysician
+
+- type: loadout
+  id: SeniorPhysicianJumpskirt
+  equipment: SeniorPhysicianJumpskirt
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtSeniorPhysician
+
+- type: loadout
+  id: MedicalBlueScrubs
+  equipment: MedicalBlueScrubs
+
+- type: startingGear
+  id: MedicalBlueScrubs
+  equipment:
+    jumpsuit: UniformScrubsColorBlue
+
+- type: loadout
+  id: MedicalGreenScrubs
+  equipment: MedicalGreenScrubs
+
+- type: startingGear
+  id: MedicalGreenScrubs
+  equipment:
+    jumpsuit: UniformScrubsColorGreen
+
+- type: loadout
+  id: MedicalPurpleScrubs
+  equipment: MedicalPurpleScrubs
+
+- type: startingGear
+  id: MedicalPurpleScrubs
+  equipment:
+    jumpsuit: UniformScrubsColorPurple
+
+# Back
+- type: loadout
+  id: MedicalDoctorBackpack
+  equipment: MedicalDoctorBackpack
+
+- type: startingGear
+  id: MedicalDoctorBackpack
+  equipment:
+    back: ClothingBackpackMedicalFilled
+
+- type: loadout
+  id: MedicalDoctorSatchel
+  equipment: MedicalDoctorSatchel
+
+- type: startingGear
+  id: MedicalDoctorSatchel
+  equipment:
+    back: ClothingBackpackSatchelMedicalFilled
+
+- type: loadout
+  id: MedicalDoctorDuffel
+  equipment: MedicalDoctorDuffel
+
+- type: startingGear
+  id: MedicalDoctorDuffel
+  equipment:
+    back: ClothingBackpackDuffelMedicalFilled
+
+- type: loadout
+  id: SeniorPhysicianBackpack
+  equipment: SeniorPhysicianBackpack
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianBackpack
+  equipment:
+    back: ClothingBackpackDuffelMedicalFilled
+
+- type: loadout
+  id: SeniorPhysicianSatchel
+  equipment: SeniorPhysicianSatchel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianSatchel
+  equipment:
+    back: ClothingBackpackSatchelMedicalFilled
+
+- type: loadout
+  id: SeniorPhysicianDuffel
+  equipment: SeniorPhysicianDuffel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianDuffel
+  equipment:
+    back: ClothingBackpackDuffelMedicalFilled
+
+# OuterClothing
+
+- type: loadout
+  id: SeniorPhysicianLabCoat
+  equipment: SeniorPhysicianLabCoat
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatLabSeniorPhysician
+
+# ID
+- type: loadout
+  id: MedicalDoctorPDA
+  equipment: MedicalDoctorPDA
+
+- type: startingGear
+  id: MedicalDoctorPDA
+  equipment:
+    id: MedicalPDA
+
+- type: loadout
+  id: SeniorPhysicianPDA
+  equipment: SeniorPhysicianPDA
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorPhysician
+
+- type: startingGear
+  id: SeniorPhysicianPDA
+  equipment:
+    id: SeniorPhysicianPDA
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/medical_intern.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/medical_intern.yml
new file mode 100644 (file)
index 0000000..f2795ad
--- /dev/null
@@ -0,0 +1,18 @@
+# Jumpsuit
+- type: loadout
+  id: WhiteJumpsuit
+  equipment: WhiteJumpsuit
+
+- type: startingGear
+  id: WhiteJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitColorWhite
+
+- type: loadout
+  id: WhiteJumpskirt
+  equipment: WhiteJumpskirt
+
+- type: startingGear
+  id: WhiteJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtColorWhite
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml b/Resources/Prototypes/Loadouts/Jobs/Medical/paramedic.yml
new file mode 100644 (file)
index 0000000..0747df5
--- /dev/null
@@ -0,0 +1,66 @@
+# Head
+- type: loadout
+  id: ParamedicHead
+  equipment: ParamedicHead
+
+- type: startingGear
+  id: ParamedicHead
+  equipment:
+    head: ClothingHeadHatParamedicsoft
+
+# Jumpsuit
+- type: loadout
+  id: ParamedicJumpsuit
+  equipment: ParamedicJumpsuit
+
+- type: startingGear
+  id: ParamedicJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitParamedic
+
+- type: loadout
+  id: ParamedicJumpskirt
+  equipment: ParamedicJumpskirt
+
+- type: startingGear
+  id: ParamedicJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtParamedic
+
+# Back
+- type: loadout
+  id: ParamedicBackpack
+  equipment: ParamedicBackpack
+
+- type: startingGear
+  id: ParamedicBackpack
+  equipment:
+    back: ClothingBackpackParamedicFilled
+
+- type: loadout
+  id: ParamedicSatchel
+  equipment: ParamedicSatchel
+
+- type: startingGear
+  id: ParamedicSatchel
+  equipment:
+    back: ClothingBackpackSatchelParamedicFilled
+
+- type: loadout
+  id: ParamedicDuffel
+  equipment: ParamedicDuffel
+
+- type: startingGear
+  id: ParamedicDuffel
+  equipment:
+    back: ClothingBackpackDuffelParamedicFilled
+
+# Outer clothing
+- type: loadout
+  id: ParamedicWindbreaker
+  equipment: ParamedicWindbreaker
+
+- type: startingGear
+  id: ParamedicWindbreaker
+  equipment:
+    outerClothing: ClothingOuterCoatParamedicWB
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml b/Resources/Prototypes/Loadouts/Jobs/Science/research_director.yml
new file mode 100644 (file)
index 0000000..b02bd51
--- /dev/null
@@ -0,0 +1,82 @@
+# Head
+
+- type: loadout
+  id: ResearchDirectorBeret
+  equipment: ScientificBeret
+
+# Neck
+
+- type: loadout
+  id: ResearchDirectorMantle
+  equipment: ResearchDirectorMantle
+
+- type: startingGear
+  id: ResearchDirectorMantle
+  equipment:
+    neck: ClothingNeckMantleRD
+
+- type: loadout
+  id: ResearchDirectorCloak
+  equipment: ResearchDirectorCloak
+
+- type: startingGear
+  id: ResearchDirectorCloak
+  equipment:
+    neck: ClothingNeckCloakRd
+
+# Jumpsuit
+- type: loadout
+  id: ResearchDirectorJumpsuit
+  equipment: ResearchDirectorJumpsuit
+
+- type: startingGear
+  id: ResearchDirectorJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitResearchDirector
+
+- type: loadout
+  id: ResearchDirectorJumpskirt
+  equipment: ResearchDirectorJumpskirt
+
+- type: startingGear
+  id: ResearchDirectorJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtResearchDirector
+
+# Back
+- type: loadout
+  id: ResearchDirectorBackpack
+  equipment: ResearchDirectorBackpack
+
+- type: startingGear
+  id: ResearchDirectorBackpack
+  equipment:
+    back: ClothingBackpackResearchDirectorFilled
+
+- type: loadout
+  id: ResearchDirectorSatchel
+  equipment: ResearchDirectorSatchel
+
+- type: startingGear
+  id: ResearchDirectorSatchel
+  equipment:
+    back: ClothingBackpackSatchelResearchDirectorFilled
+
+- type: loadout
+  id: ResearchDirectorDuffel
+  equipment: ResearchDirectorDuffel
+
+- type: startingGear
+  id: ResearchDirectorDuffel
+  equipment:
+    back: ClothingBackpackDuffelResearchDirectorFilled
+
+# OuterClothing
+- type: loadout
+  id: ResearchDirectorLabCoat
+  equipment: ResearchDirectorLabCoat
+
+- type: startingGear
+  id: ResearchDirectorLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatRD
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml b/Resources/Prototypes/Loadouts/Jobs/Science/scientist.yml
new file mode 100644 (file)
index 0000000..e447742
--- /dev/null
@@ -0,0 +1,194 @@
+# Senior Time
+- type: loadoutEffectGroup
+  id: SeniorResearcher
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:DepartmentTimeRequirement
+      department: Science
+      time: 216000 #60 hrs
+
+# Head
+
+- type: loadout
+  id: ScientificBeret
+  equipment: ScientificBeret
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: ScientificBeret
+  equipment:
+    head: ClothingHeadHatBeretRND
+
+# Neck
+
+- type: loadout
+  id: ScientistTie
+  equipment: ScientistTie
+
+- type: startingGear
+  id: ScientistTie
+  equipment:
+    neck: ClothingNeckTieSci
+
+# Jumpsuit
+- type: loadout
+  id: ScientistJumpsuit
+  equipment: ScientistJumpsuit
+
+- type: startingGear
+  id: ScientistJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitScientist
+
+- type: loadout
+  id: ScientistJumpskirt
+  equipment: ScientistJumpskirt
+
+- type: startingGear
+  id: ScientistJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtScientist
+
+- type: loadout
+  id: SeniorResearcherJumpsuit
+  equipment: SeniorResearcherJumpsuit
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitSeniorResearcher
+
+- type: loadout
+  id: SeniorResearcherJumpskirt
+  equipment: SeniorResearcherJumpskirt
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtSeniorResearcher
+
+# Back
+- type: loadout
+  id: ScientistBackpack
+  equipment: ScientistBackpack
+
+- type: startingGear
+  id: ScientistBackpack
+  equipment:
+    back: ClothingBackpackScienceFilled
+
+- type: loadout
+  id: ScientistSatchel
+  equipment: ScientistSatchel
+
+- type: startingGear
+  id: ScientistSatchel
+  equipment:
+    back: ClothingBackpackSatchelScienceFilled
+
+- type: loadout
+  id: ScientistDuffel
+  equipment: ScientistDuffel
+
+- type: startingGear
+  id: ScientistDuffel
+  equipment:
+    back: ClothingBackpackDuffelScienceFilled
+
+- type: loadout
+  id: SeniorResearcherBackpack
+  equipment: SeniorResearcherBackpack
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherBackpack
+  equipment:
+    back: ClothingBackpackScienceFilled
+
+- type: loadout
+  id: SeniorResearcherSatchel
+  equipment: SeniorResearcherSatchel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherSatchel
+  equipment:
+    back: ClothingBackpackSatchelScienceFilled
+
+- type: loadout
+  id: SeniorResearcherDuffel
+  equipment: SeniorResearcherDuffel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherDuffel
+  equipment:
+    back: ClothingBackpackDuffelScienceFilled
+
+# OuterClothing
+- type: loadout
+  id: RegularLabCoat
+  equipment: RegularLabCoat
+
+- type: startingGear
+  id: RegularLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatLab
+
+- type: loadout
+  id: ScienceLabCoat
+  equipment: ScienceLabCoat
+
+- type: startingGear
+  id: ScienceLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatRnd
+
+- type: loadout
+  id: SeniorResearcherLabCoat
+  equipment: SeniorResearcherLabCoat
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherLabCoat
+  equipment:
+    outerClothing: ClothingOuterCoatLabSeniorResearcher
+
+# ID
+- type: loadout
+  id: ScientistPDA
+  equipment: ScientistPDA
+
+- type: startingGear
+  id: ScientistPDA
+  equipment:
+    id: SciencePDA
+
+- type: loadout
+  id: SeniorResearcherPDA
+  equipment: SeniorResearcherPDA
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorResearcher
+
+- type: startingGear
+  id: SeniorResearcherPDA
+  equipment:
+    id: SeniorResearcherPDA
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/detective.yml b/Resources/Prototypes/Loadouts/Jobs/Security/detective.yml
new file mode 100644 (file)
index 0000000..c102277
--- /dev/null
@@ -0,0 +1,112 @@
+# Head
+- type: loadout
+  id: DetectiveFedora
+  equipment: DetectiveFedora
+
+- type: startingGear
+  id: DetectiveFedora
+  equipment:
+    head: ClothingHeadHatFedoraBrown
+
+- type: loadout
+  id: DetectiveFedoraGrey
+  equipment: DetectiveFedoraGrey
+
+- type: startingGear
+  id: DetectiveFedoraGrey
+  equipment:
+    head: ClothingHeadHatFedoraGrey
+  
+# Neck
+- type: loadout
+  id: DetectiveTie
+  equipment: DetectiveTie
+
+- type: startingGear
+  id: DetectiveTie
+  equipment:
+    neck: ClothingNeckTieDet
+
+# Jumpsuit
+- type: loadout
+  id: DetectiveJumpsuit
+  equipment: DetectiveJumpsuit
+
+- type: startingGear
+  id: DetectiveJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitDetective
+
+- type: loadout
+  id: DetectiveJumpskirt
+  equipment: DetectiveJumpskirt
+
+- type: startingGear
+  id: DetectiveJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtDetective
+
+- type: loadout
+  id: NoirJumpsuit
+  equipment: NoirJumpsuit
+
+- type: startingGear
+  id: NoirJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitDetectiveGrey
+
+- type: loadout
+  id: NoirJumpskirt
+  equipment: NoirJumpskirt
+
+- type: startingGear
+  id: NoirJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtDetectiveGrey
+
+# Back
+- type: loadout
+  id: DetectiveBackpack
+  equipment: DetectiveBackpack
+
+- type: startingGear
+  id: DetectiveBackpack
+  equipment:
+    back: ClothingBackpackSecurityFilledDetective
+
+- type: loadout
+  id: DetectiveSatchel
+  equipment: DetectiveSatchel
+
+- type: startingGear
+  id: DetectiveSatchel
+  equipment:
+    back: ClothingBackpackSatchelSecurityFilledDetective
+
+- type: loadout
+  id: DetectiveDuffel
+  equipment: DetectiveDuffel
+
+- type: startingGear
+  id: DetectiveDuffel
+  equipment:
+    back: ClothingBackpackDuffelSecurityFilledDetective
+
+# OuterClothing
+- type: loadout
+  id: DetectiveArmorVest
+  equipment: DetectiveArmorVest
+
+- type: startingGear
+  id: DetectiveArmorVest
+  equipment:
+    outerClothing: ClothingOuterVestDetective
+
+- type: loadout
+  id: DetectiveCoat
+  equipment: DetectiveCoat
+
+- type: startingGear
+  id: DetectiveCoat
+  equipment:
+    outerClothing: ClothingOuterCoatDetective
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml b/Resources/Prototypes/Loadouts/Jobs/Security/head_of_security.yml
new file mode 100644 (file)
index 0000000..a8a52fe
--- /dev/null
@@ -0,0 +1,111 @@
+# Jumpsuit
+- type: loadout
+  id: HeadofSecurityJumpsuit
+  equipment: HeadofSecurityJumpsuit
+
+- type: startingGear
+  id: HeadofSecurityJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitHoS
+
+- type: loadout
+  id: HeadofSecurityJumpskirt
+  equipment: HeadofSecurityJumpskirt
+
+- type: startingGear
+  id: HeadofSecurityJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtHoS
+
+- type: loadout
+  id: HeadofSecurityTurtleneck
+  equipment: HeadofSecurityTurtleneck
+
+- type: startingGear
+  id: HeadofSecurityTurtleneck
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitHoSAlt
+
+- type: loadout
+  id: HeadofSecurityTurtleneckSkirt
+  equipment: HeadofSecurityTurtleneckSkirt
+
+- type: startingGear
+  id: HeadofSecurityTurtleneckSkirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtHoSAlt
+
+- type: loadout
+  id: HeadofSecurityFormalSuit
+  equipment: HeadofSecurityFormalSuit
+
+- type: startingGear
+  id: HeadofSecurityFormalSuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitHosFormal
+
+- type: loadout
+  id: HeadofSecurityFormalSkirt
+  equipment: HeadofSecurityFormalSkirt
+
+- type: startingGear
+  id: HeadofSecurityFormalSkirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtHosFormal
+
+# Head
+- type: loadout
+  id: HeadofSecurityHead
+  equipment: HeadofSecurityHead
+
+- type: startingGear
+  id: HeadofSecurityHead
+  equipment:
+    head: ClothingHeadHatHoshat
+
+- type: loadout
+  id: HeadofSecurityBeret
+  equipment: HeadofSecurityBeret
+
+- type: startingGear
+  id: HeadofSecurityBeret
+  equipment:
+    head: ClothingHeadHatBeretHoS
+
+# Neck
+- type: loadout
+  id: HeadofSecurityCloak
+  equipment: HeadofSecurityCloak
+
+- type: startingGear
+  id: HeadofSecurityCloak
+  equipment:
+    neck: ClothingNeckCloakHos
+
+- type: loadout
+  id: HeadofSecurityMantle
+  equipment: HeadofSecurityMantle
+
+- type: startingGear
+  id: HeadofSecurityMantle
+  equipment:
+    neck: ClothingNeckMantleHOS
+
+# OuterClothing
+- type: loadout
+  id: HeadofSecurityCoat
+  equipment: HeadofSecurityCoat
+
+- type: startingGear
+  id: HeadofSecurityCoat
+  equipment:
+    outerClothing: ClothingOuterCoatHoSTrench
+
+- type: loadout
+  id: HeadofSecurityWinterCoat
+  equipment: HeadofSecurityWinterCoat
+
+- type: startingGear
+  id: HeadofSecurityWinterCoat
+  equipment:
+    outerClothing: ClothingOuterWinterHoS
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/security_cadet.yml b/Resources/Prototypes/Loadouts/Jobs/Security/security_cadet.yml
new file mode 100644 (file)
index 0000000..0157d05
--- /dev/null
@@ -0,0 +1,18 @@
+# Jumpsuit
+- type: loadout
+  id: RedJumpsuit
+  equipment: RedJumpsuit
+
+- type: startingGear
+  id: RedJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitColorRed
+
+- type: loadout
+  id: RedJumpskirt
+  equipment: RedJumpskirt
+
+- type: startingGear
+  id: RedJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtColorRed
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml b/Resources/Prototypes/Loadouts/Jobs/Security/security_officer.yml
new file mode 100644 (file)
index 0000000..30f4e99
--- /dev/null
@@ -0,0 +1,171 @@
+# Senior Time
+- type: loadoutEffectGroup
+  id: SeniorOfficer
+  effects:
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:RoleTimeRequirement
+      role: JobWarden
+      time: 21600 #6 hrs
+  - !type:JobRequirementLoadoutEffect
+    requirement:
+      !type:DepartmentTimeRequirement
+      department: Security
+      time: 216000 # 60 hrs
+
+# Head
+- type: loadout
+  id: SecurityHelmet
+  equipment: SecurityHelmet
+
+- type: startingGear
+  id: SecurityHelmet
+  equipment:
+    head: ClothingHeadHelmetBasic
+
+- type: loadout
+  id: SecurityHat
+  equipment: SecurityHat
+
+- type: startingGear
+  id: SecurityHat
+  equipment:
+    head: ClothingHeadHatSecsoft
+
+- type: loadout
+  id: SecurityBeret
+  equipment: SecurityBeret
+
+- type: startingGear
+  id: SecurityBeret
+  equipment:
+    head: ClothingHeadHatBeretSecurity
+
+# Jumpsuit
+- type: loadout
+  id: SecurityJumpsuit
+  equipment: SecurityJumpsuit
+
+- type: startingGear
+  id: SecurityJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitSec
+
+- type: loadout
+  id: SecurityJumpskirt
+  equipment: SecurityJumpskirt
+
+- type: startingGear
+  id: SecurityJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtSec
+
+- type: loadout
+  id: SeniorOfficerJumpsuit
+  equipment: SeniorOfficerJumpsuit
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitSeniorOfficer
+
+- type: loadout
+  id: SeniorOfficerJumpskirt
+  equipment: SeniorOfficerJumpskirt
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtSeniorOfficer
+
+# Back
+- type: loadout
+  id: SecurityBackpack
+  equipment: SecurityBackpack
+
+- type: startingGear
+  id: SecurityBackpack
+  equipment:
+    back: ClothingBackpackSecurityFilled
+
+- type: loadout
+  id: SecuritySatchel
+  equipment: SecuritySatchel
+
+- type: startingGear
+  id: SecuritySatchel
+  equipment:
+    back: ClothingBackpackSatchelSecurityFilled
+
+- type: loadout
+  id: SecurityDuffel
+  equipment: SecurityDuffel
+
+- type: startingGear
+  id: SecurityDuffel
+  equipment:
+    back: ClothingBackpackDuffelSecurityFilled
+
+- type: loadout
+  id: SeniorOfficerBackpack
+  equipment: SeniorOfficerBackpack
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerBackpack
+  equipment:
+    back: ClothingBackpackSecurityFilled
+
+- type: loadout
+  id: SeniorOfficerSatchel
+  equipment: SeniorOfficerSatchel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerSatchel
+  equipment:
+    back: ClothingBackpackSatchelSecurityFilled
+
+- type: loadout
+  id: SeniorOfficerDuffel
+  equipment: SeniorOfficerDuffel
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerDuffel
+  equipment:
+    back: ClothingBackpackDuffelSecurityFilled
+
+# PDA
+- type: loadout
+  id: SecurityPDA
+  equipment: SecurityPDA
+
+- type: startingGear
+  id: SecurityPDA
+  equipment:
+    id: SecurityPDA
+
+- type: loadout
+  id: SeniorOfficerPDA
+  equipment: SeniorOfficerPDA
+  effects:
+  - !type:GroupLoadoutEffect
+    proto: SeniorOfficer
+
+- type: startingGear
+  id: SeniorOfficerPDA
+  equipment:
+    id: SeniorOfficerPDA
diff --git a/Resources/Prototypes/Loadouts/Jobs/Security/warden.yml b/Resources/Prototypes/Loadouts/Jobs/Security/warden.yml
new file mode 100644 (file)
index 0000000..a2a0ac6
--- /dev/null
@@ -0,0 +1,56 @@
+# Head
+- type: loadout
+  id: WardenHead
+  equipment: WardenHead
+
+- type: startingGear
+  id: WardenHead
+  equipment:
+    head: ClothingHeadHatWarden
+
+- type: loadout
+  id: WardenBeret
+  equipment: WardenBeret
+
+- type: startingGear
+  id: WardenBeret
+  equipment:
+    head: ClothingHeadHatBeretWarden
+
+# Jumpsuit
+- type: loadout
+  id: WardenJumpsuit
+  equipment: WardenJumpsuit
+
+- type: startingGear
+  id: WardenJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitWarden
+
+- type: loadout
+  id: WardenJumpskirt
+  equipment: WardenJumpskirt
+
+- type: startingGear
+  id: WardenJumpskirt
+  equipment:
+    jumpsuit: ClothingUniformJumpskirtWarden
+
+# OuterClothing
+- type: loadout
+  id: WardenCoat
+  equipment: WardenCoat
+
+- type: startingGear
+  id: WardenCoat
+  equipment:
+    outerClothing: ClothingOuterCoatWarden
+
+- type: loadout
+  id: WardenArmoredWinterCoat
+  equipment: WardenArmoredWinterCoat
+
+- type: startingGear
+  id: WardenArmoredWinterCoat
+  equipment:
+    outerClothing: ClothingOuterWinterWarden
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Wildcards/boxer.yml b/Resources/Prototypes/Loadouts/Jobs/Wildcards/boxer.yml
new file mode 100644 (file)
index 0000000..d672fea
--- /dev/null
@@ -0,0 +1,55 @@
+# Jumpsuit
+- type: loadout
+  id: BoxerShorts
+  equipment: BoxerShorts
+
+- type: startingGear
+  id: BoxerShorts
+  equipment:
+    jumpsuit: UniformShortsRed
+
+- type: loadout
+  id: BoxerShortsWithTop
+  equipment: BoxerShortsWithTop
+
+- type: startingGear
+  id: BoxerShortsWithTop
+  equipment:
+    jumpsuit: UniformShortsRedWithTop
+
+# Gloves
+- type: loadout
+  id: RedBoxingGloves
+  equipment: RedBoxingGloves
+
+- type: startingGear
+  id: RedBoxingGloves
+  equipment:
+    gloves: ClothingHandsGlovesBoxingRed
+
+- type: loadout
+  id: BlueBoxingGloves
+  equipment: BlueBoxingGloves
+
+- type: startingGear
+  id: BlueBoxingGloves
+  equipment:
+    gloves: ClothingHandsGlovesBoxingBlue
+
+- type: loadout
+  id: GreenBoxingGloves
+  equipment: GreenBoxingGloves
+
+- type: startingGear
+  id: GreenBoxingGloves
+  equipment:
+    gloves: ClothingHandsGlovesBoxingGreen
+
+- type: loadout
+  id: YellowBoxingGloves
+  equipment: YellowBoxingGloves
+
+- type: startingGear
+  id: YellowBoxingGloves
+  equipment:
+    gloves: ClothingHandsGlovesBoxingYellow
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml b/Resources/Prototypes/Loadouts/Jobs/Wildcards/reporter.yml
new file mode 100644 (file)
index 0000000..6cd2441
--- /dev/null
@@ -0,0 +1,18 @@
+# Jumpsuit
+- type: loadout
+  id: ReporterJumpsuit
+  equipment: ReporterJumpsuit
+
+- type: startingGear
+  id: ReporterJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitReporter
+
+- type: loadout
+  id: JournalistJumpsuit
+  equipment: JournalistJumpsuit
+
+- type: startingGear
+  id: JournalistJumpsuit
+  equipment:
+    jumpsuit: ClothingUniformJumpsuitJournalist
\ No newline at end of file
diff --git a/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml b/Resources/Prototypes/Loadouts/Miscellaneous/trinkets.yml
new file mode 100644 (file)
index 0000000..98910f3
--- /dev/null
@@ -0,0 +1,99 @@
+- type: loadout
+  id: PlushieLizard
+  equipment: PlushieLizard
+
+- type: startingGear
+  id: PlushieLizard
+  storage:
+    back:
+    - PlushieLizard
+
+- type: loadout
+  id: ClothingNeckLGBTPin
+  equipment: ClothingNeckLGBTPin
+
+- type: startingGear
+  id: ClothingNeckLGBTPin
+  storage:
+    back:
+    - ClothingNeckLGBTPin
+
+- type: loadout
+  id: ClothingNeckAromanticPin
+  equipment: ClothingNeckAromanticPin
+
+- type: startingGear
+  id: ClothingNeckAromanticPin
+  storage:
+    back:
+    - ClothingNeckAromanticPin
+
+- type: loadout
+  id: ClothingNeckAsexualPin
+  equipment: ClothingNeckAsexualPin
+
+- type: startingGear
+  id: ClothingNeckAsexualPin
+  storage:
+    back:
+    - ClothingNeckAsexualPin
+
+- type: loadout
+  id: ClothingNeckBisexualPin
+  equipment: ClothingNeckBisexualPin
+
+- type: startingGear
+  id: ClothingNeckBisexualPin
+  storage:
+    back:
+    - ClothingNeckBisexualPin
+
+- type: loadout
+  id: ClothingNeckIntersexPin
+  equipment: ClothingNeckIntersexPin
+
+- type: startingGear
+  id: ClothingNeckIntersexPin
+  storage:
+    back:
+    - ClothingNeckIntersexPin
+
+- type: loadout
+  id: ClothingNeckLesbianPin
+  equipment: ClothingNeckLesbianPin
+
+- type: startingGear
+  id: ClothingNeckLesbianPin
+  storage:
+    back:
+    - ClothingNeckLesbianPin
+
+- type: loadout
+  id: ClothingNeckNonBinaryPin
+  equipment: ClothingNeckNonBinaryPin
+
+- type: startingGear
+  id: ClothingNeckNonBinaryPin
+  storage:
+    back:
+    - ClothingNeckNonBinaryPin
+
+- type: loadout
+  id: ClothingNeckPansexualPin
+  equipment: ClothingNeckPansexualPin
+
+- type: startingGear
+  id: ClothingNeckPansexualPin
+  storage:
+    back:
+    - ClothingNeckPansexualPin
+
+- type: loadout
+  id: ClothingNeckTransPin
+  equipment: ClothingNeckTransPin
+
+- type: startingGear
+  id: ClothingNeckTransPin
+  storage:
+    back:
+    - ClothingNeckTransPin
diff --git a/Resources/Prototypes/Loadouts/loadout_groups.yml b/Resources/Prototypes/Loadouts/loadout_groups.yml
new file mode 100644 (file)
index 0000000..5541d0e
--- /dev/null
@@ -0,0 +1,934 @@
+# Miscellaneous
+- type: loadoutGroup
+  id: Trinkets
+  name: loadout-group-trinkets
+  minLimit: 0
+  maxLimit: 3
+  loadouts:
+  - PlushieLizard
+  - ClothingNeckLGBTPin
+  - ClothingNeckAromanticPin
+  - ClothingNeckAsexualPin
+  - ClothingNeckBisexualPin
+  - ClothingNeckIntersexPin
+  - ClothingNeckLesbianPin
+  - ClothingNeckNonBinaryPin
+  - ClothingNeckPansexualPin
+  - ClothingNeckTransPin
+
+# Command
+- type: loadoutGroup
+  id: CaptainHead
+  name: loadout-group-captain-head
+  minLimit: 0
+  loadouts:
+  - CaptainHead
+  - CaptainCap
+
+- type: loadoutGroup
+  id: CaptainJumpsuit
+  name: loadout-group-captain-jumpsuit
+  loadouts:
+  - CaptainJumpsuit
+  - CaptainJumpskirt
+  - CaptainFormalSuit
+  - CaptainFormalSkirt
+
+- type: loadoutGroup
+  id: CaptainNeck
+  name: loadout-group-captain-neck
+  minLimit: 0
+  loadouts:
+  - CaptainCloak
+  - CaptainCloakFormal
+  - CaptainMantle
+
+- type: loadoutGroup
+  id: CaptainBackpack
+  name: loadout-group-captain-backpack
+  loadouts:
+  - CaptainBackpack
+  - CaptainSatchel
+  - CaptainDuffel
+
+- type: loadoutGroup
+  id: HoPHead
+  name: loadout-group-hop-head
+  minLimit: 0
+  loadouts:
+  - HoPHead
+
+- type: loadoutGroup
+  id: HoPJumpsuit
+  name: loadout-group-hop-jumpsuit
+  loadouts:
+  - HoPJumpsuit
+  - HoPJumpskirt
+
+- type: loadoutGroup
+  id: HoPNeck
+  name: loadout-group-hop-neck
+  minLimit: 0
+  loadouts:
+  - HoPCloak
+  - HoPMantle
+
+- type: loadoutGroup
+  id: HoPBackpack
+  name: loadout-group-hop-backpack
+  loadouts:
+  - HoPBackpack
+  - HoPSatchel
+  - HoPDuffel
+  - HoPBackpackIan
+
+# Civilian
+- type: loadoutGroup
+  id: PassengerJumpsuit
+  name: loadout-group-passenger-jumpsuit
+  loadouts:
+  - GreyJumpsuit
+  - GreyJumpskirt
+
+- type: loadoutGroup
+  id: PassengerFace
+  name: loadout-group-passenger-mask
+  minLimit: 0
+  loadouts:
+  - PassengerFace
+
+- type: loadoutGroup
+  id: PassengerGloves
+  name: loadout-group-passenger-gloves
+  minLimit: 0
+  loadouts:
+  - PassengerGloves
+
+- type: loadoutGroup
+  id: CommonBackpack
+  name: loadout-group-passenger-backpack
+  loadouts:
+  - CommonBackpack
+  - CommonSatchel
+  - CommonDuffel
+
+- type: loadoutGroup
+  id: BartenderHead
+  name: loadout-group-bartender-head
+  minLimit: 0
+  loadouts:
+  - BartenderHead
+  - BartenderBowler
+
+- type: loadoutGroup
+  id: BartenderJumpsuit
+  name: loadout-group-bartender-jumpsuit
+  loadouts:
+  - BartenderJumpsuit
+  - BartenderJumpskirt
+  - BartenderJumpsuitPurple
+
+- type: loadoutGroup
+  id: BartenderOuterClothing
+  name: loadout-group-bartender-outerclothing
+  minLimit: 0
+  loadouts:
+  - BartenderVest
+  - BartenderApron
+
+- type: loadoutGroup
+  id: ChefHead
+  name: loadout-group-chef-head
+  minLimit: 0
+  loadouts:
+  - ChefHead
+
+- type: loadoutGroup
+  id: ChefMask
+  name: loadout-group-chef-mask
+  minLimit: 0
+  loadouts:
+  - ChefMask
+
+- type: loadoutGroup
+  id: ChefJumpsuit
+  name: loadout-group-chef-jumpsuit
+  loadouts:
+  - ChefJumpsuit
+  - ChefJumpskirt
+
+- type: loadoutGroup
+  id: ChefOuterClothing
+  name: loadout-group-chef-outerclothing
+  minLimit: 0
+  loadouts:
+  - ChefApron
+  - ChefJacket
+
+- type: loadoutGroup
+  id: LibrarianJumpsuit
+  name: loadout-group-librarian-jumpsuit
+  loadouts:
+  - LibrarianJumpsuit
+  - LibrarianJumpskirt
+  - CuratorJumpsuit
+  - CuratorJumpskirt
+
+- type: loadoutGroup
+  id: LawyerJumpsuit
+  name: loadout-group-lawyer-jumpsuit
+  loadouts:
+  - LawyerJumpsuit
+  - LawyerJumpskirt
+  - LawyerJumpsuitBlue
+  - LawyerJumpskirtBlue
+  - LawyerJumpsuitPurple
+  - LawyerJumpskirtPurple
+  - LawyerJumpsuitRed
+  - LawyerJumpskirtRed
+  - LawyerJumpsuitGood
+  - LawyerJumpskirtGood
+
+- type: loadoutGroup
+  id: LawyerNeck
+  name: loadout-group-lawyer-neck
+  minLimit: 0
+  loadouts:
+  - LawyerNeck
+
+- type: loadoutGroup
+  id: ChaplainHead
+  name: loadout-group-chaplain-head
+  minLimit: 0
+  loadouts:
+  - ChaplainHead
+  - ChaplainPlagueHat
+  - ChaplainWitchHat
+  - ChaplainWitchHatAlt
+
+- type: loadoutGroup
+  id: ChaplainMask
+  name: loadout-group-chaplain-mask
+  minLimit: 0
+  loadouts:
+  - ChaplainMask
+
+- type: loadoutGroup
+  id: ChaplainJumpsuit
+  name: loadout-group-chaplain-jumpsuit
+  loadouts:
+  - ChaplainJumpsuit
+  - ChaplainJumpskirt
+  - ChaplainRobesLight
+  - ChaplainRobesDark
+
+- type: loadoutGroup
+  id: ChaplainBackpack
+  name: loadout-group-chaplain-backpack
+  loadouts:
+  - ChaplainBackpack
+  - ChaplainSatchel
+  - ChaplainDuffel
+
+- type: loadoutGroup
+  id: ChaplainOuterClothing
+  name: loadout-group-chaplain-outerclothing
+  minLimit: 0
+  loadouts:
+  - ChaplainPlagueSuit
+  - ChaplainNunRobe
+  - ChaplainBlackHoodie
+  - ChaplainHoodie
+
+- type: loadoutGroup
+  id: ChaplainNeck
+  name: loadout-group-chaplain-neck
+  minLimit: 0
+  loadouts:
+  - ChaplainNeck
+
+- type: loadoutGroup
+  id: JanitorHead
+  name: loadout-group-janitor-head
+  minLimit: 0
+  loadouts:
+  - JanitorHead
+
+- type: loadoutGroup
+  id: JanitorJumpsuit
+  name: loadout-group-janitor-jumpsuit
+  loadouts:
+  - JanitorJumpsuit
+  - JanitorJumpskirt
+
+- type: loadoutGroup
+  id: BotanistHead
+  name: loadout-group-botanist-head
+  minLimit: 0
+  loadouts:
+  - BotanistHead
+  - BotanistBandana
+
+- type: loadoutGroup
+  id: BotanistJumpsuit
+  name: loadout-group-botanist-jumpsuit
+  loadouts:
+  - BotanistJumpsuit
+  - BotanistJumpskirt
+  - BotanistOveralls
+
+- type: loadoutGroup
+  id: BotanistBackpack
+  name: loadout-group-botanist-backpack
+  loadouts:
+  - BotanistBackpack
+  - BotanistSatchel
+  - BotanistDuffel
+
+- type: loadoutGroup
+  id: BotanistOuterClothing
+  name: loadout-group-botanist-outerclothing
+  minLimit: 0
+  loadouts:
+  - BotanistApron
+
+- type: loadoutGroup
+  id: ClownHead
+  name: loadout-group-clown-head
+  minLimit: 0
+  loadouts:
+  - JesterHat
+
+- type: loadoutGroup
+  id: ClownJumpsuit
+  name: loadout-group-clown-jumpsuit
+  loadouts:
+  - ClownSuit
+  - JesterSuit
+
+- type: loadoutGroup
+  id: ClownBackpack
+  name: loadout-group-clown-backpack
+  loadouts:
+  - ClownBackpack
+  - ClownSatchel
+  - ClownDuffel
+
+- type: loadoutGroup
+  id: ClownShoes
+  name: loadout-group-clown-shoes
+  loadouts:
+  - ClownShoes
+  - JesterShoes
+
+- type: loadoutGroup
+  id: MimeHead
+  name: loadout-group-mime-head
+  minLimit: 0
+  loadouts:
+  - MimeHead
+  - MimeFrenchBeret
+  - MimeCap
+
+- type: loadoutGroup
+  id: MimeMask
+  name: loadout-group-mime-mask
+  minLimit: 0
+  loadouts:
+  - MimeMask
+  - MimeMaskSad
+  - MimeMaskScared
+
+- type: loadoutGroup
+  id: MimeJumpsuit
+  name: loadout-group-mime-jumpsuit
+  loadouts:
+  - MimeJumpsuit
+  - MimeJumpskirt
+
+- type: loadoutGroup
+  id: MimeBackpack
+  name: loadout-group-mime-backpack
+  loadouts:
+  - MimeBackpack
+  - MimeSatchel
+  - MimeDuffel
+
+- type: loadoutGroup
+  id: MusicianBackpack
+  name: loadout-group-musician-backpack
+  loadouts:
+  - MusicianBackpack
+  - MusicianSatchel
+  - MusicianDuffel
+
+# Cargo
+- type: loadoutGroup
+  id: QuartermasterHead
+  name: loadout-group-quartermaster-head
+  minLimit: 0
+  loadouts:
+  - QuartermasterHead
+  - QuartermasterBeret
+
+- type: loadoutGroup
+  id: QuartermasterJumpsuit
+  name: loadout-group-quartermaster-jumpsuit
+  loadouts:
+  - QuartermasterJumpsuit
+  - QuartermasterJumpskirt
+  - QuartermasterTurtleneck
+  - QuartermasterTurtleneckSkirt
+  - QuartermasterFormalSuit
+
+- type: loadoutGroup
+  id: QuartermasterBackpack
+  name: loadout-group-quartermaster-backpack
+  loadouts:
+  - QuartermasterBackpack
+  - QuartermasterSatchel
+  - QuartermasterDuffel
+
+- type: loadoutGroup
+  id: QuartermasterNeck
+  name: loadout-group-quartermaster-neck
+  minLimit: 0
+  loadouts:
+  - QuartermasterCloak
+  - QuartermasterMantle
+
+- type: loadoutGroup
+  id: CargoTechnicianHead
+  name: loadout-group-cargo-technician-head
+  minLimit: 0
+  loadouts:
+  - CargoTechnicianHead
+
+- type: loadoutGroup
+  id: CargoTechnicianJumpsuit
+  name: loadout-group-cargo-technician-jumpsuit
+  loadouts:
+  - CargoTechnicianJumpsuit
+  - CargoTechnicianJumpskirt
+
+- type: loadoutGroup
+  id: CargoTechnicianBackpack
+  name: loadout-group-cargo-technician-backpack
+  loadouts:
+  - CargoTechnicianBackpack
+  - CargoTechnicianSatchel
+  - CargoTechnicianDuffel
+
+- type: loadoutGroup
+  id: SalvageSpecialistBackpack
+  name: loadout-group-salvage-specialist-backpack
+  loadouts:
+  - SalvageSpecialistBackpack
+  - SalvageSpecialistSatchel
+  - SalvageSpecialistDuffel
+
+# Engineering
+- type: loadoutGroup
+  id: ChiefEngineerHead
+  name: loadout-group-chief-engineer-head
+  minLimit: 0
+  loadouts:
+  - ChiefEngineerHead
+  - ChiefEngineerBeret
+
+- type: loadoutGroup
+  id: ChiefEngineerJumpsuit
+  name: loadout-group-chief-engineer-jumpsuit
+  loadouts:
+  - ChiefEngineerJumpsuit
+  - ChiefEngineerJumpskirt
+  - ChiefEngineerTurtleneck
+  - ChiefEngineerTurtleneckSkirt
+
+- type: loadoutGroup
+  id: ChiefEngineerBackpack
+  name: loadout-group-chief-engineer-backpack
+  loadouts:
+  - ChiefEngineerBackpack
+  - ChiefEngineerSatchel
+  - ChiefEngineerDuffel
+
+- type: loadoutGroup
+  id: ChiefEngineerNeck
+  name: loadout-group-chief-engineer-neck
+  minLimit: 0
+  loadouts:
+  - ChiefEngineerCloak
+  - ChiefEngineerMantle
+
+- type: loadoutGroup
+  id: TechnicalAssistantJumpsuit
+  name: loadout-group-technical-assistant-jumpsuit
+  loadouts:
+  - YellowJumpsuit
+  - YellowJumpskirt
+
+- type: loadoutGroup
+  id: StationEngineerHead
+  name: loadout-group-station-engineer-head
+  loadouts:
+  - StationEngineerHead
+  - SeniorEngineerBeret
+
+- type: loadoutGroup
+  id: StationEngineerJumpsuit
+  name: loadout-group-station-engineer-jumpsuit
+  loadouts:
+  - StationEngineerJumpsuit
+  - StationEngineerJumpskirt
+  - StationEngineerHazardsuit
+  - SeniorEngineerJumpsuit
+  - SeniorEngineerJumpskirt
+
+- type: loadoutGroup
+  id: StationEngineerBackpack
+  name: loadout-group-station-engineer-backpack
+  loadouts:
+  - StationEngineerBackpack
+  - StationEngineerSatchel
+  - StationEngineerDuffel
+  - SeniorEngineerBackpack
+  - SeniorEngineerSatchel
+  - SeniorEngineerDuffel
+
+- type: loadoutGroup
+  id: StationEngineerOuterClothing
+  name: loadout-group-station-engineer-outerclothing
+  minLimit: 0
+  loadouts:
+  - StationEngineerOuterVest
+
+- type: loadoutGroup
+  id: StationEngineerID
+  name: loadout-group-station-engineer-id
+  loadouts:
+  - StationEngineerPDA
+  - SeniorEngineerPDA
+
+- type: loadoutGroup
+  id: AtmosphericTechnicianJumpsuit
+  name: loadout-group-atmospheric-technician-jumpsuit
+  loadouts:
+  - AtmosphericTechnicianJumpsuit
+  - AtmosphericTechnicianJumpskirt
+  - AtmosphericTechnicianJumpsuitCasual
+
+- type: loadoutGroup
+  id: AtmosphericTechnicianBackpack
+  name: loadout-group-atmospheric-technician-backpack
+  loadouts:
+  - AtmosphericTechnicianBackpack
+  - AtmosphericTechnicianSatchel
+  - AtmosphericTechnicianDuffel
+
+# Science
+- type: loadoutGroup
+  id: ResearchDirectorHead
+  name: loadout-group-research-director-head
+  minLimit: 0
+  loadouts:
+  - ResearchDirectorBeret
+
+- type: loadoutGroup
+  id: ResearchDirectorNeck
+  name: loadout-group-research-director-neck
+  minLimit: 0
+  loadouts:
+  - ResearchDirectorMantle
+  - ResearchDirectorCloak
+
+- type: loadoutGroup
+  id: ResearchDirectorJumpsuit
+  name: loadout-group-research-director-jumpsuit
+  loadouts:
+  - ResearchDirectorJumpsuit
+  - ResearchDirectorJumpskirt
+
+- type: loadoutGroup
+  id: ResearchDirectorBackpack
+  name: loadout-group-research-director-backpack
+  loadouts:
+  - ResearchDirectorBackpack
+  - ResearchDirectorSatchel
+  - ResearchDirectorDuffel
+
+- type: loadoutGroup
+  id: ResearchDirectorOuterClothing
+  name: loadout-group-research-director-outerclothing
+  minLimit: 0
+  loadouts:
+  - ResearchDirectorLabCoat
+
+- type: loadoutGroup
+  id: ScientistHead
+  name: loadout-group-scientist-head
+  minLimit: 0
+  loadouts:
+  - ScientificBeret
+
+- type: loadoutGroup
+  id: ScientistNeck
+  name: loadout-group-scientist-neck
+  minLimit: 0
+  loadouts:
+  - ScientistTie
+
+- type: loadoutGroup
+  id: ScientistJumpsuit
+  name: loadout-group-scientist-jumpsuit
+  loadouts:
+  - ScientistJumpsuit
+  - ScientistJumpskirt
+  - SeniorResearcherJumpsuit
+  - SeniorResearcherJumpskirt
+
+- type: loadoutGroup
+  id: ScientistBackpack
+  name: loadout-group-scientist-backpack
+  loadouts:
+  - ScientistBackpack
+  - ScientistSatchel
+  - ScientistDuffel
+  - SeniorResearcherBackpack
+  - SeniorResearcherSatchel
+  - SeniorResearcherDuffel
+
+- type: loadoutGroup
+  id: ScientistOuterClothing
+  name: loadout-group-scientist-outerclothing
+  minLimit: 0
+  loadouts:
+  - RegularLabCoat
+  - ScienceLabCoat
+  - SeniorResearcherLabCoat
+
+- type: loadoutGroup
+  id: ScientistPDA
+  name: loadout-group-scientist-id
+  loadouts:
+  - ScientistPDA
+  - SeniorResearcherPDA
+
+- type: loadoutGroup
+  id: ResearchAssistantJumpsuit
+  name: loadout-group-research-assistant-jumpsuit
+  loadouts:
+  - WhiteJumpsuit
+  - WhiteJumpskirt
+
+# Security
+- type: loadoutGroup
+  id: HeadofSecurityHead
+  name: loadout-group-head-of-security-head
+  minLimit: 0
+  loadouts:
+  - HeadofSecurityHead
+  - HeadofSecurityBeret
+
+- type: loadoutGroup
+  id: HeadofSecurityJumpsuit
+  name: loadout-group-head-of-security-jumpsuit
+  loadouts:
+  - HeadofSecurityJumpsuit
+  - HeadofSecurityJumpskirt
+  - HeadofSecurityTurtleneck
+  - HeadofSecurityTurtleneckSkirt
+  - HeadofSecurityFormalSuit
+  - HeadofSecurityFormalSkirt
+
+- type: loadoutGroup
+  id: HeadofSecurityNeck
+  name: loadout-group-head-of-security-neck
+  minLimit: 0
+  loadouts:
+  - HeadofSecurityCloak
+  - HeadofSecurityMantle
+
+- type: loadoutGroup
+  id: HeadofSecurityOuterClothing
+  name: loadout-group-head-of-security-outerclothing
+  minLimit: 0
+  loadouts:
+  - HeadofSecurityCoat
+  - HeadofSecurityWinterCoat
+
+- type: loadoutGroup
+  id: WardenHead
+  name: loadout-group-warden-head
+  minLimit: 0
+  loadouts:
+  - WardenHead
+  - WardenBeret
+
+- type: loadoutGroup
+  id: WardenJumpsuit
+  name: loadout-group-warden-jumpsuit
+  loadouts:
+  - WardenJumpsuit
+  - WardenJumpskirt
+
+- type: loadoutGroup
+  id: WardenOuterClothing
+  name: loadout-group-warden-outerclothing
+  minLimit: 0
+  loadouts:
+  - WardenCoat
+  - WardenArmoredWinterCoat
+
+- type: loadoutGroup
+  id: SecurityHead
+  name: loadout-group-security-head
+  minLimit: 0
+  loadouts:
+  - SecurityHelmet
+  - SecurityBeret
+  - SecurityHat
+
+- type: loadoutGroup
+  id: SecurityJumpsuit
+  name: loadout-group-security-jumpsuit
+  loadouts:
+  - SecurityJumpsuit
+  - SecurityJumpskirt
+  - SeniorOfficerJumpsuit
+  - SeniorOfficerJumpskirt
+
+- type: loadoutGroup
+  id: SecurityBackpack
+  name: loadout-group-security-backpack
+  loadouts:
+  - SecurityBackpack
+  - SecuritySatchel
+  - SecurityDuffel
+  - SeniorOfficerBackpack
+  - SeniorOfficerSatchel
+  - SeniorOfficerDuffel
+
+- type: loadoutGroup
+  id: SecurityPDA
+  name: loadout-group-security-id
+  loadouts:
+  - SecurityPDA
+  - SeniorOfficerPDA
+
+- type: loadoutGroup
+  id: DetectiveHead
+  name: loadout-group-detective-head
+  minLimit: 0
+  loadouts:
+  - DetectiveFedora
+  - DetectiveFedoraGrey
+
+- type: loadoutGroup
+  id: DetectiveNeck
+  name: loadout-group-detective-neck
+  minLimit: 0
+  loadouts:
+  - DetectiveTie
+
+- type: loadoutGroup
+  id: DetectiveJumpsuit
+  name: loadout-group-detective-jumpsuit
+  loadouts:
+  - DetectiveJumpsuit
+  - DetectiveJumpskirt
+  - NoirJumpsuit
+  - NoirJumpskirt
+
+- type: loadoutGroup
+  id: DetectiveBackpack
+  name: loadout-group-detective-backpack
+  loadouts:
+  - DetectiveBackpack
+  - DetectiveSatchel
+  - DetectiveDuffel
+
+- type: loadoutGroup
+  id: DetectiveOuterClothing
+  name: loadout-group-detective-outerclothing
+  minLimit: 0
+  loadouts:
+  - DetectiveArmorVest
+  - DetectiveCoat
+
+- type: loadoutGroup
+  id: SecurityCadetJumpsuit
+  name: loadout-group-security-cadet-jumpsuit
+  loadouts:
+  - RedJumpsuit
+  - RedJumpskirt
+
+# Medical
+- type: loadoutGroup
+  id: ChiefMedicalOfficerHead
+  name: loadout-group-chief-medical-officer-head
+  minLimit: 0
+  loadouts:
+  - ChiefMedicalOfficerBeret
+
+- type: loadoutGroup
+  id: ChiefMedicalOfficerJumpsuit
+  name: loadout-group-chief-medical-officer-jumpsuit
+  loadouts:
+  - ChiefMedicalOfficerJumpsuit
+  - ChiefMedicalOfficerJumpskirt
+
+- type: loadoutGroup
+  id: ChiefMedicalOfficerOuterClothing
+  name: loadout-group-chief-medical-officer-outerclothing
+  minLimit: 0
+  loadouts:
+  - ChiefMedicalOfficerLabCoat
+
+- type: loadoutGroup
+  id: ChiefMedicalOfficerBackpack
+  name: loadout-group-chief-medical-officer-backpack
+  loadouts:
+  - ChiefMedicalOfficerBackpack
+  - ChiefMedicalOfficerSatchel
+  - ChiefMedicalOfficerDuffel
+
+- type: loadoutGroup
+  id: ChiefMedicalOfficerNeck
+  name: loadout-group-chief-medical-officer-neck
+  minLimit: 0
+  loadouts:
+  - ChiefMedicalOfficerCloak
+  - ChiefMedicalOfficerMantle
+
+- type: loadoutGroup
+  id: MedicalDoctorHead
+  name: loadout-group-medical-doctor-head
+  minLimit: 0
+  loadouts:
+  - MedicalBeret
+  - SeniorPhysicianBeret
+  - BlueSurgeryCap
+  - GreenSurgeryCap
+  - PurpleSurgeryCap
+
+- type: loadoutGroup
+  id: MedicalDoctorJumpsuit
+  name: loadout-group-medical-doctor-jumpsuit
+  loadouts:
+  - MedicalDoctorJumpsuit
+  - MedicalDoctorJumpskirt
+  - SeniorPhysicianJumpsuit
+  - SeniorPhysicianJumpskirt
+  - MedicalBlueScrubs
+  - MedicalGreenScrubs
+  - MedicalPurpleScrubs
+
+- type: loadoutGroup
+  id: MedicalDoctorOuterClothing
+  name: loadout-group-medical-doctor-outerclothing
+  minLimit: 0
+  loadouts:
+  - RegularLabCoat
+  - SeniorPhysicianLabCoat
+
+- type: loadoutGroup
+  id: MedicalBackpack
+  name: loadout-group-medical-doctor-backpack
+  loadouts:
+  - MedicalDoctorBackpack
+  - MedicalDoctorSatchel
+  - MedicalDoctorDuffel
+  - SeniorPhysicianBackpack
+  - SeniorPhysicianSatchel
+  - SeniorPhysicianDuffel
+
+- type: loadoutGroup
+  id: MedicalDoctorPDA
+  name: loadout-group-medical-doctor-id
+  loadouts:
+  - MedicalDoctorPDA
+  - SeniorPhysicianPDA
+
+- type: loadoutGroup
+  id: MedicalInternJumpsuit
+  name: loadout-group-medical-intern-jumpsuit
+  loadouts:
+  - WhiteJumpsuit
+  - WhiteJumpskirt
+
+- type: loadoutGroup
+  id: ChemistJumpsuit
+  name: loadout-group-chemist-jumpsuit
+  loadouts:
+  - ChemistJumpsuit
+  - ChemistJumpskirt
+
+- type: loadoutGroup
+  id: ChemistOuterClothing
+  name: loadout-group-chemist-outerclothing
+  minLimit: 0
+  loadouts:
+  - RegularLabCoat
+  - ChemistLabCoat
+
+- type: loadoutGroup
+  id: ChemistBackpack
+  name: loadout-group-chemist-backpack
+  loadouts:
+  - ChemistBackpack
+  - ChemistSatchel
+  - ChemistDuffel
+
+- type: loadoutGroup
+  id: ParamedicHead
+  name: loadout-group-paramedic-head
+  minLimit: 0
+  loadouts:
+  - ParamedicHead
+
+- type: loadoutGroup
+  id: ParamedicJumpsuit
+  name: loadout-group-paramedic-jumpsuit
+  loadouts:
+  - ParamedicJumpsuit
+  - ParamedicJumpskirt
+
+- type: loadoutGroup
+  id: ParamedicOuterClothing
+  name: loadout-group-paramedic-outerclothing
+  minLimit: 0
+  loadouts:
+  - ParamedicWindbreaker
+
+- type: loadoutGroup
+  id: ParamedicBackpack
+  name: loadout-group-paramedic-backpack
+  loadouts:
+  - ParamedicBackpack
+  - ParamedicSatchel
+  - ParamedicDuffel
+
+# Wildcards
+- type: loadoutGroup
+  id: ReporterJumpsuit
+  name: loadout-group-reporter-jumpsuit
+  loadouts:
+  - ReporterJumpsuit
+  - JournalistJumpsuit
+
+- type: loadoutGroup
+  id: BoxerJumpsuit
+  name: loadout-group-boxer-jumpsuit
+  loadouts:
+  - BoxerShorts
+  - BoxerShortsWithTop
+
+- type: loadoutGroup
+  id: BoxerGloves
+  name: loadout-group-boxer-gloves
+  loadouts:
+  - RedBoxingGloves
+  - BlueBoxingGloves
+  - GreenBoxingGloves
+  - YellowBoxingGloves
diff --git a/Resources/Prototypes/Loadouts/role_loadouts.yml b/Resources/Prototypes/Loadouts/role_loadouts.yml
new file mode 100644 (file)
index 0000000..d6bdebf
--- /dev/null
@@ -0,0 +1,327 @@
+# Command
+- type: roleLoadout
+  id: JobCaptain
+  groups:
+  - CaptainHead
+  - CaptainNeck
+  - CaptainJumpsuit
+  - CaptainBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobHeadOfPersonnel
+  groups:
+  - HoPHead
+  - HoPNeck
+  - HoPJumpsuit
+  - HoPBackpack
+  - Trinkets
+
+# Civilian
+- type: roleLoadout
+  id: JobPassenger
+  groups:
+  - PassengerJumpsuit
+  - CommonBackpack
+  - PassengerFace
+  - PassengerGloves
+  - Trinkets
+
+- type: roleLoadout
+  id: JobBartender
+  groups:
+  - BartenderHead
+  - BartenderJumpsuit
+  - CommonBackpack
+  - BartenderOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobServiceWorker
+  groups:
+  - BartenderJumpsuit
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobChef
+  groups:
+  - ChefHead
+  - ChefMask
+  - ChefJumpsuit
+  - CommonBackpack
+  - ChefOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobLibrarian
+  groups:
+  - LibrarianJumpsuit
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobLawyer
+  groups:
+  - LawyerNeck
+  - LawyerJumpsuit
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobChaplain
+  groups:
+  - ChaplainHead
+  - ChaplainMask
+  - ChaplainNeck
+  - ChaplainJumpsuit
+  - ChaplainBackpack
+  - ChaplainOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobJanitor
+  groups:
+  - JanitorHead
+  - JanitorJumpsuit
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobBotanist
+  groups:
+  - BotanistHead
+  - BotanistJumpsuit
+  - BotanistBackpack
+  - BotanistOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobClown
+  groups:
+  - ClownHead
+  - ClownJumpsuit
+  - ClownBackpack
+  - ClownShoes
+  - Trinkets
+
+- type: roleLoadout
+  id: JobMime
+  groups:
+  - MimeHead
+  - MimeMask
+  - MimeJumpsuit
+  - MimeBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobMusician
+  groups:
+  - MusicianBackpack
+  - Trinkets
+
+# Cargo
+- type: roleLoadout
+  id: JobQuartermaster
+  groups:
+  - QuartermasterHead
+  - QuartermasterNeck
+  - QuartermasterJumpsuit
+  - QuartermasterBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobCargoTechnician
+  groups:
+  - CargoTechnicianHead
+  - CargoTechnicianJumpsuit
+  - CargoTechnicianBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobSalvageSpecialist
+  groups:
+  - SalvageSpecialistBackpack
+  - Trinkets
+
+# Engineering
+- type: roleLoadout
+  id: JobChiefEngineer
+  groups:
+  - ChiefEngineerHead
+  - ChiefEngineerJumpsuit
+  - ChiefEngineerBackpack
+  - ChiefEngineerNeck
+  - Trinkets
+
+- type: roleLoadout
+  id: JobTechnicalAssistant
+  groups:
+  - TechnicalAssistantJumpsuit
+  - StationEngineerBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobStationEngineer
+  groups:
+  - StationEngineerHead
+  - StationEngineerJumpsuit
+  - StationEngineerBackpack
+  - StationEngineerOuterClothing
+  - StationEngineerID
+  - Trinkets
+
+- type: roleLoadout
+  id: JobAtmosphericTechnician
+  groups:
+  - AtmosphericTechnicianJumpsuit
+  - AtmosphericTechnicianBackpack
+  - Trinkets
+
+# Science
+- type: roleLoadout
+  id: JobResearchDirector
+  groups:
+  - ResearchDirectorHead
+  - ResearchDirectorNeck
+  - ResearchDirectorJumpsuit
+  - ResearchDirectorBackpack
+  - ResearchDirectorOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobScientist
+  groups:
+  - ScientistHead
+  - ScientistNeck
+  - ScientistJumpsuit
+  - ScientistBackpack
+  - ScientistOuterClothing
+  - ScientistPDA
+  - Trinkets
+
+- type: roleLoadout
+  id: JobResearchAssistant
+  groups:
+  - ResearchAssistantJumpsuit
+  - ScientistBackpack
+  - Trinkets
+
+# Security
+- type: roleLoadout
+  id: JobHeadOfSecurity
+  groups:
+  - HeadofSecurityHead
+  - HeadofSecurityNeck
+  - HeadofSecurityJumpsuit
+  - SecurityBackpack
+  - HeadofSecurityOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobWarden
+  groups:
+  - WardenHead
+  - WardenJumpsuit
+  - SecurityBackpack
+  - WardenOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobSecurityOfficer
+  groups:
+  - SecurityHead
+  - SecurityJumpsuit
+  - SecurityBackpack
+  - SecurityPDA
+  - Trinkets
+
+- type: roleLoadout
+  id: JobDetective
+  groups:
+  - DetectiveHead
+  - DetectiveNeck
+  - DetectiveJumpsuit
+  - DetectiveBackpack
+  - DetectiveOuterClothing
+  - Trinkets
+
+- type: roleLoadout
+  id: JobSecurityCadet
+  groups:
+  - SecurityCadetJumpsuit
+  - SecurityBackpack
+  - Trinkets
+
+# Medical
+- type: roleLoadout
+  id: JobChiefMedicalOfficer
+  groups:
+  - ChiefMedicalOfficerHead
+  - ChiefMedicalOfficerJumpsuit
+  - ChiefMedicalOfficerOuterClothing
+  - ChiefMedicalOfficerBackpack
+  - ChiefMedicalOfficerNeck
+  - Trinkets
+
+- type: roleLoadout
+  id: JobMedicalDoctor
+  groups:
+  - MedicalDoctorHead
+  - MedicalDoctorJumpsuit
+  - MedicalBackpack
+  - MedicalDoctorOuterClothing
+  - MedicalDoctorPDA
+  - Trinkets
+
+- type: roleLoadout
+  id: JobMedicalIntern
+  groups:
+  - MedicalInternJumpsuit
+  - MedicalBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobChemist
+  groups:
+  - ChemistJumpsuit
+  - ChemistOuterClothing
+  - ChemistBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobParamedic
+  groups:
+  - ParamedicHead
+  - ParamedicJumpsuit
+  - ParamedicOuterClothing
+  - ParamedicBackpack
+  - Trinkets
+
+# Wildcards
+- type: roleLoadout
+  id: JobZookeeper
+  groups:
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobReporter
+  groups:
+  - ReporterJumpsuit
+  - CommonBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobPsychologist
+  groups:
+  - MedicalBackpack
+  - Trinkets
+
+- type: roleLoadout
+  id: JobBoxer
+  groups:
+  - BoxerJumpsuit
+  - BoxerGloves
+  - CommonBackpack
+  - Trinkets
index 729d8c27e9487e7f1537ae66ea5175e04ef86c98..235ccfdab86f29089a049b8b29300b325d297536 100644 (file)
@@ -8,9 +8,6 @@
     id: PiratePDA
     belt: ClothingBeltUtility
     pocket1: AppraisalTool
-  innerClothingSkirt: ClothingUniformJumpsuitPirate
-  satchel: ClothingBackpackPirateFilled
-  duffelbag: ClothingBackpackPirateFilled
 
 - type: startingGear
   id: PirateCaptainGear
@@ -24,9 +21,6 @@
     pocket1: AppraisalTool
     pocket2: EnergyCutlass
     outerClothing: ClothingOuterCoatPirate
-  innerClothingSkirt: ClothingUniformJumpskirtColorLightBrown
-  satchel: ClothingBackpackPirateFilled
-  duffelbag: ClothingBackpackPirateFilled
 
 - type: startingGear
   id: PirateFirstmateGear
@@ -39,6 +33,3 @@
     belt: ClothingBeltUtility
     pocket1: AppraisalTool
     outerClothing: ClothingOuterCoatGentle
-  innerClothingSkirt: ClothingUniformJumpsuitPirate
-  satchel: ClothingBackpackPirateFilled
-  duffelbag: ClothingBackpackPirateFilled
index 180e7c5e92a71107639764865658134553f07136..ec618db9c97a198c1bf06764d1b9b28b03594d99 100644 (file)
 - type: startingGear
   id: CargoTechGear
   equipment:
-    head: ClothingHeadHatCargosoft
-    jumpsuit: ClothingUniformJumpsuitCargo
-    back: ClothingBackpackCargoFilled
     shoes: ClothingShoesColorBlack
     id: CargoPDA
     ears: ClothingHeadsetCargo
-    pocket1: AppraisalTool
-  innerClothingSkirt: ClothingUniformJumpskirtCargo
-  satchel: ClothingBackpackSatchelCargoFilled
-  duffelbag: ClothingBackpackDuffelCargoFilled
+    pocket1: AppraisalTool
\ No newline at end of file
index 9908e9b799f8bcccb94459d620a14b977edd9b56..93f4562aed4fb5bd542f0f99ec32685a50216fc4 100644 (file)
 - type: startingGear
   id: QuartermasterGear
   equipment:
-    head: ClothingHeadHatQMsoft
-    jumpsuit: ClothingUniformJumpsuitQM
-    back: ClothingBackpackQuartermasterFilled
     shoes: ClothingShoesColorBrown
     id: QuartermasterPDA
     ears: ClothingHeadsetQM
     belt: BoxFolderClipboard
-    pocket1: AppraisalTool
-  innerClothingSkirt: ClothingUniformJumpskirtQM
-  satchel: ClothingBackpackSatchelQuartermasterFilled
-  duffelbag: ClothingBackpackDuffelQuartermasterFilled
+    pocket1: AppraisalTool
\ No newline at end of file
index 924357bf53b6af9c1bb468f2854e10f3c85e553a..ad52c56064cf5aebcc09d51d07f5fb89a9b3e4ed 100644 (file)
@@ -22,9 +22,6 @@
   id: SalvageSpecialistGear
   equipment:
     jumpsuit: ClothingUniformJumpsuitSalvageSpecialist
-    back: ClothingBackpackSalvageFilled
     shoes: ClothingShoesBootsSalvage
     id: SalvagePDA
-    ears: ClothingHeadsetCargo
-  satchel: ClothingBackpackSatchelSalvageFilled
-  duffelbag: ClothingBackpackDuffelSalvageFilled
+    ears: ClothingHeadsetCargo
\ No newline at end of file
index 5cf4fd9449bb85edb4085ca8a6324cb28b85633d..1fd5665093c1a8e23c58f78cb81a0e61e143becb 100644 (file)
 - type: startingGear
   id: PassengerGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitColorGrey
-    back: ClothingBackpackFilled
     shoes: ClothingShoesColorBlack
     id: PassengerPDA
     ears: ClothingHeadsetGrey
-  innerClothingSkirt: ClothingUniformJumpskirtColorGrey
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index 5ef2008d1ad56377f84fdeeea59d7782d24e5e44..a49e3cef7facad27b19bf8711ae6e083eeaeb93b 100644 (file)
 - type: startingGear
   id: BartenderGear
   equipment:
-    head: ClothingHeadHatTophat
-    jumpsuit: ClothingUniformJumpsuitBartender
-    outerClothing: ClothingOuterVest
-    back: ClothingBackpackFilled
     shoes: ClothingShoesColorBlack
     id: BartenderPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtBartender
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index 35b858fb388ac8a27d59fc91e5b5eb6c3506ea39..feed9e7e6bf6c469603edab7221d0825fd496360 100644 (file)
 - type: startingGear
   id: BotanistGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitHydroponics
-    back: ClothingBackpackHydroponicsFilled
     shoes: ClothingShoesColorBrown
     id: BotanistPDA
     ears: ClothingHeadsetService
-    outerClothing: ClothingOuterApronBotanist
     belt: ClothingBeltPlantFilled
-  innerClothingSkirt: ClothingUniformJumpskirtHydroponics
-  satchel: ClothingBackpackSatchelHydroponicsFilled
-  duffelbag: ClothingBackpackDuffelHydroponicsFilled
index 647a54c9e26ecf524a16b5f7e20e1da08c849f0a..e3dc1cb23db452384debecc79c8b0b656d12a3de 100644 (file)
 - type: startingGear
   id: ChaplainGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitChaplain
-    back: ClothingBackpackChaplainFilled
     shoes: ClothingShoesColorBlack
     id: ChaplainPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtChaplain
-  satchel: ClothingBackpackSatchelChaplainFilled
-  duffelbag: ClothingBackpackDuffelChaplainFilled
index 6af0753d0326c37e81ebef2ff7e14228f5d2212b..1dda98bb37d4de7c2c7c5339d45e4b9fed6eeb39 100644 (file)
 - type: startingGear
   id: ChefGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitChef
-    head: ClothingHeadHatChef
-    back: ClothingBackpackFilled
-    mask: ClothingMaskItalianMoustache
     shoes: ClothingShoesColorBlack
     id: ChefPDA
     ears: ClothingHeadsetService
-    outerClothing: ClothingOuterApronChef
     belt: ClothingBeltChefFilled
-  innerClothingSkirt: ClothingUniformJumpskirtChef
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index 75c2ce352472d4e707e1d93110aa53f88f247c99..c70b4a99435dba03bc0d8f191a431cad51568a55 100644 (file)
 - type: startingGear
   id: ClownGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitClown
-    back: ClothingBackpackClownFilled
-    shoes: ClothingShoesClown
     mask: ClothingMaskClown
     pocket1: BikeHorn
     pocket2: ClownRecorder
     id: ClownPDA
-    ears: ClothingHeadsetService
-  satchel: ClothingBackpackSatchelClownFilled
-  duffelbag: ClothingBackpackDuffelClownFilled
+    ears: ClothingHeadsetService
\ No newline at end of file
index bf11532ddbf3596d67795e889ca3581b04038f6e..39d7ef7c75a56c7245c951bbcbb4daf84c3cf3cc 100644 (file)
 - type: startingGear
   id: JanitorGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitJanitor
-    back: ClothingBackpackFilled
     shoes: ClothingShoesGaloshes
-    head: ClothingHeadHatPurplesoft
     id: JanitorPDA
     gloves: ClothingHandsGlovesJanitor
     ears: ClothingHeadsetService
     belt: ClothingBeltJanitorFilled
-  innerClothingSkirt: ClothingUniformJumpskirtJanitor
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 - type: startingGear
   id: JanitorMaidGear
   equipment:
     jumpsuit: ClothingUniformJumpskirtJanimaid
-    back: ClothingBackpackFilled
     id: JanitorPDA
     gloves: ClothingHandsGlovesJanitor
     head: ClothingHeadHatCatEars
     ears: ClothingHeadsetService
     belt: ClothingBeltJanitorFilled
-  innerClothingSkirt: ClothingUniformJumpskirtJanimaid
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index 43561d4db429aff804074efa57693ef42fd94bd7..1a321aff400a5534ec1b37c7ea15a6207e3cf595 100644 (file)
 - type: startingGear
   id: LawyerGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitLawyerBlack  # TODO change jumpsuit to randomiser of the 4 variants # ignoring this since loadouts are gonna be out soon
-    back: ClothingBackpackLawyerFilled 
     shoes: ClothingShoesBootsLaceup
     id: LawyerPDA
     ears: ClothingHeadsetSecurity
     # TODO add copy of space law
   inhand:
     - BriefcaseBrownFilled
-  innerClothingSkirt: ClothingUniformJumpskirtLawyerBlack
-  satchel: ClothingBackpackSatchelLawyerFilled
-  duffelbag: ClothingBackpackDuffelLawyerFilled
index 02c26b2e9caa82fa820b63ed1d6e4014fa46fd05..909d8da19daa33e7cf71ca4c3c841243f93b6cb5 100644 (file)
 - type: startingGear
   id: LibrarianGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitLibrarian
-    back: ClothingBackpackLibrarianFilled
     shoes: ClothingShoesBootsLaceup
     id: LibrarianPDA
     ears: ClothingHeadsetService
     pocket1: d10Dice
     pocket2: HandLabeler # for making named bestsellers
-  innerClothingSkirt: ClothingUniformJumpskirtLibrarian
-  satchel: ClothingBackpackSatchelLibrarianFilled
-  duffelbag: ClothingBackpackDuffelLibrarianFilled
index c22accaf2153377222cc664c38b2fbbbf6fa643c..1eac292eee3af30b0dee350316ab1c64941621c2 100644 (file)
 - type: startingGear
   id: MimeGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitMime
-    back: ClothingBackpackMimeFilled
-    head: ClothingHeadHatBeret
     belt: ClothingBeltSuspenders
-    gloves: ClothingHandsGlovesLatex
+    gloves: ClothingHandsGlovesColorWhite
     shoes: ClothingShoesColorWhite
     pocket1: CrayonMime
     pocket2: Paper
-    mask: ClothingMaskMime
     id: MimePDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtMime
-  satchel: ClothingBackpackSatchelMimeFilled
-  duffelbag: ClothingBackpackDuffelMimeFilled
 
 - type: entity
   id: ActionMimeInvisibleWall
index f50825945a3f995aa57968bb507490278d310126..1ba1e998f68bfa0b7acfe8eb925b6b03379a8da3 100644 (file)
   id: MusicianGear
   equipment:
     jumpsuit: ClothingUniformJumpsuitMusician
-    back: ClothingBackpackMusicianFilled
     eyes: ClothingEyesGlassesSunglasses
     shoes: ClothingShoesBootsLaceup
     id: MusicianPDA
-    ears: ClothingHeadsetService
-  satchel: ClothingBackpackSatchelMusicianFilled
-  duffelbag: ClothingBackpackDuffelMusicianFilled
+    ears: ClothingHeadsetService
\ No newline at end of file
index 7b60f3f7f74035a09dfc4eee655f1651455d052b..db2382667371ddb0b0be9dead4e37079235f3bd1 100644 (file)
 - type: startingGear
   id: ServiceWorkerGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitBartender
-    back: ClothingBackpackFilled
     shoes: ClothingShoesColorBlack
     id: ServiceWorkerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtBartender
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index b72a7186973f81aad34bd9b72bbc9869f15ef06e..9f018bf4f69ba2be91535dd7a5969b2f64c05279 100644 (file)
 - type: startingGear
   id: CaptainGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitCaptain
-    back: ClothingBackpackCaptainFilled
     shoes: ClothingShoesBootsLaceup
-    head: ClothingHeadHatCaptain
     eyes: ClothingEyesGlassesSunglasses
     gloves: ClothingHandsGlovesCaptain
     outerClothing: ClothingOuterArmorCaptainCarapace
     id: CaptainPDA
     ears: ClothingHeadsetAltCommand
-  innerClothingSkirt: ClothingUniformJumpskirtCaptain
-  satchel: ClothingBackpackSatchelCaptainFilled
-  duffelbag: ClothingBackpackDuffelCaptainFilled
index 0a934a7caa223a0fe60a6f7071ded8870e024a01..5f119b6434697e653759189bd7cd5bf82945da52 100644 (file)
 - type: startingGear
   id: HoPGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitHoP
-    back: ClothingBackpackHOPFilled
     shoes: ClothingShoesColorBrown
-    head: ClothingHeadHatHopcap
     id: HoPPDA
     gloves: ClothingHandsGlovesHop
     ears: ClothingHeadsetAltCommand
     belt: BoxFolderClipboard
-  innerClothingSkirt: ClothingUniformJumpskirtHoP
-  satchel: ClothingBackpackSatchelHOPFilled
-  duffelbag: ClothingBackpackDuffelHOPFilled
index 582b91663055ba4db4ff3150b8d79d5e018861db..5d5da684ac0902662e4d0ab39be736083092b3fc 100644 (file)
 - type: startingGear
   id: AtmosphericTechnicianGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitAtmos
-    back: ClothingBackpackAtmosphericsFilled
     shoes: ClothingShoesColorWhite
     eyes: ClothingEyesGlassesMeson
     id: AtmosPDA
     belt: ClothingBeltUtilityEngineering
     ears: ClothingHeadsetEngineering
-  innerClothingSkirt: ClothingUniformJumpskirtAtmos
-  satchel: ClothingBackpackSatchelAtmosphericsFilled
-  duffelbag: ClothingBackpackDuffelAtmosphericsFilled
index 726f715e25516cabb697c68ff5db37e5b4cf97a8..6bca6501dbe50e4a1e5ad2226159af430bb85ee3 100644 (file)
 - type: startingGear
   id: ChiefEngineerGear
   equipment:
-    head: ClothingHeadHatHardhatWhite
-    jumpsuit: ClothingUniformJumpsuitChiefEngineer
-    back: ClothingBackpackChiefEngineerFilled
     shoes: ClothingShoesColorBrown
     id: CEPDA
     eyes: ClothingEyesGlassesMeson
     ears: ClothingHeadsetCE
     belt: ClothingBeltUtilityEngineering
-  innerClothingSkirt: ClothingUniformJumpskirtChiefEngineer
-  satchel: ClothingBackpackSatchelChiefEngineerFilled
-  duffelbag: ClothingBackpackDuffelChiefEngineerFilled
index aa2adf09423fa7adedcf2caa52c911deee4d1d54..b93752d39ae2bf195986786c1514aba1b4883585 100644 (file)
 - type: startingGear
   id: StationEngineerGear
   equipment:
-    head: ClothingHeadHatHardhatYellow
-    jumpsuit: ClothingUniformJumpsuitEngineering
-    back: ClothingBackpackEngineeringFilled
     shoes: ClothingShoesBootsWork
-    outerClothing: ClothingOuterVestHazard
-    id: EngineerPDA
     eyes: ClothingEyesGlassesMeson
     belt: ClothingBeltUtilityEngineering
     ears: ClothingHeadsetEngineering
-  innerClothingSkirt: ClothingUniformJumpskirtEngineering
-  satchel: ClothingBackpackSatchelEngineeringFilled
-  duffelbag: ClothingBackpackDuffelEngineeringFilled
index e5a1d80456b62a2cd611719be5cbcc1c60d11581..f3b21e644442001845f94a57cb898f6ce2d352ab 100644 (file)
 - type: startingGear
   id: TechnicalAssistantGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitColorYellow
-    back: ClothingBackpackEngineeringFilled
     shoes: ClothingShoesBootsWork
     id: TechnicalAssistantPDA
     belt: ClothingBeltUtilityEngineering
     ears: ClothingHeadsetEngineering
-    pocket2: BookEngineersHandbook
-  innerClothingSkirt: ClothingUniformJumpskirtColorYellow
-  satchel: ClothingBackpackSatchelEngineeringFilled
-  duffelbag: ClothingBackpackDuffelEngineeringFilled
+    pocket2: BookEngineersHandbook
\ No newline at end of file
index 6c97d377995f5d12f40841b42ccc15bfc38b98c5..8748367830b4bc40ee6618087a4feabc75fffb81 100644 (file)
@@ -10,9 +10,6 @@
     shoes: ClothingShoesCult
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorBlack
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 - type: startingGear
   id: CultistGear
@@ -24,6 +21,3 @@
     shoes: ClothingShoesColorRed
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorBlack
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index df44dc180c74cf0ee0312809b872aeedc700f896..8388cec302ce8f813368352665159b856420faf9 100644 (file)
@@ -13,9 +13,6 @@
     shoes: ClothingShoesBootsJack
     id: PassengerPDA
     ears: ClothingHeadsetGrey
-  innerClothingSkirt: ClothingUniformJumpskirtColorBlack
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 #Space Ninja Outfit
 - type: startingGear
@@ -56,7 +53,6 @@
     pocket1: EnergySword
     pocket2: EnergyShield
     belt: ClothingBeltMilitaryWebbingMedFilled
-  innerClothingSkirt: ClothingUniformJumpskirtColorBlack
 
 # Syndicate Operative Outfit - Monkey
 - type: startingGear
@@ -65,7 +61,6 @@
     head: ClothingHeadHatOutlawHat
     jumpsuit: ClothingUniformJumpsuitOperative
     mask: CigaretteSyndicate
-  innerClothingSkirt: ClothingUniformJumpsuitOperative
 
 # Syndicate Operative Outfit - Barratry
 - type: startingGear
@@ -75,9 +70,6 @@
     back: ClothingBackpackDuffelSyndicateOperative
     shoes: ClothingShoesBootsCombatFilled
     gloves: ClothingHandsGlovesColorBlack
-  innerClothingSkirt: ClothingUniformJumpsuitOperative
-  satchel: ClothingBackpackDuffelSyndicateOperative
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 #Syndicate Operative Outfit - Basic
 - type: startingGear
@@ -90,9 +82,6 @@
     shoes: ClothingShoesBootsCombatFilled
     pocket1: BaseUplinkRadio40TC
     id: SyndiPDA
-  innerClothingSkirt: ClothingUniformJumpsuitOperative
-  satchel: ClothingBackpackDuffelSyndicateOperative
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 #Syndicate Operative Outfit - Full Kit
 - type: startingGear
     pocket1: DoubleEmergencyOxygenTankFilled
     pocket2: BaseUplinkRadio40TC
     belt: ClothingBeltMilitaryWebbing
-  innerClothingSkirt: ClothingUniformJumpskirtOperative
-  satchel: ClothingBackpackDuffelSyndicateOperative
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 #Nuclear Operative Commander Gear
 - type: startingGear
     belt: ClothingBeltMilitaryWebbing
   inhand:
     - NukeOpsDeclarationOfWar
-  innerClothingSkirt: ClothingUniformJumpskirtOperative
-  satchel: ClothingBackpackDuffelSyndicateOperative
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 #Nuclear Operative Medic Gear
 - type: startingGear
     pocket1: DoubleEmergencyOxygenTankFilled
     pocket2: BaseUplinkRadio40TC
     belt: ClothingBeltMilitaryWebbingMedFilled
-  innerClothingSkirt: ClothingUniformJumpskirtOperative
-  satchel: ClothingBackpackDuffelSyndicateOperativeMedic
-  duffelbag: ClothingBackpackDuffelSyndicateOperativeMedic
 
 #Syndicate Lone Operative Outfit - Full Kit
 - type: startingGear
     pocket1: DoubleEmergencyOxygenTankFilled
     pocket2: BaseUplinkRadio60TC
     belt: ClothingBeltMilitaryWebbing
-  innerClothingSkirt: ClothingUniformJumpskirtOperative
-  satchel: ClothingBackpackDuffelSyndicateOperative
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 # Syndicate Footsoldier Gear
 - type: startingGear
     back: ClothingBackpackFilled
     shoes: ClothingShoesBootsCombat
     id: SyndiPDA
-  innerClothingSkirt: ClothingUniformJumpsuitOperative
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 # Syndicate Footsoldier Gear - No Headset
 - type: startingGear
     back: ClothingBackpackFilled
     shoes: ClothingShoesBootsCombat
     id: SyndiPDA
-  innerClothingSkirt: ClothingUniformJumpsuitOperative
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelSyndicateOperative
 
 # Nanotrasen Paramilitary Unit Gear
 - type: startingGear
     outerClothing: ClothingOuterArmorBasicSlim
     ears: ClothingHeadsetSecurity
     gloves: ClothingHandsGlovesCombat
-  innerClothingSkirt: ClothingUniformJumpskirtSec
-  satchel: ClothingBackpackSatchelSecurityFilled
-  duffelbag: ClothingBackpackDuffelSecurityFilled
 
 #CBURN Unit Gear - Full Kit
 - type: startingGear
     pocket2: WeaponLaserGun
     suitstorage: OxygenTankFilled
     belt: ClothingBeltBandolier
-  innerClothingSkirt: ClothingUniformJumpsuitColorBrown
-  satchel: ClothingBackpackDuffelCBURNFilled
-  duffelbag: ClothingBackpackDuffelCBURNFilled
 
 - type: startingGear
   id: BoxingKangarooGear
   equipment:
     jumpsuit: ClothingUniformJumpsuitColorGrey
     shoes: ClothingShoesColorBlack
-  innerClothingSkirt: ClothingUniformJumpskirtColorGrey
 
 # DeathMatch Gear
 
     shoes: ClothingShoesBootsJack
     ears: ClothingHeadsetGrey
     gloves: ClothingHandsGlovesFingerless
-  innerClothingSkirt: ClothingUniformJumpskirtColorWhite
   inhand:
     - WeaponMeleeToolboxRobust
 
     ears: ClothingHeadsetBrigmedic
     mask: ClothingMaskBreathMedicalSecurity
     belt: ClothingBeltMedicalFilled
-  innerClothingSkirt: ClothingUniformJumpskirtBrigmedic
-  satchel: ClothingBackpackSatchelBrigmedicFilled
-  duffelbag: ClothingBackpackDuffelBrigmedicFilled
 
 # Aghost
 - type: startingGear
index 9f32796073a4ca04ac7c3e436e142f0f4354741c..596e42776b7c88527c6603ccaad72c3e92a87d52 100644 (file)
@@ -9,9 +9,6 @@
     shoes: ClothingShoesWizard
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorDarkBlue
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 - type: startingGear
   id: WizardRedGear
@@ -23,9 +20,6 @@
     shoes: ClothingShoesWizard
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorRed
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 - type: startingGear
   id: WizardVioletGear
@@ -37,9 +31,6 @@
     shoes: ClothingShoesWizard
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorPurple
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
 
 - type: startingGear
   id: WizardHardsuitGear
@@ -50,6 +41,3 @@
     shoes: ClothingShoesWizard
     id: PassengerPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpskirtColorPurple
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index bfa34d30a6448d0d12d777341b4788f1f0c50258..8e8d53d19d92c57ee9df9b5ff05394c25f4a0b06 100644 (file)
 - type: startingGear
   id: ChemistGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitChemistry
     back: ClothingBackpackChemistryFilled
     shoes: ClothingShoesColorWhite
-    outerClothing: ClothingOuterCoatLabChem
     id: ChemistryPDA
     ears: ClothingHeadsetMedical
     belt: ChemBag
     pocket1: HandLabeler
-    # the purple glasses?
-  innerClothingSkirt: ClothingUniformJumpskirtChemistry
-  satchel: ClothingBackpackSatchelChemistryFilled
-  duffelbag: ClothingBackpackDuffelChemistryFilled
+    # the purple glasses?
\ No newline at end of file
index 63cc527c9d13365fed68082abce73c54ca1af2ea..38008c21d59c66f5cb9bbe080956240567b5d3bb 100644 (file)
 - type: startingGear
   id: CMOGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitCMO
-    back: ClothingBackpackCMOFilled
     shoes: ClothingShoesColorBrown
-    outerClothing: ClothingOuterCoatLabCmo
     id: CMOPDA
     ears: ClothingHeadsetCMO
     belt: ClothingBeltMedicalFilled
-  innerClothingSkirt: ClothingUniformJumpskirtCMO
-  satchel: ClothingBackpackSatchelCMOFilled
-  duffelbag: ClothingBackpackDuffelCMOFilled
index 0c70272ccd49b22096c7533804fee43480bd26b9..2f61950360e1abb3db26607ed4649df0367b5999 100644 (file)
 - type: startingGear
   id: DoctorGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitMedicalDoctor
-    back: ClothingBackpackMedicalFilled
     shoes: ClothingShoesColorWhite
-    outerClothing: ClothingOuterCoatLab
-    id: MedicalPDA
     ears: ClothingHeadsetMedical
     belt: ClothingBeltMedicalFilled
-  innerClothingSkirt: ClothingUniformJumpskirtMedicalDoctor
-  satchel: ClothingBackpackSatchelMedicalFilled
-  duffelbag: ClothingBackpackDuffelMedicalFilled
index 3bdd19b14a44fca482e114dda588bfa7e7edab32..199645d5c380924f6733da6aa2d641a6f104b1ab 100644 (file)
 - type: startingGear
   id: MedicalInternGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitColorWhite
-    back: ClothingBackpackMedicalFilled
     shoes: ClothingShoesColorWhite
     id: MedicalInternPDA
     ears: ClothingHeadsetMedical
     belt: ClothingBeltMedicalFilled
-    pocket2: BookMedicalReferenceBook
-  innerClothingSkirt: ClothingUniformJumpskirtColorWhite
-  satchel: ClothingBackpackSatchelMedicalFilled
-  duffelbag: ClothingBackpackDuffelMedicalFilled
+    pocket2: BookMedicalReferenceBook
\ No newline at end of file
index 8aa7c7701232e358cde98048d8987d9c253ce1d1..678526c703ea11bbb7a9dc2e39c65648336d2f2f 100644 (file)
 - type: startingGear
   id: ParamedicGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitParamedic
-    back: ClothingBackpackParamedicFilled
     shoes: ClothingShoesColorBlue
     id: ParamedicPDA
     ears: ClothingHeadsetMedical
     belt: ClothingBeltMedicalEMTFilled
-  innerClothingSkirt: ClothingUniformJumpskirtParamedic
-  satchel: ClothingBackpackSatchelParamedicFilled
-  duffelbag: ClothingBackpackDuffelParamedicFilled
index d0865f86a5e149e6fadd1740936a3cfde46536ed..a360313d9ef8c981eff05d17cbf25cbd54741bb1 100644 (file)
 - type: startingGear
   id: ResearchAssistantGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitColorWhite
     back: ClothingBackpackScienceFilled
     shoes: ClothingShoesColorWhite
     id: ResearchAssistantPDA
     ears: ClothingHeadsetScience
     pocket2: BookScientistsGuidebook
-  innerClothingSkirt: ClothingUniformJumpskirtColorWhite
-  satchel: ClothingBackpackSatchelScienceFilled
-  duffelbag: ClothingBackpackDuffelScienceFilled
index db3624d4bda90a23c5d625c497fc62cb305cc3bd..9e6ba47b8882f54e72c95ab32071bcc405d59db6 100644 (file)
 - type: startingGear
   id: ResearchDirectorGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitResearchDirector
-    back: ClothingBackpackResearchDirectorFilled
     shoes: ClothingShoesColorBrown
-    outerClothing: ClothingOuterCoatRD
     id: RnDPDA
-    ears: ClothingHeadsetRD
-  innerClothingSkirt: ClothingUniformJumpskirtResearchDirector
-  satchel: ClothingBackpackSatchelResearchDirectorFilled
-  duffelbag: ClothingBackpackDuffelResearchDirectorFilled
+    ears: ClothingHeadsetRD
\ No newline at end of file
index bda5e84d22e57913fca43c087941589e97ba12d9..1d5c101251b17bb8e35441ba3744fcf1bb2b355e 100644 (file)
 - type: startingGear
   id: ScientistGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitScientist
-    back: ClothingBackpackScienceFilled
     shoes: ClothingShoesColorWhite
-    outerClothing: ClothingOuterCoatRnd
-    id: SciencePDA
     ears: ClothingHeadsetScience
-  innerClothingSkirt: ClothingUniformJumpskirtScientist
-  satchel: ClothingBackpackSatchelScienceFilled
-  duffelbag: ClothingBackpackDuffelScienceFilled
+
index 488410949a3a21b33d140b625764921b045a9501..5ec619265f925cdba17a76ef8bb53f5e792d86b5 100644 (file)
 - type: startingGear
   id: DetectiveGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitDetective
-    back: ClothingBackpackSecurityFilledDetective
     shoes: ClothingShoesBootsCombatFilled
     eyes: ClothingEyesGlassesSecurity
-    head: ClothingHeadHatFedoraBrown
-    outerClothing: ClothingOuterVestDetective
     id: DetectivePDA
     ears: ClothingHeadsetSecurity
     belt: ClothingBeltHolsterFilled
-  innerClothingSkirt: ClothingUniformJumpskirtDetective
-  satchel: ClothingBackpackSatchelSecurityFilledDetective
-  duffelbag: ClothingBackpackDuffelSecurityFilledDetective
index ef9a74fcb4ae67339d000b77aa0e8732df7f424b..f41c18d52390bc341423127f5a517b9b823ff530 100644 (file)
 - type: startingGear
   id: HoSGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitHoS
-    back: ClothingBackpackHOSFilled
     shoes: ClothingShoesBootsCombatFilled
-    outerClothing: ClothingOuterCoatHoSTrench
     eyes: ClothingEyesGlassesSecurity
-    head: ClothingHeadHatBeretHoS
     id: HoSPDA
     gloves: ClothingHandsGlovesCombat
     ears: ClothingHeadsetAltSecurity
     belt: ClothingBeltSecurityFilled
-    pocket1: WeaponPistolMk58
-  innerClothingSkirt: ClothingUniformJumpskirtHoS
-  satchel: ClothingBackpackSatchelHOSFilled
-  duffelbag: ClothingBackpackDuffelHOSFilled
+    pocket1: WeaponPistolMk58
\ No newline at end of file
index a1b0659384f26b94b841adaf313a13af03700494..17e09b232e98a5be6c24ca57d5c4e598598eef92 100644 (file)
 - type: startingGear
   id: SecurityCadetGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitColorRed
-    back: ClothingBackpackSecurityFilled
     shoes: ClothingShoesBootsCombatFilled
     outerClothing: ClothingOuterArmorBasic
     id: SecurityCadetPDA
     ears: ClothingHeadsetSecurity
     belt: ClothingBeltSecurityFilled
     pocket1: WeaponPistolMk58
-    pocket2: BookSecurity
-  innerClothingSkirt: ClothingUniformJumpskirtColorRed
-  satchel: ClothingBackpackSatchelSecurityFilled
-  duffelbag: ClothingBackpackDuffelSecurityFilled
+    pocket2: BookSecurity
\ No newline at end of file
index 83b439fcaba15d6e051d428058bbb0d08321f304..828df7f05cf46d9d726ee4a89b72f7f86dd02918 100644 (file)
 - type: startingGear
   id: SecurityOfficerGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitSec
-    back: ClothingBackpackSecurityFilled
     shoes: ClothingShoesBootsCombatFilled
     eyes: ClothingEyesGlassesSecurity
-    head: ClothingHeadHelmetBasic
     outerClothing: ClothingOuterArmorBasic
-    id: SecurityPDA
     ears: ClothingHeadsetSecurity
     belt: ClothingBeltSecurityFilled
     pocket1: WeaponPistolMk58
-  innerClothingSkirt: ClothingUniformJumpskirtSec
-  satchel: ClothingBackpackSatchelSecurityFilled
-  duffelbag: ClothingBackpackDuffelSecurityFilled
index dbefd46a6f72e89d39d4c7fc099ead7ee07b7323..5e4792ee8d206c0b71c89fa8db8306aa6085bcb8 100644 (file)
 - type: startingGear
   id: WardenGear
   equipment:
-    head: ClothingHeadHatWarden
-    jumpsuit: ClothingUniformJumpsuitWarden
-    back: ClothingBackpackSecurityFilled
     shoes: ClothingShoesBootsCombatFilled
     eyes: ClothingEyesGlassesSecurity
-    outerClothing: ClothingOuterCoatWarden
     id: WardenPDA
     ears: ClothingHeadsetSecurity
     belt: ClothingBeltSecurityFilled
-    pocket1: WeaponPistolMk58
-  innerClothingSkirt: ClothingUniformJumpskirtWarden
-  satchel: ClothingBackpackSatchelSecurityFilled
-  duffelbag: ClothingBackpackDuffelSecurityFilled
+    pocket1: WeaponPistolMk58
\ No newline at end of file
diff --git a/Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/nanotrasen.yml b/Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/nanotrasen.yml
deleted file mode 100644 (file)
index 765c9a7..0000000
+++ /dev/null
@@ -1,69 +0,0 @@
-#These are startingGear definitions for the currently yet=to-be-added Ship VS. Ship gamemode.
-#These are not gamemode-ready and are just here so that people know what their general outfit is supposed to look like.
-#For the love of god, please move these out of this file when the gamemode is actually added. They are currently all here for convienence's sake.
-
-#CREW
-#Recruit
-- type: startingGear
-  id: RecruitNTGear
-  equipment:
-    jumpsuit: ClothingUniformJumpsuitRecruitNT
-    back: ClothingBackpackFilled
-    shoes: ClothingShoesColorBlack
-    gloves: ClothingHandsGlovesColorBlack
-    id: PassengerPDA
-    ears: ClothingHeadsetGrey
-  innerClothingSkirt: ClothingUniformJumpsuitRecruitNT #Wearing a jumpskirt into combat is a little unfitting and silly, so there is no jumpskirt counterpart for any of the Ship VS. Ship suits.
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
-
-#Repairman
-- type: startingGear
-  id: RepairmanNTGear
-  equipment:
-    head: ClothingHeadHatHardhatYellow
-    jumpsuit: ClothingUniformJumpsuitRepairmanNT
-    back: ClothingBackpackEngineeringFilled
-    shoes: ClothingShoesBootsWork
-    gloves: ClothingHandsGlovesColorYellow #Should maybe still be in lockers - this is just so people know that they're there and a part of the outfit.
-    id: EngineerPDA
-    eyes: ClothingEyesGlassesMeson
-    belt: ClothingBeltUtilityEngineering
-    ears: ClothingHeadsetAltCommand #Should use the "alt" engineering headset sprite.
-  innerClothingSkirt: ClothingUniformJumpsuitRepairmanNT
-  satchel: ClothingBackpackSatchelEngineeringFilled
-  duffelbag: ClothingBackpackDuffelEngineeringFilled
-
-#Paramedic
-- type: startingGear
-  id: ParamedicNTGear
-  equipment:
-    jumpsuit: ClothingUniformJumpsuitParamedicNT
-    back: ClothingBackpackFilled #The medical backpack sprite looks way worse so this will do for now.
-    shoes: ClothingShoesColorBlue
-    id: MedicalPDA
-    ears: ClothingHeadsetMedical
-    eyes: ClothingEyesHudMedical
-    gloves: ClothingHandsGlovesLatex
-    belt: ClothingBeltMedicalFilled
-  innerClothingSkirt: ClothingUniformJumpskirtMedicalDoctor
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
-
-#HEADS OF STAFF
-#Chief Engineer
-- type: startingGear
-  id: ChiefEngineerNTGear
-  equipment:
-    head: ClothingHeadHatHardhatArmored
-    jumpsuit: ClothingUniformJumpsuitChiefEngineerNT
-    back: ClothingBackpackFilled #Again, the regular sprite here looks way worse than the regular backpack.
-    shoes: ClothingShoesBootsJack
-    gloves: ClothingHandsGlovesCombat
-    id: CEPDA
-    eyes: ClothingEyesGlassesMeson
-    ears: ClothingHeadsetAltCommand #Same as repairman - make this use the alt headset sprite.
-    belt: ClothingBeltUtilityEngineering
-  innerClothingSkirt: ClothingUniformJumpsuitChiefEngineerNT
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
diff --git a/Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/syndicate.yml b/Resources/Prototypes/Roles/Jobs/Ship_VS_Ship/syndicate.yml
deleted file mode 100644 (file)
index b7a4daf..0000000
+++ /dev/null
@@ -1,68 +0,0 @@
-#These are startingGear definitions for the currently yet=to-be-added Ship VS. Ship gamemode.
-#Refer to Nanotrasen.yml for additional comments.
-
-#CREW
-#Recruit
-- type: startingGear
-  id: RecruitSyndieGear
-  equipment:
-    jumpsuit: ClothingUniformJumpsuitRecruitSyndie
-    back: ClothingBackpackFilled
-    shoes: ClothingShoesColorBlack
-    gloves: ClothingHandsGlovesColorBlack
-    id: PassengerPDA
-    ears: ClothingHeadsetGrey
-  innerClothingSkirt: ClothingUniformJumpsuitRecruitSyndie #Wearing a jumpskirt into combat is a little unfitting and silly, so there is no jumpskirt counterpart for any of the Ship VS. Ship suits.
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
-
-#Repairman
-- type: startingGear
-  id: RepairmanSyndieGear
-  equipment:
-    head: ClothingHeadHatHardhatYellow
-    jumpsuit: ClothingUniformJumpsuitRepairmanSyndie
-    back: ClothingBackpackFilled #The regular industrial backpack looks really weird here, so I've opted for this instead for now. If a new one is never made, then make sure to make a prototype that has this with extended internals!
-    shoes: ClothingShoesBootsWork
-    gloves: ClothingHandsGlovesColorYellow #Should maybe still be in lockers - this is just so people know that they're there and a part of the outfit.
-    id: EngineerPDA
-    eyes: ClothingEyesGlassesMeson
-    belt: ClothingBeltUtilityEngineering
-    ears: ClothingHeadsetAltCommand #Should use the "alt" engineering headset sprite.
-  innerClothingSkirt: ClothingUniformJumpsuitRepairmanSyndie
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
-
-#Paramedic
-- type: startingGear
-  id: ParamedicSyndieGear
-  equipment:
-    jumpsuit: ClothingUniformJumpsuitParamedicSyndie
-    back: ClothingBackpackFilled #The default job backpack again looks way worse. Same case as the NT Paramedc and Syndicate repairman.
-    shoes: ClothingShoesColorRed
-    id: MedicalPDA
-    ears: ClothingHeadsetMedical
-    eyes: ClothingEyesHudMedical
-    gloves: ClothingHandsGlovesLatex
-    belt: ClothingBeltMedicalFilled
-  innerClothingSkirt: ClothingUniformJumpsuitParamedicSyndie
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
-
-#HEADS OF STAFF
-#Chief Engineer
-- type: startingGear
-  id: ChiefEngineerSyndieGear
-  equipment:
-    head: ClothingHeadHatHardhatArmored
-    jumpsuit: ClothingUniformJumpsuitChiefEngineerSyndie
-    back: ClothingBackpackFilled #In a running theme, the default station job backpack still continues to look strange in comparison to the regular one. It's not as bad as on the syndicate engineer here, though.
-    shoes: ClothingShoesBootsJack
-    gloves: ClothingHandsGlovesCombat
-    id: CEPDA
-    eyes: ClothingEyesGlassesMeson
-    ears: ClothingHeadsetAltCommand
-    belt: ClothingBeltUtilityEngineering
-  innerClothingSkirt: ClothingUniformJumpsuitChiefEngineerSyndie
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index d9fe88fc1422f42f39a47a0b14db5b54aec5288c..0f19323724a817c119e3416248be20c315a4f9d6 100644 (file)
 - type: startingGear
   id: BoxerGear
   equipment:
-    jumpsuit: UniformShortsRed
-    back: ClothingBackpackFilled
     id: BoxerPDA
     ears: ClothingHeadsetService
-    gloves: ClothingHandsGlovesBoxingRed
     shoes: ClothingShoesColorRed
     belt: ClothingBeltChampion
-  innerClothingSkirt: UniformShortsRedWithTop
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index 8861924cf6c6f4e2534f6903fa0e6037594f1e66..d1d07c008c8b84dc3773fd97240bcb9002ba69af 100644 (file)
   id: PsychologistGear
   equipment:
     jumpsuit: ClothingUniformJumpsuitPsychologist
-    back: ClothingBackpackMedicalFilled
     shoes: ClothingShoesLeather
     id: PsychologistPDA
     ears: ClothingHeadsetMedical
-  innerClothingSkirt: ClothingUniformJumpsuitPsychologist
-  satchel: ClothingBackpackSatchelMedicalFilled
-  duffelbag: ClothingBackpackDuffelMedicalFilled
index 60721273476cfd2d8b919da8b6fcafe9dd3588f3..d2d70d856a582e3d19a45e2aecb6a6e9eab91df8 100644 (file)
 - type: startingGear
   id: ReporterGear
   equipment:
-    jumpsuit: ClothingUniformJumpsuitReporter
-    back: ClothingBackpackFilled
     shoes: ClothingShoesColorWhite
     id: ReporterPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpsuitJournalist
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled
index bd8f5b85b6c893d6565e1faaecde218f73084bec..27e7fa65ff6f4e5948bcbf2ebe79acb662ae3477 100644 (file)
   id: ZookeeperGear
   equipment:
     jumpsuit: ClothingUniformJumpsuitSafari
-    back: ClothingBackpackFilled
     head: ClothingHeadSafari
     shoes: ClothingShoesColorWhite
     id: ZookeeperPDA
     ears: ClothingHeadsetService
-  innerClothingSkirt: ClothingUniformJumpsuitSafari
-  satchel: ClothingBackpackSatchelFilled
-  duffelbag: ClothingBackpackDuffelFilled