diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 84c5b75d1cb..81334cc762d 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -4,6 +4,7 @@
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:ui="clr-namespace:Content.Client.Lobby.UI"
xmlns:profile="clr-namespace:Content.Client.Lobby.UI.ProfileEditorControls"
+ xmlns:loadouts="clr-namespace:Content.Client._DEN.Lobby.UI.Loadouts"
HorizontalExpand="True">
@@ -130,6 +131,9 @@
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 3b444bf4162..c9034cd00dc 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -286,6 +286,12 @@ public HumanoidProfileEditor(
#endregion Markings
+ #region Den Loadouts
+
+ TabContainer.SetTabTitle(5, "Loadouts"); // TODO: localize
+
+ #endregion
+
RefreshFlavorText();
#region Dummy
@@ -379,7 +385,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
RefreshAntags();
RefreshJobs();
- RefreshLoadouts();
+ RefreshDenLoadouts();
RefreshSpecies();
RefreshTraits();
RefreshFlavorText();
diff --git a/Content.Client/_DEN/Lobby/UI/HumanoidProfileEditor.Loadouts.cs b/Content.Client/_DEN/Lobby/UI/HumanoidProfileEditor.Loadouts.cs
new file mode 100644
index 00000000000..a0fe5ba76a0
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/HumanoidProfileEditor.Loadouts.cs
@@ -0,0 +1,17 @@
+namespace Content.Client.Lobby.UI;
+
+public sealed partial class HumanoidProfileEditor
+{
+ private void RefreshDenLoadouts()
+ {
+ DenLoadouts.OnPreferenceUpdated += profile =>
+ {
+ Profile = profile;
+ SetDirty();
+ };
+
+ DenLoadouts.SetProfile(Profile);
+ DenLoadouts.RefreshCategories();
+ DenLoadouts.RefreshLoadoutItemCategories();
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.JobLoadouts.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.JobLoadouts.cs
new file mode 100644
index 00000000000..4f2d8a086ed
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.JobLoadouts.cs
@@ -0,0 +1,11 @@
+using Content.Shared._DEN.Loadout;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts;
+
+public sealed partial class LoadoutProfilesContainer
+{
+ public void OnClickEditJobLoadouts(DenLoadoutProfile loadoutProfile)
+ {
+
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.LoadoutProfile.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.LoadoutProfile.cs
new file mode 100644
index 00000000000..2735cbbff06
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.LoadoutProfile.cs
@@ -0,0 +1,19 @@
+using Content.Shared._DEN.Loadout;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts;
+
+public sealed partial class LoadoutProfilesContainer
+{
+ private void OnClickEditLoadoutProfile(DenLoadoutProfile loadoutProfile)
+ {
+ OnTryCreateLoadout(loadoutProfile);
+ }
+
+ private void OnClickDeleteLoadoutProfile(DenLoadoutProfile loadoutProfile)
+ {
+ _profile = _profile?.WithoutLoadoutProfile(loadoutProfile);
+ SetDirty(_profile);
+
+ RefreshCategories();
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Loadouts.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Loadouts.cs
new file mode 100644
index 00000000000..ab521057cfa
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Loadouts.cs
@@ -0,0 +1,128 @@
+using System.Linq;
+using Content.Client.Stylesheets;
+using Content.Shared._DEN.Loadout;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts;
+
+public sealed partial class LoadoutProfilesContainer
+{
+ private readonly Dictionary, LoadoutCategoryPrototype> _loadoutCategories = new();
+
+ private readonly Dictionary, ProtoId>
+ _parents = new();
+ private readonly Dictionary, EntityLoadoutPrototype> _loadoutEntities = new();
+ private readonly HashSet> _root = new();
+ private readonly Dictionary, Button> _categoryButtons = new();
+
+ public ProtoId? CurrentCategory = null;
+
+ private void OnClickEditLoadouts(DenLoadoutProfile loadoutProfile)
+ {
+ LoadoutItemSelection.Visible = !LoadoutContainer.Visible;
+ LoadoutProfileSelection.Visible = !LoadoutContainer.Visible;
+ }
+
+ private void CacheLoadouts()
+ {
+ _loadoutCategories.Clear();
+ _parents.Clear();
+ _loadoutEntities.Clear();
+
+ foreach (var category in _protoMan.EnumeratePrototypes())
+ {
+ foreach (var child in category.SubCategories)
+ {
+ _parents[child] = category;
+ }
+
+ if (category.Root && !_parents.ContainsKey(category))
+ _root.Add(category);
+
+ _loadoutCategories.Add(category, category);
+ }
+
+ foreach (var loadoutItem in _protoMan.EnumeratePrototypes())
+ {
+ _loadoutEntities.Add(loadoutItem, loadoutItem);
+ }
+ }
+
+ public void RefreshLoadoutItemCategories()
+ {
+ HashSet allCategories;
+
+ foreach (var button in _categoryButtons.Values)
+ {
+ button.Orphan();
+ }
+
+ _categoryButtons.Clear();
+
+ if (CurrentCategory != null)
+ {
+ allCategories = _loadoutCategories[CurrentCategory.Value]
+ .SubCategories
+ .Select(c => _loadoutCategories[c])
+ .ToHashSet();
+ }
+ else
+ {
+ allCategories = _root
+ .Select(c => _loadoutCategories[c])
+ .ToHashSet();
+ }
+
+ var (itemCategories, subCategories) = GetCategoryTypes(allCategories);
+
+ foreach (var category in itemCategories)
+ {
+ var button = new Button
+ {
+ Text = category.Name,
+ Margin = new Thickness(5, 1),
+ HorizontalExpand = true,
+ SetHeight = 25
+ };
+
+ LoadoutItemSelection.LoadoutItemCategoryContainer.AddChild(button);
+ _categoryButtons.Add(category, button);
+ }
+
+ foreach (var category in subCategories)
+ {
+ var button = new Button
+ {
+ Text = "boo",
+ Margin = new Thickness(5, 1),
+ HorizontalExpand = true,
+ SetHeight = 25,
+ StyleClasses = { "ButtonColorGreen" }
+ };
+
+ LoadoutItemSelection.LoadoutItemCategoryContainer.AddChild(button);
+ _categoryButtons.Add(category, button);
+ }
+ }
+
+ private (HashSet ItemCategories,
+ HashSet SubCategories) GetCategoryTypes(HashSet allCategories)
+ {
+ var itemCategories = new HashSet();
+ var subCategories = new HashSet();
+
+ foreach (var category in allCategories)
+ {
+ if (category.SubCategories.Count > 0)
+ {
+ subCategories.Add(category);
+ continue;
+ }
+
+ itemCategories.Add(category);
+ }
+
+ return (itemCategories, subCategories);
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Popups.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Popups.cs
new file mode 100644
index 00000000000..0e063df638f
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.Popups.cs
@@ -0,0 +1,106 @@
+using Content.Client._DEN.Lobby.UI.Loadouts.Popups;
+using Content.Shared._DEN.Loadout;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts;
+
+public sealed partial class LoadoutProfilesContainer
+{
+ private void OnTryCreateCategory(LoadoutProfileCategory? existingCategory = null)
+ {
+ if (_createCategoryPopup != null)
+ return;
+
+ _createCategoryPopup = new Popups.CreateCategoryPopup(existingCategory);
+ _createCategoryPopup.OnSubmit += OnSubmitNewCategory;
+ _createCategoryPopup.OnClose += OnCloseCategoryPopup;
+ _createCategoryPopup.OpenCentered();
+ }
+
+ private void OnSubmitNewCategory(CategoryCreationRequest request)
+ {
+ if (string.IsNullOrEmpty(request.CategoryName))
+ return;
+
+ var guid = request.ExistingCategoryId ?? Guid.NewGuid();
+ var priority = 100;
+ var members = new HashSet();
+
+ if (_profile?.LoadoutCategories.TryGetValue(guid, out var loadout) == true)
+ {
+ priority = loadout.Priority;
+ members.UnionWith(loadout.Members);
+ }
+
+ var category = new LoadoutProfileCategory
+ {
+ Id = guid,
+ Color = request.CategoryColor.ToHex(),
+ Name = request.CategoryName,
+ Priority = priority,
+ Members = members
+ };
+
+ _profile = _profile?.WithLoadoutCategory(guid, category);
+ OnPreferenceUpdated?.Invoke(_profile);
+
+ RefreshCategories();
+ OnCloseCategoryPopup();
+ }
+
+ private void OnTryCreateLoadout(DenLoadoutProfile? loadout = null)
+ {
+ if (_createLoadoutPopup != null)
+ return;
+
+ _createLoadoutPopup = new Popups.CreateLoadoutPopup(_popupCategories, loadout);
+ _createLoadoutPopup.OnSubmit += OnSubmitNewLoadout;
+ _createLoadoutPopup.OnClose += OnCloseLoadoutPopup;
+ _createLoadoutPopup.OpenCentered();
+ }
+
+ private void OnSubmitNewLoadout(LoadoutCreationRequest request)
+ {
+ if (string.IsNullOrEmpty(request.LoadoutName))
+ return;
+
+ var guid = Guid.NewGuid();
+ var priority = 100;
+ var loadouts = new HashSet>();
+
+ if (request.ExistingLoadoutId is { } loadoutProfileId
+ && _profile?.LoadoutProfiles.TryGetValue(loadoutProfileId, out var loadoutProfile) == true)
+ {
+ guid = loadoutProfile.Id;
+ priority = loadoutProfile.Priority;
+ loadouts.UnionWith(loadoutProfile.Loadouts);
+ }
+
+ var loadout = new DenLoadoutProfile
+ {
+ Id = guid,
+ Name = request.LoadoutName,
+ LoadoutCategory = request.CategoryId,
+ Priority = priority,
+ Loadouts = loadouts,
+ };
+
+ _profile = _profile?.WithLoadoutProfile(guid, request.CategoryId, loadout);
+ SetDirty(_profile);
+
+ RefreshCategories();
+ OnCloseLoadoutPopup();
+ }
+
+ private void OnCloseCategoryPopup()
+ {
+ _createCategoryPopup?.Orphan();
+ _createCategoryPopup = null;
+ }
+
+ private void OnCloseLoadoutPopup()
+ {
+ _createLoadoutPopup?.Orphan();
+ _createLoadoutPopup = null;
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml
new file mode 100644
index 00000000000..728e2de9b6d
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml.cs
new file mode 100644
index 00000000000..7bad2a0cd37
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/LoadoutProfilesContainer.xaml.cs
@@ -0,0 +1,178 @@
+using System.Linq;
+using Content.Client._DEN.Lobby.UI.Loadouts.Subcontainers;
+using Content.Shared._DEN.Loadout;
+using Content.Shared.Preferences;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutProfilesContainer : BoxContainer
+{
+ [Dependency] private IPrototypeManager _protoMan = null!;
+
+ public event Action? OnPreferenceUpdated;
+
+ private Popups.CreateCategoryPopup? _createCategoryPopup;
+ private Popups.CreateLoadoutPopup? _createLoadoutPopup;
+ private HumanoidCharacterProfile? _profile;
+
+ private Dictionary _popupCategories;
+ private Dictionary _categories = [];
+
+ public LoadoutProfilesContainer()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ _popupCategories = new Dictionary();
+
+ CacheLoadouts();
+
+ ButtonCreateCategory.OnPressed += _ => OnTryCreateCategory();
+ ButtonCreateLoadout.OnPressed += _ => OnTryCreateLoadout();
+
+ _protoMan.PrototypesReloaded += args =>
+ {
+ if (!args.WasModified()
+ || !args.WasModified())
+ return;
+
+ CacheLoadouts();
+ };
+ }
+
+ public void RefreshCategories()
+ {
+ _popupCategories.Clear();
+
+ if (_profile?.LoadoutCategories == null)
+ return;
+
+ var canCreateLoadout = _profile.LoadoutCategories.Count == 0;
+ ButtonCreateLoadout.Disabled = canCreateLoadout;
+
+ foreach (var (categoryId, categoryContainer) in _categories)
+ {
+ if (!_profile.LoadoutCategories.ContainsKey(categoryId))
+ categoryContainer.Orphan();
+ }
+
+ var newCategories = new Dictionary();
+
+ foreach (var (categoryId, category) in _profile.LoadoutCategories)
+ {
+ var categoryContainer = CreateCategory(categoryId);
+
+ if (categoryContainer == null)
+ continue;
+
+ var orphanList = categoryContainer.LoadoutContainers
+ .Where(c => !category.Members.Contains(c.Key))
+ .ToList();
+
+ foreach (var orphan in orphanList)
+ {
+ categoryContainer.LoadoutContainers.Remove(orphan.Key);
+ orphan.Value.Orphan();
+ }
+
+ if (orphanList.Count > 0)
+ categoryContainer.UpdateCategory(category, categoryContainer.Loadouts);
+
+ newCategories.Add(categoryId, categoryContainer);
+ }
+
+ _categories = newCategories;
+ }
+
+ private DenCategoryContainer? CreateCategory(Guid categoryId)
+ {
+ if (_profile == null
+ || !_profile.LoadoutCategories.TryGetValue(categoryId, out var category))
+ return null;
+
+ _popupCategories = new Dictionary(_profile.LoadoutCategories);
+
+ var profiles = new List();
+
+ foreach (var loadoutId in category.Members)
+ {
+ if (!_profile.LoadoutProfiles.TryGetValue(loadoutId, out var loadout))
+ continue;
+
+ profiles.Add(loadout);
+ }
+
+ DenCategoryContainer categoryContainer;
+
+ if (_categories.TryGetValue(category.Id, out var cachedContainer))
+ {
+ categoryContainer = cachedContainer;
+ categoryContainer.UpdateCategory(category, profiles);
+ }
+ else
+ {
+ categoryContainer = new DenCategoryContainer(category, profiles);
+ categoryContainer.CategoryActions.OnTryCategoryEdit += () => TryCategoryEdit(category);
+ categoryContainer.CategoryActions.OnTryCategoryDelete += () => TryCategoryDelete(category);
+ categoryContainer.OnClickLoadoutAction += OnClickLoadoutAction;
+
+ LoadoutContainer.AddChild(categoryContainer);
+ _categories.Add(category.Id, categoryContainer);
+ }
+
+ categoryContainer.CategoryActions.SetDeleteDisabled(profiles.Count > 0);
+ return categoryContainer;
+ }
+
+ private void OnClickLoadoutAction(DenLoadoutProfile loadoutProfile, DenLoadoutAction loadoutAction)
+ {
+ switch (loadoutAction)
+ {
+ case DenLoadoutAction.EditJobs:
+ OnClickEditJobLoadouts(loadoutProfile);
+ break;
+ case DenLoadoutAction.EditLoadouts:
+ OnClickEditLoadouts(loadoutProfile);
+ break;
+ case DenLoadoutAction.EditLoadoutProfile:
+ OnClickEditLoadoutProfile(loadoutProfile);
+ break;
+ case DenLoadoutAction.DeleteLoadoutProfile:
+ OnClickDeleteLoadoutProfile(loadoutProfile);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(loadoutAction), loadoutAction, null);
+ }
+ }
+
+ private void TryCategoryEdit(LoadoutProfileCategory profileCategory)
+ {
+ OnTryCreateCategory(profileCategory);
+ }
+
+ private void TryCategoryDelete(LoadoutProfileCategory profileCategory)
+ {
+ if (_profile == null
+ || !_profile.LoadoutCategories.ContainsKey(profileCategory.Id))
+ return;
+
+ _profile = _profile.WithoutLoadoutCategory(profileCategory);
+ SetDirty(_profile);
+ RefreshCategories();
+ }
+
+ private void SetDirty(HumanoidCharacterProfile? profile)
+ {
+ OnPreferenceUpdated?.Invoke(profile);
+ }
+
+ public void SetProfile(HumanoidCharacterProfile? newProfile)
+ {
+ _profile = newProfile;
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml
new file mode 100644
index 00000000000..31441ce4c4f
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml.cs
new file mode 100644
index 00000000000..5941a712802
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateCategoryPopup.xaml.cs
@@ -0,0 +1,46 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._DEN.Loadout;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Popups;
+
+[GenerateTypedNameReferences]
+public sealed partial class CreateCategoryPopup : FancyWindow
+{
+ public event Action? OnSubmit;
+
+ public CreateCategoryPopup(LoadoutProfileCategory? maybeCategory)
+ {
+ RobustXamlLoader.Load(this);
+
+ if (maybeCategory is { } existingCategory)
+ {
+ var color = Color.TryFromHex(existingCategory.Color) ?? Color.White;
+
+ CategoryNameEdit.Text = existingCategory.Name;
+ CategoryColorPicker.Color = color;
+ }
+
+ ButtonSubmit.OnPressed += _ => OnSubmitPressed(maybeCategory);
+ }
+
+ private void OnSubmitPressed(LoadoutProfileCategory? existingCategory)
+ {
+ var request = new CategoryCreationRequest
+ {
+ ExistingCategoryId = existingCategory?.Id ?? null,
+ CategoryName = CategoryNameEdit.Text,
+ CategoryColor = CategoryColorPicker.Color
+ };
+
+ OnSubmit?.Invoke(request);
+ }
+}
+
+public record struct CategoryCreationRequest
+{
+ public Guid? ExistingCategoryId { get; set; }
+ public string CategoryName { get; set; }
+ public Color CategoryColor { get; set; }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml
new file mode 100644
index 00000000000..28f4ffe5f1e
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml.cs
new file mode 100644
index 00000000000..e7b74388265
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Popups/CreateLoadoutPopup.xaml.cs
@@ -0,0 +1,71 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared._DEN.Loadout;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Popups;
+
+[GenerateTypedNameReferences]
+public sealed partial class CreateLoadoutPopup : FancyWindow
+{
+ public event Action? OnSubmit;
+
+ public CreateLoadoutPopup(Dictionary categories, DenLoadoutProfile? maybeLoadout = null)
+ {
+ RobustXamlLoader.Load(this);
+
+ var index = 0;
+
+ if (maybeLoadout is { } existingLoadout)
+ {
+ var categoryId = existingLoadout.LoadoutCategory;
+ var category = categories[categoryId];
+
+ LoadoutNameEdit.Text = existingLoadout.Name;
+ OptionsCategories.AddItem(category.Name, index);
+ OptionsCategories.SetItemMetadata(index, categoryId);
+
+ index += 1;
+ }
+
+ foreach (var (categoryId, category) in categories)
+ {
+ if (maybeLoadout is { } loadoutProfile
+ && categoryId == loadoutProfile.LoadoutCategory)
+ continue;
+
+ OptionsCategories.AddItem(category.Name);
+ OptionsCategories.SetItemMetadata(index, categoryId);
+
+ index += 1;
+ }
+
+ OptionsCategories.OnItemSelected += args => OptionsCategories.SelectId(args.Id);
+ ButtonSubmit.OnPressed += _ => OnSubmitPressed(maybeLoadout);
+ }
+
+ private void OnSubmitPressed(DenLoadoutProfile? maybeLoadout = null)
+ {
+ var categorySelectedIndex = OptionsCategories.SelectedId;
+ var categorySelectedId = (Guid) (OptionsCategories.GetItemMetadata(categorySelectedIndex) ?? Guid.Empty);
+
+ if (categorySelectedId == Guid.Empty)
+ return;
+
+ var request = new LoadoutCreationRequest
+ {
+ ExistingLoadoutId = maybeLoadout?.Id ?? null,
+ LoadoutName = LoadoutNameEdit.Text,
+ CategoryId = categorySelectedId,
+ };
+
+ OnSubmit?.Invoke(request);
+ }
+}
+
+public record struct LoadoutCreationRequest
+{
+ public Guid? ExistingLoadoutId { get; set; }
+ public string LoadoutName { get; set; }
+ public Guid CategoryId { get; set; }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml
new file mode 100644
index 00000000000..861a40722e1
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml.cs
new file mode 100644
index 00000000000..7747346f44c
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryActions.xaml.cs
@@ -0,0 +1,25 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Subcontainers;
+
+[GenerateTypedNameReferences]
+public sealed partial class DenCategoryActions : ContainerButton
+{
+ public event Action? OnTryCategoryEdit;
+ public event Action? OnTryCategoryDelete;
+
+ public DenCategoryActions()
+ {
+ RobustXamlLoader.Load(this);
+
+ ButtonEdit.OnPressed += _ => OnTryCategoryEdit?.Invoke();
+ ButtonDelete.OnPressed += _ => OnTryCategoryDelete?.Invoke();
+ }
+
+ public void SetDeleteDisabled(bool deleteDisabled)
+ {
+ ButtonDelete.Disabled = deleteDisabled;
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml
new file mode 100644
index 00000000000..25ed262a7e1
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml.cs
new file mode 100644
index 00000000000..10b3a8dff2f
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenCategoryContainer.xaml.cs
@@ -0,0 +1,107 @@
+using Content.Shared._DEN.Loadout;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Subcontainers;
+
+[GenerateTypedNameReferences]
+public sealed partial class DenCategoryContainer : PanelContainer
+{
+ public event Action? OnClickLoadoutAction;
+ public readonly Dictionary LoadoutContainers = new();
+ public List Loadouts;
+ private bool _isExpanded;
+
+ private LoadoutProfileCategory _profileCategory;
+
+ public DenCategoryContainer(LoadoutProfileCategory profileCategory,
+ List loadouts)
+ {
+ RobustXamlLoader.Load(this);
+
+ Loadouts = loadouts;
+ _profileCategory = profileCategory;
+
+ RefreshCategoryName(profileCategory);
+ RefreshCategoryColor(profileCategory);
+ RefreshLoadoutContainers(loadouts);
+
+ HeaderButton.OnPressed += _ => ToggleExpanded();
+ UpdateExpandedState();
+ }
+
+ public void UpdateCategory(LoadoutProfileCategory profileCategory, List loadouts)
+ {
+ RefreshCategoryName(profileCategory);
+ RefreshCategoryColor(profileCategory);
+ RefreshLoadoutContainers(loadouts);
+
+ Loadouts = loadouts;
+ _profileCategory = profileCategory;
+ }
+
+ private void RefreshCategoryName(LoadoutProfileCategory profileCategory)
+ {
+ CategoryNameLabel.Text = profileCategory.Name;
+ }
+
+ private void RefreshCategoryColor(LoadoutProfileCategory profileCategory)
+ {
+ var color = Color.TryFromHex(profileCategory.Color);
+
+ AccentBar.PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color ?? Color.White
+ };
+ }
+
+ private void RefreshLoadoutContainers(List loadoutProfiles)
+ {
+ foreach (var loadoutProfile in loadoutProfiles)
+ {
+ DenLoadoutContainer container;
+
+ if (LoadoutContainers.TryGetValue(loadoutProfile.Id, out var cachedContainer))
+ {
+ container = cachedContainer;
+ }
+ else
+ {
+ container = new DenLoadoutContainer(loadoutProfile);
+ container.Visible = true;
+ container.OnClickLoadoutAction += DoLoadoutAction;
+
+ LoadoutContainers.Add(loadoutProfile.Id, container);
+ LoadoutContainer.AddChild(container);
+ }
+
+ container.UpdateLoadout(loadoutProfile);
+ }
+
+ var countText = $"{LoadoutContainers.Count} loadout";
+
+ if (LoadoutContainers.Count != 1)
+ countText += "s";
+
+ CategoryLoadoutCount.Text = countText;
+ }
+
+ private void DoLoadoutAction(DenLoadoutProfile profile, DenLoadoutAction action)
+ {
+ OnClickLoadoutAction?.Invoke(profile, action);
+ }
+
+ public void ToggleExpanded()
+ {
+ _isExpanded = !_isExpanded;
+ UpdateExpandedState();
+ }
+
+ private void UpdateExpandedState()
+ {
+ ContentPanel.Visible = _isExpanded;
+ ExpandIcon.Text = _isExpanded ? "▼" : "▶";
+ }
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml
new file mode 100644
index 00000000000..da08bc6d911
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml.cs
new file mode 100644
index 00000000000..7229270e3f8
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/DenLoadoutContainer.xaml.cs
@@ -0,0 +1,51 @@
+using Content.Shared._DEN.Loadout;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Subcontainers;
+
+[GenerateTypedNameReferences]
+public sealed partial class DenLoadoutContainer : ContainerButton
+{
+ public event Action? OnClickLoadoutAction;
+
+ private DenLoadoutProfile _loadoutProfile;
+
+ public DenLoadoutContainer(DenLoadoutProfile loadoutProfileProfile)
+ {
+ RobustXamlLoader.Load(this);
+
+ _loadoutProfile = loadoutProfileProfile;
+
+ LoadoutNameLabel.Text = loadoutProfileProfile.Name;
+
+ ButtonEditJobs.OnPressed += _ =>
+ OnClickLoadoutAction?.Invoke(_loadoutProfile, DenLoadoutAction.EditJobs);
+ ButtonEditLoadouts.OnPressed += _ =>
+ OnClickLoadoutAction?.Invoke(_loadoutProfile, DenLoadoutAction.EditLoadouts);
+ ButtonEditLoadoutProfile.OnPressed += _ =>
+ OnClickLoadoutAction?.Invoke(_loadoutProfile, DenLoadoutAction.EditLoadoutProfile);
+ ButtonDeleteProfile.OnPressed += _ =>
+ OnClickLoadoutAction?.Invoke(_loadoutProfile, DenLoadoutAction.DeleteLoadoutProfile);
+ }
+
+ public void UpdateLoadout(DenLoadoutProfile loadoutProfile)
+ {
+ if (_loadoutProfile == loadoutProfile)
+ return;
+
+ if (loadoutProfile.Name != _loadoutProfile.Name)
+ LoadoutNameLabel.Text = loadoutProfile.Name;
+
+ _loadoutProfile = loadoutProfile;
+ }
+}
+
+public enum DenLoadoutAction
+{
+ EditJobs,
+ EditLoadouts,
+ EditLoadoutProfile,
+ DeleteLoadoutProfile
+}
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml
new file mode 100644
index 00000000000..78b1d0c3bcb
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml.cs b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml.cs
new file mode 100644
index 00000000000..e7c65046155
--- /dev/null
+++ b/Content.Client/_DEN/Lobby/UI/Loadouts/Subcontainers/LoadoutItemSelection.xaml.cs
@@ -0,0 +1,15 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._DEN.Lobby.UI.Loadouts.Subcontainers;
+
+[GenerateTypedNameReferences]
+public sealed partial class LoadoutItemSelection : PanelContainer
+{
+ public LoadoutItemSelection()
+ {
+ RobustXamlLoader.Load(this);
+ }
+}
+
diff --git a/Content.Server.Database/DenModel.cs b/Content.Server.Database/DenModel.cs
new file mode 100644
index 00000000000..25a86bf9d18
--- /dev/null
+++ b/Content.Server.Database/DenModel.cs
@@ -0,0 +1,75 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace Content.Server.Database;
+
+public static class DenModel
+{
+ [Table("denu_settings")]
+ public sealed class DenuSettings
+ {
+ [Key]
+ [Column("player_user_id")]
+ public Guid PlayerUserId { get; set; }
+
+ [Column("settings", TypeName = "jsonb")]
+ public string SettingsJson { get; set; } = "{}";
+
+ public Player Player { get; set; } = null!;
+ }
+
+ public class LoadoutProfile
+ {
+ public int Id { get; set; }
+
+ public int LoadoutCategoryId { get; set; }
+
+ public LoadoutCategory Category { get; set; } = null!;
+
+ public Guid LoadoutUniqueId { get; set; }
+
+ [MaxLength(256)]
+ public string LoadoutName { get; set; } = string.Empty;
+
+ public int Priority { get; set; }
+
+ public List LoadoutItems { get; set; } = new();
+ }
+
+ public class JobLoadout
+ {
+ public int Id { get; set; }
+
+ public int ProfileId { get; set; }
+
+ public Profile Profile { get; set; } = null!;
+
+ [MaxLength(256)]
+ public string JobName { get; set; } = string.Empty;
+
+ public List LoadoutProfiles { get; set; } = new();
+ }
+
+ public class LoadoutCategory
+ {
+ public int Id { get; set; }
+
+ public int ProfileId { get; set; }
+
+ public Profile Profile { get; set; } = null!;
+
+ public Guid CategoryUniqueId { get; set; }
+
+ public int Priority { get; set; }
+
+ [MaxLength(256)]
+ public string CategoryName { get; set; } = string.Empty;
+
+ [MaxLength(10)]
+ public string CategoryColor { get; set; } = string.Empty;
+
+ public List Members { get; set; } = new();
+ }
+}
diff --git a/Content.Server.Database/DenuModel.cs b/Content.Server.Database/DenuModel.cs
deleted file mode 100644
index 903f3222cef..00000000000
--- a/Content.Server.Database/DenuModel.cs
+++ /dev/null
@@ -1,21 +0,0 @@
-using System;
-using System.ComponentModel.DataAnnotations;
-using System.ComponentModel.DataAnnotations.Schema;
-
-namespace Content.Server.Database;
-
-public static class DenuModel
-{
- [Table("denu_settings")]
- public sealed class DenuSettings
- {
- [Key]
- [Column("player_user_id")]
- public Guid PlayerUserId { get; set; }
-
- [Column("settings", TypeName = "jsonb")]
- public string SettingsJson { get; set; } = "{}";
-
- public Player Player { get; set; } = null!;
- }
-}
diff --git a/Content.Server.Database/Migrations/Postgres/20260706120346_DenLoadouts.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260706120346_DenLoadouts.Designer.cs
new file mode 100644
index 00000000000..ce3fcab750a
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20260706120346_DenLoadouts.Designer.cs
@@ -0,0 +1,2318 @@
+//
+using System;
+using System.Collections.Generic;
+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("20260706120346_DenLoadouts")]
+ partial class DenLoadouts
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.0.6")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Deadminned")
+ .HasColumnType("boolean")
+ .HasColumnName("deadminned");
+
+ b.Property("Suspended")
+ .HasColumnType("boolean")
+ .HasColumnName("suspended");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("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("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("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("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Dismissed")
+ .HasColumnType("boolean")
+ .HasColumnName("dismissed");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("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.Ban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Type")
+ .HasColumnType("smallint")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("PK_ban");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.ToTable("ban", null, t =>
+ {
+ t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_address_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_address");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_address_ban_id");
+
+ b.ToTable("ban_address", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_hwid_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_hwid");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_hwid_ban_id");
+
+ b.ToTable("ban_hwid", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_player");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_player_ban_id");
+
+ b.HasIndex("UserId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_role_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.Property("RoleType")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_type");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_role");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_role_ban_id");
+
+ b.HasIndex("RoleType", "RoleId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_role", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRound", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_round");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_round_ban_id");
+
+ b.HasIndex("RoundId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_template_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("Length")
+ .HasColumnType("interval")
+ .HasColumnName("length");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_template");
+
+ b.ToTable("ban_template", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_blacklist");
+
+ b.ToTable("blacklist", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("Trust")
+ .HasColumnType("real")
+ .HasColumnName("trust");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("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("Time");
+
+ 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.ConsentData", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("global_consent_info_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ConsentId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("consent_id");
+
+ b.Property("ConsentValue")
+ .HasColumnType("boolean")
+ .HasColumnName("consent_value");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_global_consent_info");
+
+ b.HasIndex("Id", "UserId")
+ .IsUnique();
+
+ b.ToTable("global_consent_info", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ipintel_cache_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Score")
+ .HasColumnType("real")
+ .HasColumnName("score");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.HasKey("Id")
+ .HasName("PK_ipintel_cache");
+
+ b.ToTable("ipintel_cache", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("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.JobLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("job_name");
+
+ b.PrimitiveCollection>("LoadoutProfiles")
+ .IsRequired()
+ .HasColumnType("uuid[]")
+ .HasColumnName("loadout_profiles");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_job_loadout");
+
+ b.HasIndex("ProfileId");
+
+ b.ToTable("job_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.LoadoutCategory", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("loadout_category_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CategoryColor")
+ .IsRequired()
+ .HasMaxLength(10)
+ .HasColumnType("character varying(10)")
+ .HasColumnName("category_color");
+
+ b.Property("CategoryName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("category_name");
+
+ b.Property("CategoryUniqueId")
+ .HasColumnType("uuid")
+ .HasColumnName("category_unique_id");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_loadout_category");
+
+ b.HasIndex("ProfileId");
+
+ b.ToTable("loadout_category", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.LoadoutProfile", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("loadout_profile_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutCategoryId")
+ .HasColumnType("integer")
+ .HasColumnName("loadout_category_id");
+
+ b.PrimitiveCollection>("LoadoutItems")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("loadout_items");
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("loadout_name");
+
+ b.Property("LoadoutUniqueId")
+ .HasColumnType("uuid")
+ .HasColumnName("loadout_unique_id");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.HasKey("Id")
+ .HasName("PK_loadout_profile");
+
+ b.HasIndex("LoadoutCategoryId");
+
+ b.ToTable("loadout_profile", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PlayTime", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("play_time_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("PlayerId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_id");
+
+ b.Property("TimeSpent")
+ .HasColumnType("interval")
+ .HasColumnName("time_spent");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("FirstSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("first_seen_time");
+
+ b.Property("LastReadRules")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_read_rules");
+
+ b.Property("LastSeenAddress")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("last_seen_address");
+
+ b.Property("LastSeenTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_seen_time");
+
+ b.Property("LastSeenUserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("last_seen_user_name");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminOOCColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("admin_ooc_color");
+
+ b.PrimitiveCollection>("ConstructionFavorites")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("construction_favorites");
+
+ b.Property("SelectedCharacterSlot")
+ .HasColumnType("integer")
+ .HasColumnName("selected_character_slot");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Age")
+ .HasColumnType("integer")
+ .HasColumnName("age");
+
+ b.Property("CharacterName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("char_name");
+
+ b.Property("EyeColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("eye_color");
+
+ b.Property("FacialHairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_color");
+
+ b.Property("FacialHairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_name");
+
+ b.Property("FlavorText")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flavor_text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("gender");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_color");
+
+ b.Property("HairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_name");
+
+ b.Property("Markings")
+ .HasColumnType("jsonb")
+ .HasColumnName("markings");
+
+ b.Property("OrganMarkings")
+ .HasColumnType("jsonb")
+ .HasColumnName("organ_markings");
+
+ b.Property("PreferenceId")
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ b.Property("PreferenceUnavailable")
+ .HasColumnType("integer")
+ .HasColumnName("pref_unavailable");
+
+ b.Property("Sex")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("sex");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("skin_color");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasColumnName("slot");
+
+ b.Property("SpawnPriority")
+ .HasColumnType("integer")
+ .HasColumnName("spawn_priority");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("loadout_name");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("group_name");
+
+ b.Property("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