diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminMessageEui.cs b/Content.Client/Administration/UI/AdminRemarks/AdminMessageEui.cs
new file mode 100644
index 00000000000..06eace118d7
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/AdminMessageEui.cs
@@ -0,0 +1,38 @@
+using Content.Client.Eui;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using static Content.Shared.Administration.Notes.AdminMessageEuiMsg;
+
+namespace Content.Client.Administration.UI.AdminRemarks;
+
+[UsedImplicitly]
+public sealed class AdminMessageEui : BaseEui
+{
+ private readonly AdminMessagePopupWindow _popup;
+
+ public AdminMessageEui()
+ {
+ _popup = new AdminMessagePopupWindow();
+ _popup.OnAcceptPressed += () => SendMessage(new Accept());
+ _popup.OnDismissPressed += () => SendMessage(new Dismiss());
+ _popup.OnClose += () => SendMessage(new CloseEuiMessage());
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not AdminMessageEuiState s)
+ {
+ return;
+ }
+
+ _popup.SetMessage(s.Message);
+ _popup.SetDetails(s.AdminName, s.AddedOn);
+ _popup.Timer = s.Time;
+ }
+
+ public override void Opened()
+ {
+ _popup.OpenCentered();
+ }
+}
diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml
new file mode 100644
index 00000000000..eac7e37a86d
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs
new file mode 100644
index 00000000000..f28fd1b60b1
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/AdminMessagePopupWindow.xaml.cs
@@ -0,0 +1,75 @@
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Administration.UI.AdminRemarks;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminMessagePopupWindow : FancyWindow
+{
+ private float _timer = float.MaxValue;
+ public float Timer
+ {
+ get => _timer;
+ set
+ {
+ WaitLabel.Text = Loc.GetString("admin-notes-message-wait", ("time", MathF.Floor(value)));
+ _timer = value;
+ }
+ }
+
+ public event Action? OnDismissPressed;
+ public event Action? OnAcceptPressed;
+
+ public AdminMessagePopupWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ AcceptButton.OnPressed += OnAcceptButtonPressed;
+ DismissButton.OnPressed += OnDismissButtonPressed;
+ }
+
+ public void SetMessage(string message)
+ {
+ MessageLabel.SetMessage(message);
+ }
+
+ public void SetDetails(string adminName, DateTime addedOn)
+ {
+ AdminLabel.Text = Loc.GetString("admin-notes-message-admin", ("admin", adminName), ("date", addedOn));
+ }
+
+ private void OnDismissButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ OnDismissPressed?.Invoke();
+ Close();
+ }
+
+ private void OnAcceptButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ OnAcceptPressed?.Invoke();
+ Close();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ if (!AcceptButton.Disabled)
+ return;
+
+ if (Timer > 0.0)
+ {
+ if (Timer - args.DeltaSeconds < 0)
+ Timer = 0;
+ else
+ Timer -= args.DeltaSeconds;
+ }
+ else
+ {
+ AcceptButton.Disabled = false;
+ }
+ }
+}
diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml b/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml
new file mode 100644
index 00000000000..a2c4f47a22a
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml.cs b/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml.cs
new file mode 100644
index 00000000000..ae77ad68cbb
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/AdminRemarksWindow.xaml.cs
@@ -0,0 +1,49 @@
+using System.Linq;
+using Content.Client.Administration.UI.Notes;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Database;
+using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Administration.UI.AdminRemarks;
+
+[GenerateTypedNameReferences]
+public sealed partial class AdminRemarksWindow : FancyWindow
+{
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ private readonly SpriteSystem _sprites;
+ private readonly Dictionary<(int, NoteType), AdminNotesLine> _inputs = new();
+
+ public AdminRemarksWindow()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ _sprites = _entitySystem.GetEntitySystem();
+ }
+
+ public void SetNotes(Dictionary<(int, NoteType), SharedAdminNote> notes)
+ {
+ foreach (var (id, input) in _inputs)
+ {
+ if (notes.ContainsKey(id))
+ continue;
+ NotesContainer.RemoveChild(input);
+ _inputs.Remove(id);
+ }
+
+ foreach (var note in notes.Values.OrderByDescending(note => note.CreatedAt))
+ {
+ if (_inputs.TryGetValue((note.Id, note.NoteType), out var input))
+ {
+ input.UpdateNote(note);
+ continue;
+ }
+
+ input = new AdminNotesLine(_sprites, note);
+ NotesContainer.AddChild(input);
+ _inputs[(note.Id, note.NoteType)] = input;
+ }
+ }
+}
diff --git a/Content.Client/Administration/UI/AdminRemarks/UserNotesEui.cs b/Content.Client/Administration/UI/AdminRemarks/UserNotesEui.cs
new file mode 100644
index 00000000000..8a2bdc28f1f
--- /dev/null
+++ b/Content.Client/Administration/UI/AdminRemarks/UserNotesEui.cs
@@ -0,0 +1,34 @@
+using Content.Client.Administration.UI.Notes;
+using Content.Client.Eui;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+
+namespace Content.Client.Administration.UI.AdminRemarks;
+
+[UsedImplicitly]
+public sealed class UserNotesEui : BaseEui
+{
+ public UserNotesEui()
+ {
+ NoteWindow = new AdminRemarksWindow();
+ NoteWindow.OnClose += () => SendMessage(new CloseEuiMessage());
+ }
+
+ private AdminRemarksWindow NoteWindow { get; }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not UserNotesEuiState s)
+ {
+ return;
+ }
+
+ NoteWindow.SetNotes(s.Notes);
+ }
+
+ public override void Opened()
+ {
+ NoteWindow.OpenCentered();
+ }
+}
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml
new file mode 100644
index 00000000000..64eb7d206cf
--- /dev/null
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
new file mode 100644
index 00000000000..e6d122766e6
--- /dev/null
+++ b/Content.Client/Administration/UI/BanPanel/BanPanel.xaml.cs
@@ -0,0 +1,459 @@
+using System.Globalization;
+using System.Linq;
+using System.Net;
+using System.Net.Sockets;
+using System.Text.RegularExpressions;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Client.Stylesheets;
+using Content.Shared.Administration;
+using Content.Shared.Database;
+using Content.Shared.Roles;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.BanPanel;
+
+[GenerateTypedNameReferences]
+public sealed partial class BanPanel : DefaultWindow
+{
+ public event Action? BanSubmitted;
+ public event Action? PlayerChanged;
+ private string? PlayerUsername { get; set; }
+ private (IPAddress, int)? IpAddress { get; set; }
+ private byte[]? Hwid { get; set; }
+ private double TimeEntered { get; set; }
+ private uint Multiplier { get; set; }
+ private bool HasBanFlag { get; set; }
+ private TimeSpan? ButtonResetOn { get; set; }
+ // This is less efficient than just holding a reference to the root control and enumerating children, but you
+ // have to know how the controls are nested, which makes the code more complicated.
+ private readonly List _roleCheckboxes = new();
+
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ private enum TabNumbers
+ {
+ BasicInfo,
+ //Text,
+ Players,
+ Roles
+ }
+
+ private enum Multipliers
+ {
+ Minutes,
+ Hours,
+ Days,
+ Weeks,
+ Months,
+ Years,
+ Permanent
+ }
+
+ private enum Types
+ {
+ None,
+ Server,
+ Role
+ }
+
+ public BanPanel()
+ {
+ RobustXamlLoader.Load(this);
+ IoCManager.InjectDependencies(this);
+ PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
+ PlayerNameLine.OnFocusExit += _ => OnPlayerNameChanged();
+ PlayerCheckbox.OnPressed += _ =>
+ {
+ PlayerNameLine.Editable = PlayerCheckbox.Pressed;
+ PlayerNameLine.ModulateSelfOverride = null;
+ };
+ TimeLine.OnTextChanged += OnMinutesChanged;
+ MultiplierOption.OnItemSelected += args =>
+ {
+ MultiplierOption.SelectId(args.Id);
+ OnMultiplierChanged();
+ };
+ IpLine.OnFocusExit += _ => OnIpChanged();
+ IpCheckbox.OnPressed += _ =>
+ {
+ IpLine.Editable = IpCheckbox.Pressed;
+ OnIpChanged();
+ };
+ HwidLine.OnFocusExit += _ => OnHwidChanged();
+ HwidCheckbox.OnPressed += _ =>
+ {
+ HwidLine.Editable = HwidCheckbox.Pressed;
+ OnHwidChanged();
+ };
+ TypeOption.OnItemSelected += args =>
+ {
+ TypeOption.SelectId(args.Id);
+ OnTypeChanged();
+ };
+ LastConnCheckbox.OnPressed += args =>
+ {
+ IpLine.ModulateSelfOverride = null;
+ HwidLine.ModulateSelfOverride = null;
+ OnIpChanged();
+ OnHwidChanged();
+ };
+ SubmitButton.OnPressed += SubmitButtonOnOnPressed;
+
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-none"), (int) NoteSeverity.None);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-low"), (int) NoteSeverity.Minor);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-medium"), (int) NoteSeverity.Medium);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-high"), (int) NoteSeverity.High);
+ SeverityOption.SelectId((int) NoteSeverity.Medium);
+ SeverityOption.OnItemSelected += args => SeverityOption.SelectId(args.Id);
+
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-minutes"), (int) Multipliers.Minutes);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-hours"), (int) Multipliers.Hours);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-days"), (int) Multipliers.Days);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-weeks"), (int) Multipliers.Weeks);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-months"), (int) Multipliers.Months);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-years"), (int) Multipliers.Years);
+ MultiplierOption.AddItem(Loc.GetString("ban-panel-permanent"), (int) Multipliers.Permanent);
+ MultiplierOption.SelectId((int) Multipliers.Minutes);
+ OnMultiplierChanged();
+
+ Tabs.SetTabTitle((int) TabNumbers.BasicInfo, Loc.GetString("ban-panel-tabs-basic"));
+ //Tabs.SetTabTitle((int) TabNumbers.Text, Loc.GetString("ban-panel-tabs-reason"));
+ Tabs.SetTabTitle((int) TabNumbers.Players, Loc.GetString("ban-panel-tabs-players"));
+ Tabs.SetTabTitle((int) TabNumbers.Roles, Loc.GetString("ban-panel-tabs-role"));
+ Tabs.SetTabVisible((int) TabNumbers.Roles, false);
+
+ TypeOption.AddItem(Loc.GetString("ban-panel-select"), (int) Types.None);
+ TypeOption.AddItem(Loc.GetString("ban-panel-server"), (int) Types.Server);
+ TypeOption.AddItem(Loc.GetString("ban-panel-role"), (int) Types.Role);
+
+ ReasonTextEdit.Placeholder = new Rope.Leaf(Loc.GetString("ban-panel-reason"));
+
+ var prototypeManager = IoCManager.Resolve();
+ foreach (var proto in prototypeManager.EnumeratePrototypes())
+ {
+ CreateRoleGroup(proto.ID, proto.Roles, proto.Color);
+ }
+
+ CreateRoleGroup("Antagonist", prototypeManager.EnumeratePrototypes().Select(p => p.ID), Color.Red);
+ }
+
+ private void CreateRoleGroup(string roleName, IEnumerable roleList, Color color)
+ {
+ var outerContainer = new BoxContainer
+ {
+ Name = $"{roleName}GroupOuterBox",
+ HorizontalExpand = true,
+ VerticalExpand = true,
+ Orientation = BoxContainer.LayoutOrientation.Vertical,
+ Margin = new Thickness(4)
+ };
+ var departmentCheckbox = new CheckBox
+ {
+ Name = $"{roleName}GroupCheckbox",
+ Text = roleName,
+ Modulate = color,
+ HorizontalAlignment = HAlignment.Left
+ };
+ outerContainer.AddChild(departmentCheckbox);
+ var innerContainer = new BoxContainer
+ {
+ Name = $"{roleName}GroupInnerBox",
+ HorizontalExpand = true,
+ Orientation = BoxContainer.LayoutOrientation.Horizontal
+ };
+ departmentCheckbox.OnToggled += args =>
+ {
+ foreach (var child in innerContainer.Children)
+ {
+ if (child is CheckBox c)
+ {
+ c.Pressed = args.Pressed;
+ }
+ }
+ };
+ outerContainer.AddChild(innerContainer);
+ foreach (var role in roleList)
+ {
+ AddRoleCheckbox(role, innerContainer, departmentCheckbox);
+ }
+ RolesContainer.AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color
+ }
+ });
+ RolesContainer.AddChild(outerContainer);
+ RolesContainer.AddChild(new HSeparator());
+ }
+
+ private void AddRoleCheckbox(string role, Control container, CheckBox header)
+ {
+ var roleCheckbox = new CheckBox
+ {
+ Name = $"{role}RoleCheckbox",
+ Text = role
+ };
+ roleCheckbox.OnToggled += args =>
+ {
+ if (args is { Pressed: true, Button.Parent: { } } && args.Button.Parent.Children.Where(e => e is CheckBox).All(e => ((CheckBox) e).Pressed))
+ header.Pressed = args.Pressed;
+ else
+ header.Pressed = false;
+ };
+ container.AddChild(roleCheckbox);
+ _roleCheckboxes.Add(roleCheckbox);
+ }
+
+ public void UpdateBanFlag(bool newFlag)
+ {
+ HasBanFlag = newFlag;
+ SubmitButton.Visible = HasBanFlag;
+ ModulateSelfOverride = HasBanFlag ? Color.Red : null;
+ }
+
+ public void UpdatePlayerData(string playerName)
+ {
+ if (string.IsNullOrEmpty(playerName))
+ {
+ PlayerNameLine.ModulateSelfOverride = Color.Red;
+ ErrorLevel |= ErrorLevelEnum.PlayerName;
+ UpdateSubmitEnabled();
+ return;
+ }
+ PlayerNameLine.ModulateSelfOverride = null;
+ ErrorLevel &= ~ErrorLevelEnum.PlayerName;
+ UpdateSubmitEnabled();
+ PlayerUsername = playerName;
+ PlayerNameLine.Text = playerName;
+ }
+
+ [Flags]
+ private enum ErrorLevelEnum : byte
+ {
+ None = 0,
+ Minutes = 1 << 0,
+ PlayerName = 1 << 1,
+ IpAddress = 1 << 2,
+ Hwid = 1 << 3,
+ }
+
+ private ErrorLevelEnum ErrorLevel { get; set; }
+
+ private void OnMinutesChanged(LineEdit.LineEditEventArgs args)
+ {
+ TimeLine.Text = args.Text;
+ if (!double.TryParse(args.Text, out var result))
+ {
+ ExpiresLabel.Text = "err";
+ ErrorLevel |= ErrorLevelEnum.Minutes;
+ TimeLine.ModulateSelfOverride = Color.Red;
+ UpdateSubmitEnabled();
+ return;
+ }
+
+ ErrorLevel &= ~ErrorLevelEnum.Minutes;
+ TimeLine.ModulateSelfOverride = null;
+ TimeEntered = result;
+ UpdateSubmitEnabled();
+ UpdateExpiresLabel();
+ }
+
+ private void OnMultiplierChanged()
+ {
+ TimeLine.Editable = MultiplierOption.SelectedId != (int) Multipliers.Permanent;
+ Multiplier = MultiplierOption.SelectedId switch
+ {
+ (int) Multipliers.Minutes => 1,
+ (int) Multipliers.Hours => 60,
+ (int) Multipliers.Days => 60 * 24,
+ (int) Multipliers.Weeks => 60 * 24 * 7,
+ (int) Multipliers.Months => 60 * 24 * 30,
+ (int) Multipliers.Years => 60 * 24 * 365,
+ (int) Multipliers.Permanent => 0,
+ _ => throw new ArgumentOutOfRangeException(nameof(MultiplierOption.SelectedId), "Multiplier out of range")
+ };
+ UpdateExpiresLabel();
+ }
+
+ private void UpdateExpiresLabel()
+ {
+ var minutes = (uint) (TimeEntered * Multiplier);
+ ExpiresLabel.Text = minutes == 0
+ ? $"{Loc.GetString("admin-note-editor-expiry-label")} {Loc.GetString("server-ban-string-never")}"
+ : $"{Loc.GetString("admin-note-editor-expiry-label")} {DateTime.Now + TimeSpan.FromMinutes(minutes):yyyy/MM/dd HH:mm:ss}";
+ }
+
+ private void OnIpChanged()
+ {
+ if (LastConnCheckbox.Pressed && IpAddress is null || !IpCheckbox.Pressed)
+ {
+ IpAddress = null;
+ ErrorLevel &= ~ErrorLevelEnum.IpAddress;
+ IpLine.ModulateSelfOverride = null;
+ UpdateSubmitEnabled();
+ return;
+ }
+ var ip = IpLine.Text;
+ var hid = "0";
+ if (ip.Contains('/'))
+ {
+ var split = ip.Split('/');
+ ip = split[0];
+ hid = split[1];
+ }
+
+ if (!IPAddress.TryParse(ip, out var parsedIp) || !byte.TryParse(hid, out var hidInt) || hidInt > 128 || hidInt > 32 && parsedIp.AddressFamily == AddressFamily.InterNetwork)
+ {
+ ErrorLevel |= ErrorLevelEnum.IpAddress;
+ IpLine.ModulateSelfOverride = Color.Red;
+ UpdateSubmitEnabled();
+ return;
+ }
+
+ if (hidInt == 0)
+ hidInt = (byte) (parsedIp.AddressFamily == AddressFamily.InterNetworkV6 ? 128 : 32);
+ IpAddress = (parsedIp, hidInt);
+ ErrorLevel &= ~ErrorLevelEnum.IpAddress;
+ IpLine.ModulateSelfOverride = null;
+ UpdateSubmitEnabled();
+ }
+
+ private void OnHwidChanged()
+ {
+ var hwidString = HwidLine.Text;
+ var length = 3 * (hwidString.Length / 4) - hwidString.TakeLast(2).Count(c => c == '=');
+ Hwid = new byte[length];
+ if (HwidCheckbox.Pressed && !(string.IsNullOrEmpty(hwidString) && LastConnCheckbox.Pressed) && !Convert.TryFromBase64String(hwidString, Hwid, out _))
+ {
+ ErrorLevel |= ErrorLevelEnum.Hwid;
+ HwidLine.ModulateSelfOverride = Color.Red;
+ UpdateSubmitEnabled();
+ return;
+ }
+
+ ErrorLevel &= ~ErrorLevelEnum.Hwid;
+ HwidLine.ModulateSelfOverride = null;
+ UpdateSubmitEnabled();
+
+ if (LastConnCheckbox.Pressed || !HwidCheckbox.Pressed)
+ {
+ Hwid = null;
+ return;
+ }
+ Hwid = Convert.FromHexString(hwidString);
+ }
+
+ private void OnTypeChanged()
+ {
+ TypeOption.ModulateSelfOverride = null;
+ Tabs.SetTabVisible((int) TabNumbers.Roles, TypeOption.SelectedId == (int) Types.Role);
+ }
+
+ private void UpdateSubmitEnabled()
+ {
+ SubmitButton.Disabled = ErrorLevel != ErrorLevelEnum.None;
+ }
+
+ private void OnPlayerNameChanged()
+ {
+ if (PlayerUsername == PlayerNameLine.Text)
+ return;
+ PlayerUsername = PlayerNameLine.Text;
+ if (!PlayerCheckbox.Pressed)
+ return;
+ if (string.IsNullOrWhiteSpace(PlayerUsername))
+ ErrorLevel |= ErrorLevelEnum.PlayerName;
+ else
+ ErrorLevel &= ~ErrorLevelEnum.PlayerName;
+
+ UpdateSubmitEnabled();
+ PlayerChanged?.Invoke(PlayerUsername);
+ }
+
+ public void OnPlayerSelectionChanged(PlayerInfo? player)
+ {
+ PlayerNameLine.Text = player?.Username ?? string.Empty;
+ OnPlayerNameChanged();
+ }
+
+ private void ResetTextEditor(GUIBoundKeyEventArgs _)
+ {
+ ReasonTextEdit.ModulateSelfOverride = null;
+ ReasonTextEdit.OnKeyBindDown -= ResetTextEditor;
+ }
+
+ private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
+ {
+ string[]? roles = null;
+ if (TypeOption.SelectedId == (int) Types.Role)
+ {
+ var rolesList = new List();
+ if (_roleCheckboxes.Count == 0)
+ throw new DebugAssertException("RoleCheckboxes was empty");
+
+ rolesList.AddRange(_roleCheckboxes.Where(c => c is { Pressed: true, Text: { } }).Select(c => c.Text!));
+
+ if (rolesList.Count == 0)
+ {
+ Tabs.CurrentTab = (int) TabNumbers.Roles;
+ return;
+ }
+
+ roles = rolesList.ToArray();
+ }
+
+ if (TypeOption.SelectedId == (int) Types.None)
+ {
+ TypeOption.ModulateSelfOverride = Color.Red;
+ Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
+ return;
+ }
+
+ var reason = Rope.Collapse(ReasonTextEdit.TextRope);
+ if (string.IsNullOrWhiteSpace(reason))
+ {
+ //Tabs.CurrentTab = (int) TabNumbers.Text;
+ Tabs.CurrentTab = (int) TabNumbers.BasicInfo;
+ ReasonTextEdit.GrabKeyboardFocus();
+ ReasonTextEdit.ModulateSelfOverride = Color.Red;
+ ReasonTextEdit.OnKeyBindDown += ResetTextEditor;
+ return;
+ }
+
+ if (ButtonResetOn is null)
+ {
+ ButtonResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
+ SubmitButton.ModulateSelfOverride = Color.Red;
+ SubmitButton.Text = Loc.GetString("ban-panel-confirm");
+ return;
+ }
+
+ var player = PlayerCheckbox.Pressed ? PlayerUsername : null;
+ var useLastIp = IpCheckbox.Pressed && LastConnCheckbox.Pressed && IpAddress is null;
+ var useLastHwid = HwidCheckbox.Pressed && LastConnCheckbox.Pressed && Hwid is null;
+ var severity = (NoteSeverity) SeverityOption.SelectedId;
+ BanSubmitted?.Invoke(player, IpAddress, useLastIp, Hwid, useLastHwid, (uint) (TimeEntered * Multiplier), reason, severity, roles);
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+
+ // This checks for null for free, do not invert it as null always produces a false value
+ if (_gameTiming.CurTime > ButtonResetOn)
+ {
+ ButtonResetOn = null;
+ SubmitButton.ModulateSelfOverride = null;
+ SubmitButton.Text = Loc.GetString("ban-panel-submit");
+ }
+ }
+}
diff --git a/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
new file mode 100644
index 00000000000..0a7d88f65db
--- /dev/null
+++ b/Content.Client/Administration/UI/BanPanel/BanPanelEui.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using Content.Client.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+
+namespace Content.Client.Administration.UI.BanPanel;
+
+[UsedImplicitly]
+public sealed class BanPanelEui : BaseEui
+{
+ private BanPanel BanPanel { get; }
+
+ public BanPanelEui()
+ {
+ BanPanel = new BanPanel();
+ BanPanel.OnClose += () => SendMessage(new CloseEuiMessage());
+ BanPanel.BanSubmitted += (player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles)
+ => SendMessage(new BanPanelEuiStateMsg.CreateBanRequest(player, ip, useLastIp, hwid, useLastHwid, minutes, reason, severity, roles));
+ BanPanel.PlayerChanged += player => SendMessage(new BanPanelEuiStateMsg.GetPlayerInfoRequest(player));
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ if (state is not BanPanelEuiState s)
+ {
+ return;
+ }
+
+ BanPanel.UpdateBanFlag(s.HasBan);
+ BanPanel.UpdatePlayerData(s.PlayerName);
+ }
+
+ public override void Opened()
+ {
+ BanPanel.OpenCentered();
+ }
+
+ public override void Closed()
+ {
+ BanPanel.Close();
+ BanPanel.Dispose();
+ }
+}
diff --git a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
index 664ba220722..e1903c307b2 100644
--- a/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
+++ b/Content.Client/Administration/UI/Bwoink/BwoinkControl.xaml.cs
@@ -1,4 +1,4 @@
-using System.Linq;
+using System.Linq;
using System.Text;
using System.Threading;
using Content.Client.Administration.Managers;
@@ -123,12 +123,10 @@ namespace Content.Client.Administration.UI.Bwoink
_console.ExecuteCommand($"adminnotes \"{_currentPlayer.SessionId}\"");
};
- // ew
Ban.OnPressed += _ =>
{
- var bw = new BanWindow();
- bw.OnPlayerSelectionChanged(_currentPlayer);
- bw.Open();
+ if (_currentPlayer is not null)
+ _console.ExecuteCommand($"banpanel \"{_currentPlayer.SessionId}\"");
};
Kick.OnPressed += _ =>
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml
index 16b5f0b3e02..4ebd2d792dd 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml
+++ b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml
@@ -1,4 +1,4 @@
-
@@ -8,8 +8,8 @@
-
-
+
+
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
index 49a74342c4c..a733186f24f 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesControl.xaml.cs
@@ -1,119 +1,167 @@
-using System.Linq;
+using System.Linq;
using System.Numerics;
using Content.Shared.Administration.Notes;
+using Content.Shared.CCVar;
+using Content.Shared.Database;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
-using static Robust.Client.UserInterface.Controls.LineEdit;
+using Robust.Shared.Configuration;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesControl : Control
{
- public event Action? OnNoteChanged;
- public event Action? OnNewNoteEntered;
- public event Action? OnNoteDeleted;
+ [Dependency] private readonly IEntitySystemManager _entitySystem = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ public event Action? NoteChanged;
+ public event Action? NewNoteEntered;
+ public event Action? NoteDeleted;
private AdminNotesLinePopup? _popup;
+ private readonly SpriteSystem _sprites;
+ private readonly double _noteFreshDays;
+ private readonly double _noteStaleDays;
public AdminNotesControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
+ _sprites = _entitySystem.GetEntitySystem();
- NewNote.OnTextEntered += NewNoteEntered;
+ // There should be a warning somewhere if fresh > stale
+ // I thought about putting it here but then it would spam you every time you open notes
+ _noteFreshDays = _cfg.GetCVar(CCVars.NoteFreshDays);
+ _noteStaleDays = _cfg.GetCVar(CCVars.NoteStaleDays);
+
+ NewNoteButton.OnPressed += OnNewNoteButtonPressed;
+ ShowMoreButton.OnPressed += OnShowMoreButtonPressed;
}
- private Dictionary Inputs { get; } = new();
+ private Dictionary<(int noteId, NoteType noteType), AdminNotesLine> Inputs { get; } = new();
private bool CanCreate { get; set; }
private bool CanDelete { get; set; }
private bool CanEdit { get; set; }
+ private string PlayerName { get; set; } = "";
- private void NewNoteEntered(LineEditEventArgs args)
+ public void SetPlayerName(string playerName)
{
- if (string.IsNullOrWhiteSpace(args.Text))
- {
- return;
- }
-
- NewNote.Clear();
- OnNewNoteEntered?.Invoke(args.Text);
+ PlayerName = playerName;
}
- private void NoteSubmitted(AdminNotesLine input)
+ private void OnNewNoteButtonPressed(BaseButton.ButtonEventArgs obj)
{
- var text = input.EditText.Trim();
- if (input.OriginalMessage == text)
+ var noteEdit = new NoteEdit(null, PlayerName, CanCreate, CanEdit);
+ noteEdit.SubmitPressed += OnNoteSubmitted;
+ noteEdit.OpenCentered();
+ }
+
+ private void OnNoteSubmitted(int id, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime)
+ {
+ if (id == 0)
{
+ NewNoteEntered?.Invoke(type, message, severity, secret, expiryTime);
return;
}
- OnNoteChanged?.Invoke(input.Id, text);
+ NoteChanged?.Invoke(id, type, message, severity, secret, expiryTime);
}
private bool NoteClicked(AdminNotesLine line)
{
- ClosePopup();
-
- _popup = new AdminNotesLinePopup(line.Note, CanDelete, CanEdit);
- _popup.OnEditPressed += noteId =>
+ _popup = new AdminNotesLinePopup(line.Note, PlayerName, CanDelete, CanEdit);
+ _popup.OnEditPressed += (noteId, noteType) =>
{
- if (!Inputs.TryGetValue(noteId, out var input))
+ if (!Inputs.TryGetValue((noteId, noteType), out var input))
{
return;
}
- input.SetEditable(true);
+ var noteEdit = new NoteEdit(input.Note, PlayerName, CanCreate, CanEdit);
+ noteEdit.SubmitPressed += OnNoteSubmitted;
+ noteEdit.OpenCentered();
};
- _popup.OnDeletePressed += noteId => OnNoteDeleted?.Invoke(noteId);
+ _popup.OnDeletePressed += (noteId, noteType) => NoteDeleted?.Invoke(noteId, noteType);
var box = UIBox2.FromDimensions(UserInterfaceManager.MousePositionScaled.Position, Vector2.One);
_popup.Open(box);
return true;
}
- private void ClosePopup()
+ public void SetNotes(Dictionary<(int, NoteType), SharedAdminNote> notes)
{
- _popup?.Close();
- _popup = null;
- }
-
- public void SetNotes(Dictionary notes)
- {
- foreach (var (id, input) in Inputs)
+ foreach (var (key, input) in Inputs)
{
- if (!notes.ContainsKey(id))
+ if (!notes.ContainsKey(key))
{
- Notes.RemoveChild(input);
- Inputs.Remove(id);
+ // Yes this is slower than just updating, but new notes get added at the bottom. The user won't notice.
+ Notes.RemoveAllChildren();
+ Inputs.Clear();
+ break;
}
+ Notes.RemoveChild(input);
+ Inputs.Remove(key);
}
- foreach (var note in notes.Values.OrderBy(note => note.Id))
+ var showMoreButtonVisible = false;
+ foreach (var note in notes.Values.OrderByDescending(note => note.CreatedAt))
{
- if (Inputs.TryGetValue(note.Id, out var input))
+ if (Inputs.TryGetValue((note.Id, note.NoteType), out var input))
{
input.UpdateNote(note);
continue;
}
- input = new AdminNotesLine(note);
- input.OnSubmitted += NoteSubmitted;
+ input = new AdminNotesLine(_sprites, note);
input.OnClicked += NoteClicked;
+
+ var timeDiff = DateTime.UtcNow - note.CreatedAt;
+ float alpha;
+ if (_noteFreshDays == 0 || timeDiff.TotalDays <= _noteFreshDays)
+ {
+ alpha = 1f;
+ }
+ else if (_noteStaleDays == 0 || timeDiff.TotalDays > _noteStaleDays)
+ {
+ alpha = 0f;
+ input.Visible = false;
+ showMoreButtonVisible = true;
+ }
+ else
+ {
+ alpha = (float) (1 - Math.Clamp((timeDiff.TotalDays - _noteFreshDays) / (_noteStaleDays - _noteFreshDays), 0, 1));
+ }
+
+ input.Modulate = input.Modulate.WithAlpha(alpha);
Notes.AddChild(input);
- Inputs[note.Id] = input;
+ Inputs[(note.Id, note.NoteType)] = input;
+ ShowMoreButton.Visible = showMoreButtonVisible;
}
}
+ private void OnShowMoreButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ foreach (var input in Inputs.Values)
+ {
+ input.Modulate = input.Modulate.WithAlpha(1f);
+ input.Visible = true;
+ }
+
+ ShowMoreButton.Visible = false;
+ }
+
public void SetPermissions(bool create, bool delete, bool edit)
{
CanCreate = create;
CanDelete = delete;
CanEdit = edit;
- NewNoteLabel.Visible = create;
- NewNote.Visible = create;
+ NewNoteButton.Visible = create;
+ NewNoteButton.Disabled = !create;
}
protected override void Dispose(bool disposing)
@@ -125,21 +173,14 @@ public sealed partial class AdminNotesControl : Control
return;
}
- foreach (var input in Inputs.Values)
- {
- input.OnSubmitted -= NoteSubmitted;
- }
-
Inputs.Clear();
- NewNote.OnTextEntered -= NewNoteEntered;
+ NewNoteButton.OnPressed -= OnNewNoteButtonPressed;
if (_popup != null)
{
UserInterfaceManager.PopupRoot.RemoveChild(_popup);
}
- OnNoteChanged = null;
- OnNewNoteEntered = null;
- OnNoteDeleted = null;
+ NoteDeleted = null;
}
}
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesEui.cs b/Content.Client/Administration/UI/Notes/AdminNotesEui.cs
index 0e95209f41a..af0f55acfc6 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesEui.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesEui.cs
@@ -1,4 +1,4 @@
-using Content.Client.Eui;
+using Content.Client.Eui;
using Content.Shared.Administration.Notes;
using Content.Shared.Eui;
using JetBrains.Annotations;
@@ -14,15 +14,10 @@ public sealed class AdminNotesEui : BaseEui
NoteWindow = new AdminNotesWindow();
NoteControl = NoteWindow.Notes;
- NoteControl.OnNoteChanged += (id, text) => SendMessage(new EditNoteRequest(id, text));
- NoteControl.OnNewNoteEntered += text => SendMessage(new CreateNoteRequest(text));
- NoteControl.OnNoteDeleted += id => SendMessage(new DeleteNoteRequest(id));
- NoteWindow.OnClose += OnClosed;
- }
-
- private void OnClosed()
- {
- SendMessage(new CloseEuiMessage());
+ NoteControl.NoteChanged += (id, type, text, severity, secret, expiryTime) => SendMessage(new EditNoteRequest(id, type, text, severity, secret, expiryTime));
+ NoteControl.NewNoteEntered += (type, text, severity, secret, expiryTime) => SendMessage(new CreateNoteRequest(type, text, severity, secret, expiryTime));
+ NoteControl.NoteDeleted += (id, type) => SendMessage(new DeleteNoteRequest(id, type));
+ NoteWindow.OnClose += () => SendMessage(new CloseEuiMessage());
}
public override void Closed()
@@ -43,6 +38,7 @@ public sealed class AdminNotesEui : BaseEui
}
NoteWindow.SetTitlePlayer(s.NotedPlayerName);
+ NoteControl.SetPlayerName(s.NotedPlayerName);
NoteControl.SetNotes(s.Notes);
NoteControl.SetPermissions(s.CanCreate, s.CanDelete, s.CanEdit);
}
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml
index 579645aa6d1..aca2d3a2357 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml
@@ -1,5 +1,23 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
index 476747e07ed..5852653cdb9 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLine.xaml.cs
@@ -1,85 +1,172 @@
-using Content.Shared.Administration.Notes;
+using System.Text;
+using Content.Client.Resources;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Database;
using Robust.Client.AutoGenerated;
+using Robust.Client.GameObjects;
+using Robust.Client.Graphics;
+using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Input;
-using static Robust.Client.UserInterface.Controls.LineEdit;
+using Robust.Shared.Utility;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesLine : BoxContainer
{
- private RichTextLabel? _label;
- private LineEdit? _edit;
+ private readonly SpriteSystem _sprites;
- public AdminNotesLine(SharedAdminNote note)
+ private const string AdminNotesTextureBase = "/Textures/Interface/AdminNotes/";
+ private static readonly Dictionary SeverityIcons = new()
+ {
+ { NoteSeverity.None, AdminNotesTextureBase + "none_button.png" },
+ { NoteSeverity.Minor, AdminNotesTextureBase + "minor_button.png" },
+ { NoteSeverity.Medium, AdminNotesTextureBase + "medium_button.png" },
+ { NoteSeverity.High, AdminNotesTextureBase + "high_button.png" },
+ };
+ private static readonly Dictionary NoteTypeIcons = new()
+ {
+ { NoteType.Message, AdminNotesTextureBase + "message.png" },
+ { NoteType.Watchlist, AdminNotesTextureBase + "watchlist.png" },
+ };
+
+ public AdminNotesLine(SpriteSystem sprites, SharedAdminNote note)
{
RobustXamlLoader.Load(this);
+ _sprites = sprites;
Note = note;
MouseFilter = MouseFilterMode.Pass;
- AddLabel();
+ Separator.Visible = true;
+ Refresh();
}
public SharedAdminNote Note { get; private set; }
public int Id => Note.Id;
- public string OriginalMessage => Note.Message;
- public string EditText => _edit?.Text ?? OriginalMessage;
- public event Action? OnSubmitted;
public event Func? OnClicked;
- private void AddLabel()
+ ///
+ /// Attempts to refresh the current note line with new data. The note it draws data on is stored in
+ ///
+ private void Refresh()
{
- if (_edit != null)
- {
- _edit.OnTextEntered -= Submitted;
- _edit.OnFocusExit -= Submitted;
+ string? iconPath;
+ if(Note.NoteSeverity is not null)
+ SeverityIcons.TryGetValue(Note.NoteSeverity.Value, out iconPath);
+ else
+ NoteTypeIcons.TryGetValue(Note.NoteType, out iconPath);
- RemoveChild(_edit);
- _edit = null;
+ if (iconPath is null)
+ {
+ SeverityRect.Visible = false;
+ Logger.WarningS("admin.notes", $"Could not find an icon for note ID {Note.Id}");
+ }
+ else
+ {
+ SeverityRect.Texture = _sprites.Frame0(new SpriteSpecifier.Texture(new ResPath(iconPath)));
}
- _label = new RichTextLabel();
- _label.SetMessage(Note.Message);
+ TimeLabel.Text = Note.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss");
+ ServerLabel.Text = Note.ServerName ?? "Unknown";
+ RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round;
+ AdminLabel.Text = Note.CreatedByName;
+ PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h";
- AddChild(_label);
- _label.SetPositionFirst();
-
- Separator.Visible = true;
- }
-
- private void AddLineEdit()
- {
- if (_label != null)
+ if (Note.Secret)
{
- RemoveChild(_label);
- _label = null;
+ SecretSeparator.Visible = true;
+ SecretLabel.Visible = true;
}
- _edit = new LineEdit {Text = Note.Message};
- _edit.OnTextEntered += Submitted;
- _edit.OnFocusExit += Submitted;
+ if (Note.UnbannedTime is not null)
+ {
+ ExtraLabel.Text = Loc.GetString("admin-notes-unbanned", ("admin", Note.UnbannedByName ?? "[error]"), ("date", Note.UnbannedTime));
+ ExtraLabel.Visible = true;
+ }
+ else if (Note.ExpiryTime is not null)
+ {
+ // Notes should never be visible when expired, bans should
+ if (Note.ExpiryTime.Value > DateTime.UtcNow)
+ {
+ ExpiresLabel.Text = Loc.GetString("admin-note-editor-expiry-label-params",
+ ("date", Note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss")),
+ ("expiresIn", (Note.ExpiryTime.Value - DateTime.UtcNow).ToString("d'd 'hh':'mm")));
+ ExpiresLabel.Modulate = Color.FromHex("#86DC3D");
+ }
+ else
+ {
+ ExpiresLabel.Text = Loc.GetString("admin-note-editor-expiry-label-expired");
+ }
+ ExpiresLabel.Visible = true;
+ }
- AddChild(_edit);
- _edit.SetPositionFirst();
- _edit.GrabKeyboardFocus();
- _edit.CursorPosition = _edit.Text.Length;
+ if (Note.LastEditedAt > Note.CreatedAt)
+ {
+ EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt));
+ EditedLabel.Visible = true;
+ }
- Separator.Visible = false;
+ switch (Note.NoteType)
+ {
+ case NoteType.ServerBan:
+ NoteLabel.SetMessage(FormatBanMessage());
+ break;
+ case NoteType.RoleBan:
+ NoteLabel.SetMessage(FormatRoleBanMessage());
+ break;
+ case NoteType.Note:
+ case NoteType.Watchlist:
+ case NoteType.Message:
+ default:
+ NoteLabel.SetMessage(Note.Message);
+ break;
+ }
+
+ if (Note.Seen == true)
+ {
+ ExtraLabel.Text = Loc.GetString("admin-notes-message-seen");
+ ExtraLabel.Visible = true;
+ }
}
- private void Submitted(LineEditEventArgs args)
+ private string FormatBanMessage()
{
- OnSubmitted?.Invoke(this);
+ var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {Loc.GetString("admin-notes-the-server")} ");
+ return FormatBanMessageCommon(banMessage);
+ }
- AddLabel();
+ private string FormatRoleBanMessage()
+ {
+ var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
+ return FormatBanMessageCommon(banMessage);
+ }
- var note = Note with {Message = args.Text};
- UpdateNote(note);
+ private string FormatBanMessageCommon(StringBuilder sb)
+ {
+ if (Note.ExpiryTime is null)
+ {
+ sb.Append(Loc.GetString("admin-notes-permanently"));
+ }
+ else
+ {
+ sb.Append("for ");
+ var banLength = Note.ExpiryTime.Value - Note.CreatedAt;
+ if (banLength.Days > 0)
+ sb.Append(Loc.GetString("admin-notes-days", ("days", banLength.TotalDays.ToString(".00"))));
+ else if (banLength.Hours > 0)
+ sb.Append(Loc.GetString("admin-notes-hours", ("hours", banLength.TotalHours.ToString(".00"))));
+ else
+ sb.Append(Loc.GetString("admin-notes-minutes", ("minutes", banLength.TotalMinutes.ToString(".00"))));
+ }
+
+ sb.Append(" - ");
+ sb.Append(Note.Message);
+ return sb.ToString();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
@@ -101,24 +188,7 @@ public sealed partial class AdminNotesLine : BoxContainer
public void UpdateNote(SharedAdminNote note)
{
Note = note;
- _label?.SetMessage(note.Message);
-
- if (_edit != null && _edit.Text != note.Message)
- {
- _edit.Text = note.Message;
- }
- }
-
- public void SetEditable(bool editable)
- {
- if (editable)
- {
- AddLineEdit();
- }
- else
- {
- AddLabel();
- }
+ Refresh();
}
protected override void Dispose(bool disposing)
@@ -130,13 +200,6 @@ public sealed partial class AdminNotesLine : BoxContainer
return;
}
- if (_edit != null)
- {
- _edit.OnTextEntered -= Submitted;
- _edit.OnFocusExit -= Submitted;
- }
-
- OnSubmitted = null;
OnClicked = null;
}
}
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
index 48a0baff117..ba9da8eff36 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml
@@ -1,16 +1,21 @@
-
-
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs
index 2965aaebc98..d78f2aac495 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesLinePopup.xaml.cs
@@ -1,7 +1,9 @@
-using Content.Shared.Administration.Notes;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Database;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Administration.UI.Notes;
@@ -9,57 +11,89 @@ namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
public sealed partial class AdminNotesLinePopup : Popup
{
- public event Action? OnEditPressed;
- public event Action? OnDeletePressed;
+ public event Action? OnEditPressed;
+ public event Action? OnDeletePressed;
- public AdminNotesLinePopup(SharedAdminNote note, bool showDelete, bool showEdit)
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+
+ public AdminNotesLinePopup(SharedAdminNote note, string playerName, bool showDelete, bool showEdit)
{
RobustXamlLoader.Load(this);
NoteId = note.Id;
+ NoteType = note.NoteType;
DeleteButton.Visible = showDelete;
EditButton.Visible = showEdit;
UserInterfaceManager.ModalRoot.AddChild(this);
+ PlayerNameLabel.Text = Loc.GetString("admin-notes-for", ("player", playerName));
IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id));
+ TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType));
+ SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None));
RoundIdLabel.Text = note.Round == null
? Loc.GetString("admin-notes-round-id-unknown")
: Loc.GetString("admin-notes-round-id", ("id", note.Round));
CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName));
- CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToString("dd MMM yyyy HH:mm:ss")));
+ CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToString("yyyy-MM-dd HH:mm:ss")));
EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName));
- EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt.ToString("dd MMM yyyy HH:mm:ss")));
+ EditedAtLabel.Text = Loc.GetString("admin-notes-last-edited-at", ("date", note.LastEditedAt?.ToString("yyyy-MM-dd HH:mm:ss") ?? Loc.GetString("admin-notes-edited-never")));
+ ExpiryTimeLabel.Text = note.ExpiryTime == null
+ ? Loc.GetString("admin-notes-expires-never")
+ : Loc.GetString("admin-notes-expires", ("expires", note.ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss")));
+ NoteTextEdit.InsertAtCursor(note.Message);
+
+ if (note.NoteType is NoteType.ServerBan or NoteType.RoleBan)
+ {
+ DeleteButton.Text = Loc.GetString("admin-notes-hide");
+ }
EditButton.OnPressed += EditPressed;
DeleteButton.OnPressed += DeletePressed;
}
private int NoteId { get; }
- private bool ConfirmingDelete { get; set; }
+ private NoteType NoteType { get; }
+ private TimeSpan? DeleteResetOn { get; set; }
private void EditPressed(ButtonEventArgs args)
{
- OnEditPressed?.Invoke(NoteId);
+ OnEditPressed?.Invoke(NoteId, NoteType);
Close();
}
private void DeletePressed(ButtonEventArgs args)
{
- if (!ConfirmingDelete)
+ if (DeleteResetOn is null)
{
- ConfirmingDelete = true;
+ DeleteResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
DeleteButton.Text = Loc.GetString("admin-notes-delete-confirm");
DeleteButton.ModulateSelfOverride = Color.Red;
return;
}
- ConfirmingDelete = false;
- DeleteButton.ModulateSelfOverride = null;
- OnDeletePressed?.Invoke(NoteId);
+ ResetDeleteButton();
+ OnDeletePressed?.Invoke(NoteId, NoteType);
Close();
}
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ // This checks for null for free, do not invert it as null always produces a false value
+ if (DeleteResetOn < _gameTiming.CurTime)
+ {
+ ResetDeleteButton();
+ DeleteResetOn = null;
+ }
+ }
+
+ private void ResetDeleteButton()
+ {
+ DeleteButton.Text = Loc.GetString("admin-notes-delete");
+ DeleteButton.ModulateSelfOverride = null;
+ }
+
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml b/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml
index e56ec4fa4bc..98ee6a14c25 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml
+++ b/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml
@@ -1,5 +1,7 @@
-
-
-
+ xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+ SetSize="600 400"
+ Title="Loading...">
+
+
diff --git a/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml.cs b/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml.cs
index 76d1926ec5b..163b412c4c3 100644
--- a/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml.cs
+++ b/Content.Client/Administration/UI/Notes/AdminNotesWindow.xaml.cs
@@ -1,11 +1,11 @@
-using Robust.Client.AutoGenerated;
-using Robust.Client.UserInterface.CustomControls;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.Notes;
[GenerateTypedNameReferences]
-public sealed partial class AdminNotesWindow : DefaultWindow
+public sealed partial class AdminNotesWindow : FancyWindow
{
public AdminNotesWindow()
{
diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml b/Content.Client/Administration/UI/Notes/NoteEdit.xaml
new file mode 100644
index 00000000000..506abac540c
--- /dev/null
+++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
new file mode 100644
index 00000000000..23dd1b83ecd
--- /dev/null
+++ b/Content.Client/Administration/UI/Notes/NoteEdit.xaml.cs
@@ -0,0 +1,242 @@
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Administration.Notes;
+using Content.Shared.Database;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Console;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Administration.UI.Notes;
+
+[GenerateTypedNameReferences]
+public sealed partial class NoteEdit : FancyWindow
+{
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly IClientConsoleHost _console = default!;
+
+ public event Action? SubmitPressed;
+
+ public NoteEdit(SharedAdminNote? note, string playerName, bool canCreate, bool canEdit)
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+ PlayerName = playerName;
+ Title = Loc.GetString("admin-note-editor-title-new", ("player", PlayerName));
+
+ ResetSubmitButton();
+
+ TypeOption.AddItem(Loc.GetString("admin-note-editor-type-note"), (int) NoteType.Note);
+ TypeOption.AddItem(Loc.GetString("admin-note-editor-type-message"), (int) NoteType.Message);
+ TypeOption.AddItem(Loc.GetString("admin-note-editor-type-watchlist"), (int) NoteType.Watchlist);
+ TypeOption.OnItemSelected += OnTypeChanged;
+
+
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-none"), (int) Shared.Database.NoteSeverity.None);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-low"), (int) Shared.Database.NoteSeverity.Minor);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-medium"), (int) Shared.Database.NoteSeverity.Medium);
+ SeverityOption.AddItem(Loc.GetString("admin-note-editor-severity-high"), (int) Shared.Database.NoteSeverity.High);
+ SeverityOption.OnItemSelected += OnSeverityChanged;
+
+ PermanentCheckBox.OnPressed += OnPermanentPressed;
+ SecretCheckBox.OnPressed += OnSecretPressed;
+ SubmitButton.OnPressed += OnSubmitButtonPressed;
+
+ if (note is null && !canCreate)
+ {
+ SubmitButton.Disabled = true;
+ TypeOption.Disabled = true;
+ SeverityOption.Disabled = true;
+ }
+
+ if (note is not null)
+ {
+ Title = Loc.GetString("admin-note-editor-title-existing", ("id", note.Id), ("player", PlayerName), ("author", note.CreatedByName));
+ NoteId = note.Id;
+
+ NoteType = note.NoteType;
+ TypeOption.AddItem(Loc.GetString("admin-note-editor-type-server-ban"), (int) NoteType.ServerBan);
+ TypeOption.AddItem(Loc.GetString("admin-note-editor-type-role-ban"), (int) NoteType.RoleBan);
+ TypeOption.SelectId((int)NoteType);
+ TypeOption.Disabled = true;
+
+ NoteTextEdit.InsertAtCursor(note.Message);
+
+ NoteSeverity = note.NoteSeverity ?? Shared.Database.NoteSeverity.Minor;
+ SeverityOption.SelectId((int)NoteSeverity);
+ SeverityOption.Disabled = note.NoteType is not (NoteType.Note or NoteType.ServerBan or NoteType.RoleBan);
+
+ IsSecret = note.Secret;
+ SecretCheckBox.Pressed = note.Secret;
+ SecretCheckBox.Disabled = note.NoteType is not NoteType.Note;
+ ExpiryTime = note.ExpiryTime;
+ if (ExpiryTime is not null)
+ {
+ PermanentCheckBox.Pressed = false;
+ UpdatePermanentCheckboxFields();
+ ExpiryLineEdit.Text = ExpiryTime.Value.ToString("yyyy-MM-dd HH:mm:ss");
+ }
+
+ if (!canEdit)
+ {
+ SubmitButton.Disabled = true;
+ }
+ }
+ }
+
+ private string PlayerName { get; }
+ private int NoteId { get; }
+ private bool IsSecret { get; set; }
+ private NoteType NoteType { get; set; }
+ private NoteSeverity? NoteSeverity { get; set; } = Shared.Database.NoteSeverity.None;
+ private DateTime? ExpiryTime { get; set; }
+ private TimeSpan? DeleteResetOn { get; set; }
+
+ private void OnTypeChanged(OptionButton.ItemSelectedEventArgs args)
+ {
+ // We should be resetting the underlying values too but the server handles that anyway
+ switch (args.Id)
+ {
+ case (int) NoteType.Note: // Note: your standard note, does nothing special
+ NoteType = NoteType.Note;
+ SecretCheckBox.Disabled = false;
+ SecretCheckBox.Pressed = false;
+ SeverityOption.Disabled = false;
+ PermanentCheckBox.Pressed = true;
+ UpdatePermanentCheckboxFields();
+ break;
+ case (int) NoteType.Message: // Message: these are shown to the player when they log on
+ NoteType = NoteType.Message;
+ SecretCheckBox.Disabled = true;
+ SecretCheckBox.Pressed = false;
+ SeverityOption.Disabled = true;
+ SeverityOption.SelectId((int) Shared.Database.NoteSeverity.None);
+ NoteSeverity = null;
+ PermanentCheckBox.Pressed = false;
+ UpdatePermanentCheckboxFields();
+ break;
+ case (int) NoteType.Watchlist: // Watchlist: these are always secret and only shown to admins when the player logs on
+ NoteType = NoteType.Watchlist;
+ SecretCheckBox.Disabled = true;
+ SecretCheckBox.Pressed = true;
+ SeverityOption.Disabled = true;
+ SeverityOption.SelectId((int) Shared.Database.NoteSeverity.None);
+ NoteSeverity = null;
+ PermanentCheckBox.Pressed = false;
+ UpdatePermanentCheckboxFields();
+ break;
+ default: // Wuh oh
+ throw new ArgumentOutOfRangeException(nameof(args.Id), args.Id, "Unknown note type");
+ }
+
+ TypeOption.SelectId(args.Id);
+ }
+
+ private void OnPermanentPressed(BaseButton.ButtonEventArgs _)
+ {
+ UpdatePermanentCheckboxFields();
+ }
+
+ private void UpdatePermanentCheckboxFields()
+ {
+ ExpiryLabel.Visible = !PermanentCheckBox.Pressed;
+ ExpiryLineEdit.Visible = !PermanentCheckBox.Pressed;
+
+ ExpiryLineEdit.Text = !PermanentCheckBox.Pressed ? DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty;
+ }
+
+ private void OnSecretPressed(BaseButton.ButtonEventArgs _)
+ {
+ IsSecret = SecretCheckBox.Pressed;
+ }
+
+ private void OnSeverityChanged(OptionButton.ItemSelectedEventArgs args)
+ {
+ NoteSeverity = (NoteSeverity) args.Id;
+ SeverityOption.SelectId(args.Id);
+ }
+
+ private void OnSubmitButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ if (!ParseExpiryTime())
+ return;
+ if (DeleteResetOn is null)
+ {
+ DeleteResetOn = _gameTiming.CurTime.Add(TimeSpan.FromSeconds(3));
+ SubmitButton.Text = Loc.GetString("admin-note-editor-submit-confirm");
+ SubmitButton.ModulateSelfOverride = Color.Red;
+ // Task.Delay(3000).ContinueWith(_ => ResetSubmitButton()); // TODO: fix
+ return;
+ }
+
+ ResetSubmitButton();
+
+ SubmitPressed?.Invoke(NoteId, NoteType, Rope.Collapse(NoteTextEdit.TextRope), NoteSeverity, IsSecret, ExpiryTime);
+
+ if (Parent is null)
+ {
+ _console.ExecuteCommand($"adminnotes \"{PlayerName}\"");
+ }
+ Close();
+ }
+
+ protected override void FrameUpdate(FrameEventArgs args)
+ {
+ base.FrameUpdate(args);
+ // This checks for null for free, do not invert it as null always produces a false value
+ if (DeleteResetOn > _gameTiming.CurTime)
+ {
+ ResetSubmitButton();
+ DeleteResetOn = null;
+ }
+ }
+
+ private void ResetSubmitButton()
+ {
+ SubmitButton.Text = Loc.GetString("admin-note-editor-submit");
+ SubmitButton.ModulateSelfOverride = null;
+ UpdateDraw();
+ }
+
+ ///
+ /// Tries to parse the currently entered expiry time. As a side effect this function
+ /// will colour its respective line edit to indicate an error
+ ///
+ /// True if parsing was successful, false if not
+ private bool ParseExpiryTime()
+ {
+ // If the checkbox is pressed the note is permanent, so expiry is null
+ if (PermanentCheckBox.Pressed)
+ {
+ ExpiryTime = null;
+ return true;
+ }
+
+ if (string.IsNullOrWhiteSpace(ExpiryLineEdit.Text) || !DateTime.TryParse(ExpiryLineEdit.Text, out var result) || DateTime.UtcNow > result)
+ {
+ ExpiryLineEdit.ModulateSelfOverride = Color.Red;
+ return false;
+ }
+
+ ExpiryTime = result;
+ ExpiryLineEdit.ModulateSelfOverride = null;
+ return true;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (!disposing)
+ {
+ return;
+ }
+
+ PermanentCheckBox.OnPressed -= OnPermanentPressed;
+ SecretCheckBox.OnPressed -= OnSecretPressed;
+ SubmitButton.OnPressed -= OnSubmitButtonPressed;
+
+ SubmitPressed = null;
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index 547e8d01a93..8b68487547f 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -1,4 +1,4 @@
-
-
+
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml
deleted file mode 100644
index 9eb60ec5781..00000000000
--- a/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml.cs b/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml.cs
deleted file mode 100644
index f3ac70bbef6..00000000000
--- a/Content.Client/Administration/UI/Tabs/AdminTab/BanWindow.xaml.cs
+++ /dev/null
@@ -1,84 +0,0 @@
-using Content.Shared.Administration;
-using JetBrains.Annotations;
-using Robust.Client.AutoGenerated;
-using Robust.Client.Console;
-using Robust.Client.UserInterface.Controls;
-using Robust.Client.UserInterface.CustomControls;
-using Robust.Client.UserInterface.XAML;
-using Robust.Shared.IoC;
-using Robust.Shared.Utility;
-using static Robust.Client.UserInterface.Controls.LineEdit;
-
-namespace Content.Client.Administration.UI.Tabs.AdminTab
-{
- [GenerateTypedNameReferences]
- [UsedImplicitly]
- public sealed partial class BanWindow : DefaultWindow
- {
- public BanWindow()
- {
- RobustXamlLoader.Load(this);
- PlayerNameLine.OnTextChanged += _ => OnPlayerNameChanged();
- PlayerList.OnSelectionChanged += OnPlayerSelectionChanged;
- SubmitButton.OnPressed += SubmitButtonOnOnPressed;
- MinutesLine.OnTextChanged += UpdateButtonsText;
- HourButton.OnPressed += _ => AddMinutes(60);
- DayButton.OnPressed += _ => AddMinutes(1440);
- WeekButton.OnPressed += _ => AddMinutes(10080);
- MonthButton.OnPressed += _ => AddMinutes(43200);
- }
-
- private bool TryGetMinutes(string str, out uint minutes)
- {
- if(string.IsNullOrWhiteSpace(str))
- {
- minutes = 0;
- return true;
- }
-
- return uint.TryParse(str, out minutes);
- }
-
- private void AddMinutes(uint add)
- {
- if (!TryGetMinutes(MinutesLine.Text, out var minutes))
- return;
-
- MinutesLine.Text = $"{minutes + add}";
- UpdateButtons(minutes+add);
- }
-
- private void UpdateButtonsText(LineEditEventArgs obj)
- {
- if (!TryGetMinutes(obj.Text, out var minutes))
- return;
- UpdateButtons(minutes);
- }
-
- private void UpdateButtons(uint minutes)
- {
- HourButton.Text = $"+1h ({minutes / 60})";
- DayButton.Text = $"+1d ({minutes / 1440})";
- WeekButton.Text = $"+1w ({minutes / 10080})";
- MonthButton.Text = $"+1M ({minutes / 43200})";
- }
-
- private void OnPlayerNameChanged()
- {
- SubmitButton.Disabled = string.IsNullOrEmpty(PlayerNameLine.Text);
- }
-
- public void OnPlayerSelectionChanged(PlayerInfo? player)
- {
- PlayerNameLine.Text = player?.Username ?? string.Empty;
- OnPlayerNameChanged();
- }
-
- private void SubmitButtonOnOnPressed(BaseButton.ButtonEventArgs obj)
- {
- // Small verification if Player Name exists
- IoCManager.Resolve().ExecuteCommand(
- $"ban \"{PlayerNameLine.Text}\" \"{CommandParsing.Escape(ReasonLine.Text)}\" {MinutesLine.Text}");
- }
- }
-}
diff --git a/Content.Client/Info/RulesPopup.xaml.cs b/Content.Client/Info/RulesPopup.xaml.cs
index 266c35dbe29..1e090049366 100644
--- a/Content.Client/Info/RulesPopup.xaml.cs
+++ b/Content.Client/Info/RulesPopup.xaml.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
diff --git a/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs
new file mode 100644
index 00000000000..c020b62da1f
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.Designer.cs
@@ -0,0 +1,1770 @@
+//
+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;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20230503001749_AdminNotesImprovement")]
+ partial class AdminNotesImprovement
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "7.0.4")
+ .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("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_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("Id", "RoundId")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_log_round_id");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogEntity", b =>
+ {
+ b.Property("Uid")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("uid");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Uid"));
+
+ b.Property("AdminLogId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("AdminLogRoundId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_round_id");
+
+ b.Property("Name")
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Uid")
+ .HasName("PK_admin_log_entity");
+
+ b.HasIndex("AdminLogId", "AdminLogRoundId")
+ .HasDatabaseName("IX_admin_log_entity_admin_log_id_admin_log_round_id");
+
+ b.ToTable("admin_log_entity", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("PlayerUserId", "LogId", "RoundId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("LogId", "RoundId");
+
+ 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("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", (string)null);
+ });
+
+ 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("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ 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.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.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("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("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.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("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("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("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.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.HasKey("Id")
+ .HasName("PK_round");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_round_server_id");
+
+ 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("TraitName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trait_name");
+
+ b.HasKey("Id")
+ .HasName("PK_trait");
+
+ b.HasIndex("ProfileId", "TraitName")
+ .IsUnique();
+
+ b.ToTable("trait", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("uploaded_resource_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("data");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("path");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_uploaded_resource_log");
+
+ b.ToTable("uploaded_resource_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_whitelist");
+
+ b.ToTable("whitelist", (string)null);
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.Property("PlayersId")
+ .HasColumnType("integer")
+ .HasColumnName("players_id");
+
+ b.Property("RoundsId")
+ .HasColumnType("integer")
+ .HasColumnName("rounds_id");
+
+ b.HasKey("PlayersId", "RoundsId")
+ .HasName("PK_player_round");
+
+ b.HasIndex("RoundsId")
+ .HasDatabaseName("IX_player_round_rounds_id");
+
+ b.ToTable("player_round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+ .WithMany("Admins")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+ b.Navigation("AdminRank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.Admin", "Admin")
+ .WithMany("Flags")
+ .HasForeignKey("AdminId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+ b.Navigation("Admin");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany("AdminLogs")
+ .HasForeignKey("RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_round_round_id");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogEntity", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminLog", null)
+ .WithMany("Entities")
+ .HasForeignKey("AdminLogId", "AdminLogRoundId")
+ .HasConstraintName("FK_admin_log_entity_admin_log_admin_log_id_admin_log_round_id");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminLogs")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.AdminLog", "Log")
+ .WithMany("Players")
+ .HasForeignKey("LogId", "RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_player_admin_log_log_id_round_id");
+
+ b.Navigation("Log");
+
+ b.Navigation("Player");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminMessagesCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminMessagesDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminMessagesLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminMessagesReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_messages_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminNotesCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminNotesDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminNotesLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminNotesReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_notes_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "Rank")
+ .WithMany("Flags")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+ b.Navigation("Rank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminWatchlistsCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminWatchlistsDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminWatchlistsLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminWatchlistsReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Antags")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_antag_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Jobs")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_job_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.HasOne("Content.Server.Database.Preference", "Preference")
+ .WithMany("Profiles")
+ .HasForeignKey("PreferenceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_preference_preference_id");
+
+ b.Navigation("Preference");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.HasOne("Content.Server.Database.Server", "Server")
+ .WithMany("Rounds")
+ .HasForeignKey("ServerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_round_server_server_id");
+
+ b.Navigation("Server");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminServerBansCreated")
+ .HasForeignKey("BanningAdmin")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_ban_player_banning_admin");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminServerBansLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_server_ban_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerBan", "Ban")
+ .WithMany("BanHits")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+ b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+ .WithMany("BanHits")
+ .HasForeignKey("ConnectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+ b.Navigation("Ban");
+
+ b.Navigation("Connection");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminServerRoleBansCreated")
+ .HasForeignKey("BanningAdmin")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminServerRoleBansLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_server_role_ban_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerBan", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Trait", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Traits")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_trait_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", null)
+ .WithMany()
+ .HasForeignKey("PlayersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_player_players_id");
+
+ b.HasOne("Content.Server.Database.Round", null)
+ .WithMany()
+ .HasForeignKey("RoundsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_round_rounds_id");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Navigation("Entities");
+
+ b.Navigation("Players");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Navigation("Admins");
+
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Navigation("BanHits");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Navigation("AdminLogs");
+
+ b.Navigation("AdminMessagesCreated");
+
+ b.Navigation("AdminMessagesDeleted");
+
+ b.Navigation("AdminMessagesLastEdited");
+
+ b.Navigation("AdminMessagesReceived");
+
+ b.Navigation("AdminNotesCreated");
+
+ b.Navigation("AdminNotesDeleted");
+
+ b.Navigation("AdminNotesLastEdited");
+
+ b.Navigation("AdminNotesReceived");
+
+ b.Navigation("AdminServerBansCreated");
+
+ b.Navigation("AdminServerBansLastEdited");
+
+ b.Navigation("AdminServerRoleBansCreated");
+
+ b.Navigation("AdminServerRoleBansLastEdited");
+
+ b.Navigation("AdminWatchlistsCreated");
+
+ b.Navigation("AdminWatchlistsDeleted");
+
+ b.Navigation("AdminWatchlistsLastEdited");
+
+ b.Navigation("AdminWatchlistsReceived");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Preference", b =>
+ {
+ b.Navigation("Profiles");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.Navigation("Antags");
+
+ b.Navigation("Jobs");
+
+ b.Navigation("Traits");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Navigation("AdminLogs");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Server", b =>
+ {
+ b.Navigation("Rounds");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.Navigation("BanHits");
+
+ b.Navigation("Unban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.Navigation("Unban");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.cs b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.cs
new file mode 100644
index 00000000000..bfcaaabbd53
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20230503001749_AdminNotesImprovement.cs
@@ -0,0 +1,714 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ ///
+ public partial class AdminNotesImprovement : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropForeignKey(
+ name: "FK_admin_notes_player_created_by_id",
+ table: "admin_notes");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_admin_notes_player_deleted_by_id",
+ table: "admin_notes");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_admin_notes_player_last_edited_by_id",
+ table: "admin_notes");
+
+ migrationBuilder.DropForeignKey(
+ name: "FK_admin_notes_player_player_user_id",
+ table: "admin_notes");
+
+ migrationBuilder.DropCheckConstraint(
+ name: "HaveEitherAddressOrUserIdOrHWId",
+ table: "server_role_ban");
+
+ migrationBuilder.DropCheckConstraint(
+ name: "HaveEitherAddressOrUserIdOrHWId",
+ table: "server_ban");
+
+ migrationBuilder.RenameColumn(
+ name: "user_id",
+ table: "server_role_ban",
+ newName: "player_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_server_role_ban_user_id",
+ table: "server_role_ban",
+ newName: "IX_server_role_ban_player_user_id");
+
+ migrationBuilder.RenameColumn(
+ name: "user_id",
+ table: "server_ban",
+ newName: "player_user_id");
+
+ migrationBuilder.RenameIndex(
+ name: "IX_server_ban_user_id",
+ table: "server_ban",
+ newName: "IX_server_ban_player_user_id");
+
+ migrationBuilder.RenameColumn(
+ name: "shown_to_player",
+ table: "admin_notes",
+ newName: "secret");
+
+ migrationBuilder.UpdateData(
+ table: "admin_notes",
+ keyColumn: "secret",
+ keyValue: false,
+ column: "secret",
+ value: true);
+
+ migrationBuilder.AddColumn(
+ name: "hidden",
+ table: "server_role_ban",
+ type: "boolean",
+ nullable: false,
+ defaultValue: true);
+
+ migrationBuilder.AddColumn(
+ name: "last_edited_at",
+ table: "server_role_ban",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "last_edited_by_id",
+ table: "server_role_ban",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "playtime_at_note",
+ table: "server_role_ban",
+ type: "interval",
+ nullable: false,
+ defaultValue: new TimeSpan(0, 0, 0, 0, 0));
+
+ migrationBuilder.AddColumn(
+ name: "round_id",
+ table: "server_role_ban",
+ type: "integer",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "severity",
+ table: "server_role_ban",
+ type: "integer",
+ nullable: false,
+ defaultValue: 2);
+
+ migrationBuilder.AddColumn(
+ name: "hidden",
+ table: "server_ban",
+ type: "boolean",
+ nullable: false,
+ defaultValue: false);
+
+ migrationBuilder.AddColumn(
+ name: "last_edited_at",
+ table: "server_ban",
+ type: "timestamp with time zone",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "last_edited_by_id",
+ table: "server_ban",
+ type: "uuid",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "playtime_at_note",
+ table: "server_ban",
+ type: "interval",
+ nullable: false,
+ defaultValue: new TimeSpan(0, 0, 0, 0, 0));
+
+ migrationBuilder.AddColumn(
+ name: "round_id",
+ table: "server_ban",
+ type: "integer",
+ nullable: true);
+
+ migrationBuilder.AddColumn(
+ name: "severity",
+ table: "server_ban",
+ type: "integer",
+ nullable: false,
+ defaultValue: 3);
+
+ migrationBuilder.AlterColumn(
+ name: "player_user_id",
+ table: "admin_notes",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn(
+ name: "last_edited_by_id",
+ table: "admin_notes",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AlterColumn(
+ name: "created_by_id",
+ table: "admin_notes",
+ type: "uuid",
+ nullable: true,
+ oldClrType: typeof(Guid),
+ oldType: "uuid");
+
+ migrationBuilder.AddColumn