diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 9c01a8d979..2a8df98763 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -50,8 +50,8 @@ jobs:
- name: Publish changelog (Discord)
run: Tools/actions_changelogs_since_last_run.py
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- DISCORD_WEBHOOK_URL: ${{ secrets.CHANGELOG_DISCORD_WEBHOOK }}
+ PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
+ GITHUB_REPOSITORY: ${{ vars.GITHUB_REPOSITORY }}
- name: Publish changelog (RSS)
run: Tools/actions_changelog_rss.py
diff --git a/.github/workflows/update-credits.yml b/.github/workflows/update-credits.yml
index 69a8bc1988..5dc6299c6c 100644
--- a/.github/workflows/update-credits.yml
+++ b/.github/workflows/update-credits.yml
@@ -19,6 +19,8 @@ jobs:
- name: Get this week's Contributors
shell: pwsh
+ env:
+ GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: Tools/dump_github_contributors.ps1 > Resources/Credits/GitHub.txt
# TODO
diff --git a/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml b/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml
new file mode 100644
index 0000000000..d5f77aedd5
--- /dev/null
+++ b/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml.cs b/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml.cs
new file mode 100644
index 0000000000..275055daf6
--- /dev/null
+++ b/Content.Client/Administration/UI/DepartmentWhitelistPanel.xaml.cs
@@ -0,0 +1,49 @@
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Administration.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class DepartmentWhitelistPanel : PanelContainer
+{
+ public Action, bool>? OnSetJob;
+
+ public DepartmentWhitelistPanel(DepartmentPrototype department, IPrototypeManager proto, HashSet> whitelists)
+ {
+ RobustXamlLoader.Load(this);
+
+ var allWhitelisted = true;
+ var grey = Color.FromHex("#ccc");
+ foreach (var id in department.Roles)
+ {
+ var thisJob = id; // closure capturing funny
+ var button = new CheckBox();
+ button.Text = proto.Index(id).LocalizedName;
+ if (!proto.Index(id).Whitelisted)
+ button.Modulate = grey; // Let admins know whitelisting this job is only for futureproofing.
+ button.Pressed = whitelists.Contains(id);
+ button.OnPressed += _ => OnSetJob?.Invoke(thisJob, button.Pressed);
+ JobsContainer.AddChild(button);
+
+ allWhitelisted &= button.Pressed;
+ }
+
+ Department.Text = Loc.GetString(department.ID);
+ Department.Modulate = department.Color;
+ Department.Pressed = allWhitelisted;
+ Department.OnPressed += args =>
+ {
+ foreach (var id in department.Roles)
+ {
+ // only request to whitelist roles that aren't already whitelisted, and vice versa
+ if (whitelists.Contains(id) != Department.Pressed)
+ OnSetJob?.Invoke(id, Department.Pressed);
+ }
+ };
+ }
+}
diff --git a/Content.Client/Administration/UI/JobWhitelistsEui.cs b/Content.Client/Administration/UI/JobWhitelistsEui.cs
new file mode 100644
index 0000000000..b8fe974c0a
--- /dev/null
+++ b/Content.Client/Administration/UI/JobWhitelistsEui.cs
@@ -0,0 +1,40 @@
+using Content.Client.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+
+namespace Content.Client.Administration.UI;
+
+public sealed class JobWhitelistsEui : BaseEui
+{
+ private JobWhitelistsWindow Window;
+
+ public JobWhitelistsEui()
+ {
+ Window = new JobWhitelistsWindow();
+ Window.OnClose += () => SendMessage(new CloseEuiMessage());
+ Window.OnSetJob += (id, whitelisted) => SendMessage(new SetJobWhitelistedMessage(id, whitelisted));
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not JobWhitelistsEuiState cast)
+ return;
+
+ Window.HandleState(cast);
+ }
+
+ public override void Opened()
+ {
+ base.Opened();
+
+ Window.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+
+ Window.Close();
+ Window.Dispose();
+ }
+}
diff --git a/Content.Client/Administration/UI/JobWhitelistsWindow.xaml b/Content.Client/Administration/UI/JobWhitelistsWindow.xaml
new file mode 100644
index 0000000000..165f5ac3d7
--- /dev/null
+++ b/Content.Client/Administration/UI/JobWhitelistsWindow.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/JobWhitelistsWindow.xaml.cs b/Content.Client/Administration/UI/JobWhitelistsWindow.xaml.cs
new file mode 100644
index 0000000000..51fb5287dc
--- /dev/null
+++ b/Content.Client/Administration/UI/JobWhitelistsWindow.xaml.cs
@@ -0,0 +1,46 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Database;
+using Content.Shared.Administration;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.Administration.UI;
+
+///
+/// An admin panel to toggle whitelists for individual jobs or entire departments.
+/// This should generally be preferred to a blanket whitelist (Whitelisted: True) since
+/// being good with a batong doesn't mean you know engineering and vice versa.
+///
+[GenerateTypedNameReferences]
+public sealed partial class JobWhitelistsWindow : FancyWindow
+{
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ public Action, bool>? OnSetJob;
+
+ public JobWhitelistsWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+
+ PlayerName.Text = "???";
+ }
+
+ public void HandleState(JobWhitelistsEuiState state)
+ {
+ PlayerName.Text = state.PlayerName;
+
+ Departments.RemoveAllChildren();
+ foreach (var proto in _proto.EnumeratePrototypes())
+ {
+ var panel = new DepartmentWhitelistPanel(proto, _proto, state.Whitelists);
+ panel.OnSetJob += (id, whitelisting) => OnSetJob?.Invoke(id, whitelisting);
+ Departments.AddChild(panel);
+ }
+ }
+}
diff --git a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
index f3b2d373d7..2d4d192ea8 100644
--- a/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
+++ b/Content.Client/CartridgeLoader/Cartridges/NewsReaderUiFragment.xaml.cs
@@ -31,7 +31,7 @@ public void UpdateState(NewsArticle article, int targetNum, int totalNum, bool n
Author.Visible = true;
PageName.Text = article.Title;
- PageText.SetMarkup(article.Content);
+ PageText.SetMarkupPermissive(article.Content);
PageNum.Text = $"{targetNum}/{totalNum}";
diff --git a/Content.Client/Chapel/SacrificialAltarSystem.cs b/Content.Client/Chapel/SacrificialAltarSystem.cs
new file mode 100644
index 0000000000..5b694e4b4e
--- /dev/null
+++ b/Content.Client/Chapel/SacrificialAltarSystem.cs
@@ -0,0 +1,5 @@
+using Content.Shared.Chapel;
+
+namespace Content.Client.Chapel;
+
+public sealed class SacrificialAltarSystem : SharedSacrificialAltarSystem;
diff --git a/Content.Client/Arachne/CocoonSystem.cs b/Content.Client/Cocoon/CocoonSystem.cs
similarity index 98%
rename from Content.Client/Arachne/CocoonSystem.cs
rename to Content.Client/Cocoon/CocoonSystem.cs
index 7645c5bc81..d3eb4a8205 100644
--- a/Content.Client/Arachne/CocoonSystem.cs
+++ b/Content.Client/Cocoon/CocoonSystem.cs
@@ -1,4 +1,4 @@
-using Content.Shared.Arachne;
+using Content.Shared.Cocoon;
using Content.Shared.Humanoid;
using Robust.Client.GameObjects;
using Robust.Shared.Containers;
diff --git a/Content.Client/LateJoin/LateJoinGui.cs b/Content.Client/LateJoin/LateJoinGui.cs
index 1b2a87be72..e474f0c942 100644
--- a/Content.Client/LateJoin/LateJoinGui.cs
+++ b/Content.Client/LateJoin/LateJoinGui.cs
@@ -261,7 +261,24 @@ private void RebuildUI()
jobButton.OnPressed += _ => SelectedId.Invoke((id, jobButton.JobId));
- if (!_characterRequirements.CheckRequirementsValid(
+ if (!_jobRequirements.CheckJobWhitelist(prototype, out var reason))
+ {
+ jobButton.Disabled = true;
+
+ var tooltip = new Tooltip();
+ tooltip.SetMessage(reason);
+ jobButton.TooltipSupplier = _ => tooltip;
+
+ jobSelector.AddChild(new TextureRect
+ {
+ TextureScale = new Vector2(0.4f, 0.4f),
+ Stretch = TextureRect.StretchMode.KeepCentered,
+ Texture = _sprites.Frame0(new SpriteSpecifier.Texture(new ("/Textures/Interface/Nano/lock.svg.192dpi.png"))),
+ HorizontalExpand = true,
+ HorizontalAlignment = HAlignment.Right,
+ });
+ }
+ else if (!_characterRequirements.CheckRequirementsValid(
prototype.Requirements ?? new(),
prototype,
(HumanoidCharacterProfile) (_prefs.Preferences?.SelectedCharacter
diff --git a/Content.Client/Lobby/LobbyUIController.cs b/Content.Client/Lobby/LobbyUIController.cs
index ccefbb0aa4..26643cb603 100644
--- a/Content.Client/Lobby/LobbyUIController.cs
+++ b/Content.Client/Lobby/LobbyUIController.cs
@@ -63,12 +63,8 @@ public override void Initialize()
_requirements.Updated += OnRequirementsUpdated;
_configurationManager.OnValueChanged(CCVars.FlavorText, _ => _profileEditor?.RefreshFlavorText());
- _configurationManager.OnValueChanged(CCVars.GameRoleTimers,
- _ =>
- {
- _profileEditor?.RefreshAntags();
- _profileEditor?.RefreshJobs();
- });
+ _configurationManager.OnValueChanged(CCVars.GameRoleTimers, _ => RefreshProfileEditor());
+ _configurationManager.OnValueChanged(CCVars.GameRoleWhitelist, _ => RefreshProfileEditor());
_preferencesManager.OnServerDataLoaded += PreferencesDataLoaded;
}
@@ -168,6 +164,12 @@ private void RefreshLobbyPreview()
PreviewPanel.SetSummaryText(humanoid.Summary);
}
+ private void RefreshProfileEditor()
+ {
+ _profileEditor?.RefreshAntags();
+ _profileEditor?.RefreshJobs();
+ }
+
private void SaveProfile()
{
DebugTools.Assert(EditedProfile != null);
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 012d894aee..3f526981a4 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -639,6 +639,8 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
UpdateGenderControls();
UpdateSkinColor();
UpdateSpawnPriorityControls();
+ UpdateFlavorTextEdit();
+ UpdateCustomSpecieNameEdit();
UpdateAgeEdit();
UpdateEyePickers();
UpdateSaveButton();
@@ -770,7 +772,9 @@ public void RefreshJobs()
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
- if (!_characterRequirementsSystem.CheckRequirementsValid(
+ if (!_requirements.CheckJobWhitelist(job, out var reason))
+ selector.LockRequirements(reason);
+ else if (!_characterRequirementsSystem.CheckRequirementsValid(
job.Requirements ?? new(),
job,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
@@ -916,7 +920,9 @@ private void UpdateRoleRequirements()
icon.Texture = jobIcon.Icon.Frame0();
selector.Setup(items, job.LocalizedName, 200, job.LocalizedDescription, icon);
- if (!_characterRequirementsSystem.CheckRequirementsValid(
+ if (!_requirements.CheckJobWhitelist(job, out var reason))
+ selector.LockRequirements(reason);
+ else if (!_characterRequirementsSystem.CheckRequirementsValid(
job.Requirements ?? new(),
job,
Profile ?? HumanoidCharacterProfile.DefaultWithSpecies(),
@@ -972,6 +978,7 @@ private void EnsureJobRequirementsValid()
{
var proto = _prototypeManager.Index(jobId);
if ((JobPriority) selector.Selected == JobPriority.Never
+ || _requirements.CheckJobWhitelist(proto, out _)
|| _characterRequirementsSystem.CheckRequirementsValid(
proto.Requirements ?? new(),
proto,
@@ -1200,15 +1207,9 @@ private void UpdateNameEdit()
private void UpdateCustomSpecieNameEdit()
{
- if (Profile == null)
- return;
-
- _customspecienameEdit.Text = Profile.Customspeciename ?? "";
-
- if (!_prototypeManager.TryIndex(Profile.Species, out var speciesProto))
- return;
-
- _ccustomspecienamecontainerEdit.Visible = speciesProto.CustomName;
+ var species = _species.Find(x => x.ID == Profile?.Species) ?? _species.First();
+ _customspecienameEdit.Text = string.IsNullOrEmpty(Profile?.Customspeciename) ? Loc.GetString(species.Name) : Profile.Customspeciename;
+ _ccustomspecienamecontainerEdit.Visible = species.CustomName;
}
private void UpdateFlavorTextEdit()
@@ -1437,7 +1438,7 @@ private void UpdateWeight()
var avg = (Profile.Width + Profile.Height) / 2;
var weight = MathF.Round(MathF.PI * MathF.Pow(radius * avg, 2) * density);
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) weight));
- }
+ }
else // Whelp, the fixture doesn't exist, guesstimate it instead
WeightLabel.Text = Loc.GetString("humanoid-profile-editor-weight-label", ("weight", (int) 71));
diff --git a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
index 7f98e3e0c3..5e068f1e9c 100644
--- a/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
+++ b/Content.Client/MassMedia/Ui/ArticleEditorPanel.xaml.cs
@@ -76,7 +76,7 @@ private void OnPreview(BaseButton.ButtonEventArgs eventArgs)
TextEditPanel.Visible = !_preview;
PreviewPanel.Visible = _preview;
- PreviewLabel.SetMarkup(Rope.Collapse(ContentField.TextRope));
+ PreviewLabel.SetMarkupPermissive(Rope.Collapse(ContentField.TextRope));
}
private void OnCancel(BaseButton.ButtonEventArgs eventArgs)
diff --git a/Content.Client/Message/RichTextLabelExt.cs b/Content.Client/Message/RichTextLabelExt.cs
index ab6d17bf44..f3de61be12 100644
--- a/Content.Client/Message/RichTextLabelExt.cs
+++ b/Content.Client/Message/RichTextLabelExt.cs
@@ -5,9 +5,27 @@ namespace Content.Client.Message;
public static class RichTextLabelExt
{
+
+
+ ///
+ /// Sets the labels markup.
+ ///
+ ///
+ /// Invalid markup will cause exceptions to be thrown. Don't use this for user input!
+ ///
public static RichTextLabel SetMarkup(this RichTextLabel label, string markup)
{
label.SetMessage(FormattedMessage.FromMarkup(markup));
return label;
}
+
+ ///
+ /// Sets the labels markup.
+ /// Uses FormatedMessage.FromMarkupPermissive which treats invalid markup as text.
+ ///
+ public static RichTextLabel SetMarkupPermissive(this RichTextLabel label, string markup)
+ {
+ label.SetMessage(FormattedMessage.FromMarkupPermissive(markup));
+ return label;
+ }
}
diff --git a/Content.Client/Overlays/ColorTintOverlay.cs b/Content.Client/Overlays/ColorTintOverlay.cs
new file mode 100644
index 0000000000..f40a8d7342
--- /dev/null
+++ b/Content.Client/Overlays/ColorTintOverlay.cs
@@ -0,0 +1,63 @@
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Shadowkin;
+
+namespace Content.Client.Overlays;
+
+///
+/// A simple overlay that applies a colored tint to the screen.
+///
+public sealed class ColorTintOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpace;
+ private readonly ShaderInstance _shader;
+
+ ///
+ /// The color to tint the screen to as RGB on a scale of 0-1.
+ ///
+ public Vector3? TintColor = null;
+ ///
+ /// The percent to tint the screen by on a scale of 0-1.
+ ///
+ public float? TintAmount = null;
+
+ public ColorTintOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _shader = _prototype.Index("ColorTint").InstanceUnique();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_player.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+ if (TintColor != null)
+ _shader.SetParameter("tint_color", (Vector3) TintColor);
+ if (TintAmount != null)
+ _shader.SetParameter("tint_amount", (float) TintAmount);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.UseShader(_shader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Overlays/EtherealOverlay.cs b/Content.Client/Overlays/EtherealOverlay.cs
new file mode 100644
index 0000000000..3d771de8ce
--- /dev/null
+++ b/Content.Client/Overlays/EtherealOverlay.cs
@@ -0,0 +1,48 @@
+using Robust.Client.Graphics;
+using Robust.Client.Player;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Content.Shared.Shadowkin;
+
+namespace Content.Client.Overlays;
+
+public sealed class EtherealOverlay : Overlay
+{
+ [Dependency] private readonly IPrototypeManager _prototype = default!;
+ [Dependency] private readonly IPlayerManager _player = default!;
+ [Dependency] IEntityManager _entityManager = default!;
+
+ public override bool RequestScreenTexture => true;
+ public override OverlaySpace Space => OverlaySpace.WorldSpaceBelowFOV;
+ private readonly ShaderInstance _shader;
+
+ public EtherealOverlay()
+ {
+ IoCManager.InjectDependencies(this);
+ _shader = _prototype.Index("Ethereal").InstanceUnique();
+ }
+
+ protected override bool BeforeDraw(in OverlayDrawArgs args)
+ {
+ if (_player.LocalEntity is not { Valid: true } player
+ || !_entityManager.HasComponent(player))
+ return false;
+
+ return base.BeforeDraw(in args);
+ }
+
+ protected override void Draw(in OverlayDrawArgs args)
+ {
+ if (ScreenTexture is null)
+ return;
+
+ _shader.SetParameter("SCREEN_TEXTURE", ScreenTexture);
+
+ var worldHandle = args.WorldHandle;
+ var viewport = args.WorldBounds;
+ worldHandle.SetTransform(Matrix3.Identity);
+ worldHandle.UseShader(_shader);
+ worldHandle.DrawRect(viewport, Color.White);
+ worldHandle.UseShader(null);
+ }
+}
\ No newline at end of file
diff --git a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
index a2f8061d05..286358b85e 100644
--- a/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
+++ b/Content.Client/Players/PlayTimeTracking/JobRequirementsManager.cs
@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.CCVar;
using Content.Shared.Customization.Systems;
+using Content.Shared.Players.JobWhitelist;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
@@ -18,13 +19,13 @@ public sealed partial class JobRequirementsManager : ISharedPlaytimeManager
{
[Dependency] private readonly IBaseClient _client = default!;
[Dependency] private readonly IClientNetManager _net = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypes = default!;
private readonly Dictionary _roles = new();
private readonly List _roleBans = new();
-
private ISawmill _sawmill = default!;
-
+ private readonly List _jobWhitelists = new();
public event Action? Updated;
public void Initialize()
@@ -35,6 +36,7 @@ public void Initialize()
_net.RegisterNetMessage(RxRoleBans);
_net.RegisterNetMessage(RxPlayTime);
_net.RegisterNetMessage(RxWhitelist);
+ _net.RegisterNetMessage(RxJobWhitelist);
_client.RunLevelChanged += ClientOnRunLevelChanged;
}
@@ -78,6 +80,28 @@ private void RxPlayTime(MsgPlayTime message)
Updated?.Invoke();
}
+ private void RxJobWhitelist(MsgJobWhitelist message)
+ {
+ _jobWhitelists.Clear();
+ _jobWhitelists.AddRange(message.Whitelist);
+ Updated?.Invoke();
+ }
+
+ public bool CheckJobWhitelist(JobPrototype job, [NotNullWhen(false)] out FormattedMessage? reason)
+ {
+ reason = default;
+ if (!_cfg.GetCVar(CCVars.GameRoleWhitelist))
+ return true;
+
+ if (job.Whitelisted && !_jobWhitelists.Contains(job.ID))
+ {
+ reason = FormattedMessage.FromUnformatted(Loc.GetString("role-not-whitelisted"));
+ return false;
+ }
+
+ return true;
+ }
+
public TimeSpan FetchOverallPlaytime()
{
return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
diff --git a/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs b/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs
new file mode 100644
index 0000000000..6b2a89f7a9
--- /dev/null
+++ b/Content.Client/RadialSelector/RadialSelectorMenuBUI.cs
@@ -0,0 +1,202 @@
+using System.Linq;
+using System.Numerics;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Construction.Prototypes;
+using Content.Shared.RadialSelector;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.Input;
+using Robust.Client.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Prototypes;
+
+// ReSharper disable InconsistentNaming
+
+namespace Content.Client.RadialSelector;
+
+[UsedImplicitly]
+public sealed class RadialSelectorMenuBUI : BoundUserInterface
+{
+ [Dependency] private readonly IClyde _displayManager = default!;
+ [Dependency] private readonly IInputManager _inputManager = default!;
+ [Dependency] private readonly IResourceCache _resources = default!;
+ [Dependency] private readonly IPrototypeManager _protoManager = default!;
+ [Dependency] private readonly IEntityManager _entManager = default!;
+
+ private readonly SpriteSystem _spriteSystem;
+
+ private readonly RadialMenu _menu;
+
+ // Used to clearing on state changing
+ private readonly HashSet _cachedContainers = new();
+
+ private bool _openCentered;
+ private readonly Vector2 ItemSize = Vector2.One * 64;
+
+ public RadialSelectorMenuBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+ {
+ _spriteSystem = _entManager.System();
+ _menu = new RadialMenu
+ {
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ BackButtonStyleClass = "RadialMenuBackButton",
+ CloseButtonStyleClass = "RadialMenuCloseButton"
+ };
+ }
+
+ protected override void Open()
+ {
+ _menu.OnClose += Close;
+
+ if (_openCentered)
+ _menu.OpenCentered();
+ else
+ _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / _displayManager.ScreenSize);
+ }
+
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+
+ if (state is not RadialSelectorState radialSelectorState)
+ return;
+
+ ClearExistingContainers();
+ CreateMenu(radialSelectorState.Entries);
+ _openCentered = radialSelectorState.OpenCentered;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (disposing)
+ _menu.Dispose();
+ }
+
+ private void CreateMenu(List entries, string parentCategory = "")
+ {
+ var container = new RadialContainer
+ {
+ Name = !string.IsNullOrEmpty(parentCategory) ? parentCategory : "Main",
+ Radius = 48f + 24f * MathF.Log(entries.Count),
+ };
+
+ _menu.AddChild(container);
+ _cachedContainers.Add(container);
+
+ foreach (var entry in entries)
+ {
+ if (entry.Category != null)
+ {
+ var button = CreateButton(entry.Category.Name, _spriteSystem.Frame0(entry.Category.Icon));
+ button.TargetLayer = entry.Category.Name;
+ CreateMenu(entry.Category.Entries, entry.Category.Name);
+ container.AddChild(button);
+ }
+ else if (entry.Prototype != null)
+ {
+ var name = GetName(entry.Prototype);
+ var icon = GetTextures(entry);
+ var button = CreateButton(name, icon);
+ button.OnButtonUp += _ =>
+ {
+ var msg = new RadialSelectorSelectedMessage(entry.Prototype);
+ SendPredictedMessage(msg);
+ };
+
+ container.AddChild(button);
+ }
+ }
+ }
+
+ private string GetName(string proto)
+ {
+ if (_protoManager.TryIndex(proto, out var prototype))
+ return prototype.Name;
+
+ if (_protoManager.TryIndex(proto, out ConstructionPrototype? constructionPrototype))
+ return constructionPrototype.Name;
+
+ return proto;
+ }
+
+ private List GetTextures(RadialSelectorEntry entry)
+ {
+ var result = new List();
+ if (entry.Icon is not null)
+ {
+ result.Add(_spriteSystem.Frame0(entry.Icon));
+ return result;
+ }
+
+ if (_protoManager.TryIndex(entry.Prototype!, out var prototype))
+ {
+ result.AddRange(SpriteComponent.GetPrototypeTextures(prototype, _resources).Select(o => o.Default));
+ return result;
+ }
+
+ if (_protoManager.TryIndex(entry.Prototype!, out ConstructionPrototype? constructionProto))
+ {
+ result.Add(_spriteSystem.Frame0(constructionProto.Icon));
+ return result;
+ }
+
+ // No icons provided and no icons found in prototypes. There's nothing we can do.
+ return result;
+ }
+
+ private RadialMenuTextureButton CreateButton(string name, Texture icon)
+ {
+ var button = new RadialMenuTextureButton
+ {
+ ToolTip = Loc.GetString(name),
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = ItemSize
+ };
+
+ var iconScale = ItemSize / icon.Size;
+ var texture = new TextureRect
+ {
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalAlignment = Control.HAlignment.Center,
+ Texture = icon,
+ TextureScale = iconScale
+ };
+
+ button.AddChild(texture);
+ return button;
+ }
+
+ private RadialMenuTextureButton CreateButton(string name, List icons)
+ {
+ var button = new RadialMenuTextureButton
+ {
+ ToolTip = Loc.GetString(name),
+ StyleClasses = { "RadialMenuButton" },
+ SetSize = ItemSize
+ };
+
+ var iconScale = ItemSize / icons[0].Size;
+ var texture = new LayeredTextureRect
+ {
+ VerticalAlignment = Control.VAlignment.Center,
+ HorizontalAlignment = Control.HAlignment.Center,
+ Textures = icons,
+ TextureScale = iconScale
+ };
+
+ button.AddChild(texture);
+ return button;
+ }
+
+ private void ClearExistingContainers()
+ {
+ foreach (var container in _cachedContainers)
+ _menu.RemoveChild(container);
+
+ _cachedContainers.Clear();
+ }
+}
diff --git a/Content.Client/Shadowkin/EtherealSystem.cs b/Content.Client/Shadowkin/EtherealSystem.cs
new file mode 100644
index 0000000000..cb289a87f1
--- /dev/null
+++ b/Content.Client/Shadowkin/EtherealSystem.cs
@@ -0,0 +1,52 @@
+using Content.Shared.Shadowkin;
+using Robust.Client.Graphics;
+using Robust.Shared.Player;
+using Content.Client.Overlays;
+
+namespace Content.Client.Shadowkin;
+
+public sealed partial class EtherealSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private EtherealOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(Onhutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ _overlay = new();
+ }
+
+ private void OnInit(EntityUid uid, EtherealComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void Onhutdown(EntityUid uid, EtherealComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, EtherealComponent component, LocalPlayerAttachedEvent args)
+ {
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, EtherealComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/Shadowkin/ShadowkinSystem.cs b/Content.Client/Shadowkin/ShadowkinSystem.cs
new file mode 100644
index 0000000000..d8e1b69fc7
--- /dev/null
+++ b/Content.Client/Shadowkin/ShadowkinSystem.cs
@@ -0,0 +1,114 @@
+using Content.Shared.Shadowkin;
+using Content.Shared.CCVar;
+using Robust.Client.Graphics;
+using Robust.Shared.Configuration;
+using Robust.Shared.Player;
+using Content.Shared.Humanoid;
+using Content.Shared.Abilities.Psionics;
+using Content.Client.Overlays;
+
+namespace Content.Client.Shadowkin;
+
+public sealed partial class ShadowkinSystem : EntitySystem
+{
+ [Dependency] private readonly IOverlayManager _overlayMan = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly ISharedPlayerManager _playerMan = default!;
+
+ private ColorTintOverlay _overlay = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnInit);
+ SubscribeLocalEvent(Onhutdown);
+ SubscribeLocalEvent(OnPlayerAttached);
+ SubscribeLocalEvent(OnPlayerDetached);
+
+ Subs.CVar(_cfg, CCVars.NoVisionFilters, OnNoVisionFiltersChanged);
+
+ _overlay = new();
+ }
+
+ private void OnInit(EntityUid uid, ShadowkinComponent component, ComponentInit args)
+ {
+ if (uid != _playerMan.LocalEntity
+ || _cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void Onhutdown(EntityUid uid, ShadowkinComponent component, ComponentShutdown args)
+ {
+ if (uid != _playerMan.LocalEntity)
+ return;
+
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnPlayerAttached(EntityUid uid, ShadowkinComponent component, LocalPlayerAttachedEvent args)
+ {
+ if (_cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ private void OnPlayerDetached(EntityUid uid, ShadowkinComponent component, LocalPlayerDetachedEvent args)
+ {
+ _overlayMan.RemoveOverlay(_overlay);
+ }
+
+ private void OnNoVisionFiltersChanged(bool enabled)
+ {
+ if (enabled)
+ _overlayMan.RemoveOverlay(_overlay);
+ else
+ _overlayMan.AddOverlay(_overlay);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+
+ if (_cfg.GetCVar(CCVars.NoVisionFilters))
+ return;
+
+ var uid = _playerMan.LocalEntity;
+ if (uid == null
+ || !TryComp(uid, out var comp)
+ || !TryComp(uid, out var humanoid))
+ return;
+
+ // 1/3 = 0.333...
+ // intensity = min + (power / max)
+ // intensity = intensity / 0.333
+ // intensity = clamp intensity min, max
+
+ var tintIntensity = 0.65f;
+ if (TryComp(uid, out var magic))
+ {
+ var min = 0.45f;
+ var max = 0.75f;
+ tintIntensity = Math.Clamp(min + (magic.Mana / magic.MaxMana) * 0.333f, min, max);
+ }
+
+ UpdateShader(new Vector3(humanoid.EyeColor.R, humanoid.EyeColor.G, humanoid.EyeColor.B), tintIntensity);
+ }
+
+ private void UpdateShader(Vector3? color, float? intensity)
+ {
+ while (_overlayMan.HasOverlay())
+ _overlayMan.RemoveOverlay(_overlay);
+
+ if (color != null)
+ _overlay.TintColor = color;
+ if (intensity != null)
+ _overlay.TintAmount = intensity;
+
+ if (!_cfg.GetCVar(CCVars.NoVisionFilters))
+ _overlayMan.AddOverlay(_overlay);
+ }
+}
diff --git a/Content.Client/ShortConstruction/ShortConstructionSystem.cs b/Content.Client/ShortConstruction/ShortConstructionSystem.cs
new file mode 100644
index 0000000000..492411977b
--- /dev/null
+++ b/Content.Client/ShortConstruction/ShortConstructionSystem.cs
@@ -0,0 +1,46 @@
+using Content.Client.Construction;
+using Content.Shared.Construction.Prototypes;
+using Content.Shared.RadialSelector;
+using Content.Shared.ShortConstruction;
+using Robust.Client.Placement;
+using Robust.Shared.Enums;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+
+namespace Content.Client.ShortConstruction;
+
+public sealed class ShortConstructionSystem : EntitySystem
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IPlacementManager _placement = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ [Dependency] private readonly ConstructionSystem _construction = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnItemRecieved);
+ }
+
+ private void OnItemRecieved(Entity ent, ref RadialSelectorSelectedMessage args)
+ {
+ if (!_proto.TryIndex(args.SelectedItem, out ConstructionPrototype? prototype) ||
+ !_gameTiming.IsFirstTimePredicted)
+ return;
+
+ if (prototype.Type == ConstructionType.Item)
+ {
+ _construction.TryStartItemConstruction(prototype.ID);
+ return;
+ }
+
+ _placement.BeginPlacing(new PlacementInformation
+ {
+ IsTile = false,
+ PlacementOption = prototype.PlacementMode
+ },
+ new ConstructionPlacementHijack(_construction, prototype));
+ }
+}
diff --git a/Content.Client/ShortConstruction/UI/ShortConstructionMenuBUI.cs b/Content.Client/ShortConstruction/UI/ShortConstructionMenuBUI.cs
deleted file mode 100644
index 95da2e0a33..0000000000
--- a/Content.Client/ShortConstruction/UI/ShortConstructionMenuBUI.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-using System.Numerics;
-using Content.Client.Construction;
-using Content.Client.UserInterface.Controls;
-using Content.Shared.Construction.Prototypes;
-using Content.Shared.ShortConstruction;
-using JetBrains.Annotations;
-using Robust.Client.GameObjects;
-using Robust.Client.Graphics;
-using Robust.Client.Input;
-using Robust.Client.Placement;
-using Robust.Client.UserInterface;
-using Robust.Client.UserInterface.Controls;
-using Robust.Shared.Enums;
-using Robust.Shared.Prototypes;
-using Robust.Shared.Utility;
-
-// ReSharper disable InconsistentNaming
-
-namespace Content.Client.ShortConstruction.UI;
-
-[UsedImplicitly]
-public sealed class ShortConstructionMenuBUI : BoundUserInterface
-{
- [Dependency] private readonly IClyde _displayManager = default!;
- [Dependency] private readonly IInputManager _inputManager = default!;
- [Dependency] private readonly EntityManager _entManager = default!;
- [Dependency] private readonly IPrototypeManager _protoManager = default!;
- [Dependency] private readonly IPlacementManager _placementManager = default!;
-
- private readonly ConstructionSystem _construction;
- private readonly SpriteSystem _spriteSystem;
-
- private RadialMenu? _menu;
- public ShortConstructionMenuBUI(EntityUid owner, Enum uiKey) : base(owner, uiKey)
- {
- _construction = _entManager.System();
- _spriteSystem = _entManager.System();
- }
-
- protected override void Open()
- {
- _menu = new RadialMenu
- {
- HorizontalExpand = true,
- VerticalExpand = true,
- BackButtonStyleClass = "RadialMenuBackButton",
- CloseButtonStyleClass = "RadialMenuCloseButton"
- };
-
- if (_entManager.TryGetComponent(Owner, out var crafting))
- CreateMenu(crafting.Entries);
-
- _menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / _displayManager.ScreenSize);
- }
-
- protected override void Dispose(bool disposing)
- {
- base.Dispose(disposing);
- if (disposing)
- _menu?.Dispose();
- }
-
- private void CreateMenu(List entries, string? parentCategory = null)
- {
- if (_menu == null)
- return;
-
- var container = new RadialContainer
- {
- Name = parentCategory ?? "Main",
- Radius = 48f + 24f * MathF.Log(entries.Count),
- };
-
- _menu.AddChild(container);
-
- foreach (var entry in entries)
- {
- if (entry.Category != null)
- {
- var button = CreateButton(entry.Category.Name, entry.Category.Icon);
- button.TargetLayer = entry.Category.Name;
- CreateMenu(entry.Category.Entries, entry.Category.Name);
- container.AddChild(button);
- }
- else if (entry.Prototype != null
- && _protoManager.TryIndex(entry.Prototype, out var proto))
- {
- var button = CreateButton(proto.Name, proto.Icon);
- button.OnButtonUp += _ => ConstructItem(proto);
- container.AddChild(button);
- }
- }
-
- }
-
- private RadialMenuTextureButton CreateButton(string name, SpriteSpecifier icon)
- {
- var button = new RadialMenuTextureButton
- {
- ToolTip = Loc.GetString(name),
- StyleClasses = { "RadialMenuButton" },
- SetSize = new Vector2(64f, 64f),
- };
-
- var texture = new TextureRect
- {
- VerticalAlignment = Control.VAlignment.Center,
- HorizontalAlignment = Control.HAlignment.Center,
- Texture = _spriteSystem.Frame0(icon),
- TextureScale = new Vector2(2f, 2f)
- };
-
- button.AddChild(texture);
- return button;
- }
-
- ///
- /// Makes an item or places a schematic based on the type of construction recipe.
- ///
- private void ConstructItem(ConstructionPrototype prototype)
- {
- if (prototype.Type == ConstructionType.Item)
- {
- _construction.TryStartItemConstruction(prototype.ID);
- return;
- }
-
- _placementManager.BeginPlacing(new PlacementInformation
- {
- IsTile = false,
- PlacementOption = prototype.PlacementMode
- }, new ConstructionPlacementHijack(_construction, prototype));
-
- // Should only close the menu if we're placing a construction hijack.
- // Theres not much point to closing it though. _menu!.Close();
- }
-}
diff --git a/Content.Client/Tips/TippyUIController.cs b/Content.Client/Tips/TippyUIController.cs
index 67c3ee45a7..2cc694d97d 100644
--- a/Content.Client/Tips/TippyUIController.cs
+++ b/Content.Client/Tips/TippyUIController.cs
@@ -175,7 +175,7 @@ private void NextState(TippyUI tippy)
sprite.LayerSetVisible("hiding", false);
}
sprite.Rotation = 0;
- tippy.Label.SetMarkup(_currentMessage.Msg);
+ tippy.Label.SetMarkupPermissive(_currentMessage.Msg);
tippy.Label.Visible = false;
tippy.LabelPanel.Visible = false;
tippy.Visible = true;
diff --git a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
index e39ac5d322..a9d7e09826 100644
--- a/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
+++ b/Content.Client/UserInterface/Systems/Storage/Controls/StorageContainer.cs
@@ -254,7 +254,7 @@ public void BuildItemPieces()
//todo. at some point, we may want to only rebuild the pieces that have actually received new data.
- _pieceGrid.Children.Clear();
+ _pieceGrid.RemoveAllChildren();
_pieceGrid.Rows = boundingGrid.Height + 1;
_pieceGrid.Columns = boundingGrid.Width + 1;
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
@@ -275,18 +275,29 @@ public void BuildItemPieces()
if (_entity.TryGetComponent(itemEnt, out var itemEntComponent))
{
- var gridPiece = new ItemGridPiece((itemEnt, itemEntComponent), itemPos, _entity)
+ ItemGridPiece gridPiece;
+
+ if (_storageController.CurrentlyDragging?.Entity is { } dragging
+ && dragging == itemEnt)
+ {
+ _storageController.CurrentlyDragging.Orphan();
+ gridPiece = _storageController.CurrentlyDragging;
+ }
+ else
{
- MinSize = size,
- Marked = Array.IndexOf(containedEntities, itemEnt) switch
+ gridPiece = new ItemGridPiece((itemEnt, itemEntComponent), itemPos, _entity)
{
- 0 => ItemGridPieceMarks.First,
- 1 => ItemGridPieceMarks.Second,
- _ => null,
- }
- };
- gridPiece.OnPiecePressed += OnPiecePressed;
- gridPiece.OnPieceUnpressed += OnPieceUnpressed;
+ MinSize = size,
+ Marked = Array.IndexOf(containedEntities, itemEnt) switch
+ {
+ 0 => ItemGridPieceMarks.First,
+ 1 => ItemGridPieceMarks.Second,
+ _ => null,
+ }
+ };
+ gridPiece.OnPiecePressed += OnPiecePressed;
+ gridPiece.OnPieceUnpressed += OnPieceUnpressed;
+ }
control.AddChild(gridPiece);
}
diff --git a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
index 346469d29d..97c9d8b795 100644
--- a/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
+++ b/Content.Client/UserInterface/Systems/Storage/StorageUIController.cs
@@ -314,15 +314,16 @@ private void OnPieceUnpressed(GUIBoundKeyEventArgs args, ItemGridPiece control)
_entity.GetNetEntity(storageEnt)));
}
+ _menuDragHelper.EndDrag();
_container?.BuildItemPieces();
}
else //if we just clicked, then take it out of the bag.
{
+ _menuDragHelper.EndDrag();
_entity.RaisePredictiveEvent(new StorageInteractWithItemEvent(
_entity.GetNetEntity(control.Entity),
_entity.GetNetEntity(storageEnt)));
}
- _menuDragHelper.EndDrag();
args.Handle();
}
diff --git a/Content.IntegrationTests/PoolManager.Cvars.cs b/Content.IntegrationTests/PoolManager.Cvars.cs
index aa6b4dffdc..5acd9d502c 100644
--- a/Content.IntegrationTests/PoolManager.Cvars.cs
+++ b/Content.IntegrationTests/PoolManager.Cvars.cs
@@ -21,6 +21,7 @@ private static readonly (string cvar, string value)[] TestCvars =
(CCVars.NPCMaxUpdates.Name, "999999"),
(CVars.ThreadParallelCount.Name, "1"),
(CCVars.GameRoleTimers.Name, "false"),
+ (CCVars.GameRoleWhitelist.Name, "false"),
(CCVars.GridFill.Name, "false"),
(CCVars.PreloadGrids.Name, "false"),
(CCVars.ArrivalsShuttles.Name, "false"),
diff --git a/Content.Server.Database/Migrations/Postgres/20241018043329_RoleWhitelist.Designer.cs b/Content.Server.Database/Migrations/Postgres/20241018043329_RoleWhitelist.Designer.cs
new file mode 100644
index 0000000000..159af4d192
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20241018043329_RoleWhitelist.Designer.cs
@@ -0,0 +1,1896 @@
+//
+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("20241018043329_RoleWhitelist")]
+ partial class RoleWhitelist
+ {
+ ///
+ 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("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ 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")
+ .IsRequired()
+ .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")
+ .IsRequired()
+ .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.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.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("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ 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("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("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.Loadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("loadout_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_loadout");
+
+ b.HasIndex("ProfileId", "LoadoutName")
+ .IsUnique();
+
+ b.ToTable("loadout", (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("LastSeenHWId")
+ .HasColumnType("bytea")
+ .HasColumnName("last_seen_hwid");
+
+ 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.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("Backpack")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("backpack");
+
+ b.Property("CharacterName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("char_name");
+
+ b.Property("Clothing")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("clothing");
+
+ b.Property("CustomSpecieName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("custom_specie_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("Height")
+ .HasColumnType("real")
+ .HasColumnName("height");
+
+ b.Property("Markings")
+ .HasColumnType("jsonb")
+ .HasColumnName("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.Property("Width")
+ .HasColumnType("real")
+ .HasColumnName("width");
+
+ 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.RoleWhitelist", b =>
+ {
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("RoleId")
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("PlayerUserId", "RoleId")
+ .HasName("PK_role_whitelists");
+
+ b.ToTable("role_whitelists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ServerId")
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ 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("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ 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("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("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("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_hit_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_role_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ 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("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("role_unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("trait_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.Property