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 @@ public BwoinkControl()
_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 318216341ed..14c051915a5 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();
+
+ // 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);
- NewNote.OnTextEntered += NewNoteEntered;
+ 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;
- }
+ PlayerName = playerName;
+ }
- NewNote.Clear();
- OnNewNoteEntered?.Invoke(args.Text);
+ private void OnNewNoteButtonPressed(BaseButton.ButtonEventArgs obj)
+ {
+ var noteEdit = new NoteEdit(null, PlayerName, CanCreate, CanEdit);
+ noteEdit.SubmitPressed += OnNoteSubmitted;
+ noteEdit.OpenCentered();
}
- private void NoteSubmitted(AdminNotesLine input)
+ private void OnNoteSubmitted(int id, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime)
{
- var text = input.EditText.Trim();
- if (input.OriginalMessage == text)
+ 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 @@ protected override void Dispose(bool disposing)
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 AdminNotesEui()
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 override void HandleState(EuiStateBase state)
}
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();
+ if (Note.Secret)
+ {
+ SecretSeparator.Visible = true;
+ SecretLabel.Visible = true;
+ }
- Separator.Visible = true;
- }
+ 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;
+ }
- private void AddLineEdit()
- {
- if (_label != null)
+ if (Note.LastEditedAt > Note.CreatedAt)
{
- RemoveChild(_label);
- _label = null;
+ EditedLabel.Text = Loc.GetString("admin-notes-edited", ("author", Note.EditedByName), ("date", Note.LastEditedAt));
+ EditedLabel.Visible = true;
}
- _edit = new LineEdit {Text = Note.Message};
- _edit.OnTextEntered += Submitted;
- _edit.OnFocusExit += Submitted;
+ 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;
+ }
- AddChild(_edit);
- _edit.SetPositionFirst();
- _edit.GrabKeyboardFocus();
- _edit.CursorPosition = _edit.Text.Length;
+ if (Note.Seen == true)
+ {
+ ExtraLabel.Text = Loc.GetString("admin-notes-message-seen");
+ ExtraLabel.Visible = true;
+ }
+ }
- Separator.Visible = false;
+ private string FormatBanMessage()
+ {
+ var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {Loc.GetString("admin-notes-the-server")} ");
+ return FormatBanMessageCommon(banMessage);
}
- private void Submitted(LineEditEventArgs args)
+ private string FormatRoleBanMessage()
{
- OnSubmitted?.Invoke(this);
+ var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new []{"unknown"})} ");
+ return FormatBanMessageCommon(banMessage);
+ }
- AddLabel();
+ 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"))));
+ }
- var note = Note with {Message = args.Text};
- UpdateNote(note);
+ sb.Append(" - ");
+ sb.Append(Note.Message);
+ return sb.ToString();
}
protected override void KeyBindDown(GUIBoundKeyEventArgs args)
@@ -101,24 +188,7 @@ protected override void KeyBindDown(GUIBoundKeyEventArgs args)
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 @@ protected override void Dispose(bool disposing)
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 0ebf4d13dc3..8362f7a9f6b 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..4dd44274872 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.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,90 @@ 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)
{
+ IoCManager.InjectDependencies(this);
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..5cd3da438e4
--- /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.RealTime + 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.RealTime)
+ {
+ 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/Animations/ReusableAnimations.cs b/Content.Client/Animations/ReusableAnimations.cs
index b061cfd05f7..504b81e734e 100644
--- a/Content.Client/Animations/ReusableAnimations.cs
+++ b/Content.Client/Animations/ReusableAnimations.cs
@@ -1,19 +1,15 @@
-using System;
using System.Numerics;
+using Content.Shared.Spawners.Components;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
-using Robust.Shared.GameObjects;
-using Robust.Shared.IoC;
-using Robust.Shared.Log;
using Robust.Shared.Map;
-using Robust.Shared.Maths;
namespace Content.Client.Animations
{
public static class ReusableAnimations
{
- public static void AnimateEntityPickup(EntityUid entity, EntityCoordinates initialPosition, Vector2 finalPosition, IEntityManager? entMan = null)
+ public static void AnimateEntityPickup(EntityUid entity, EntityCoordinates initialPosition, Vector2 finalPosition, Angle initialAngle, IEntityManager? entMan = null)
{
IoCManager.Resolve(ref entMan);
@@ -38,6 +34,10 @@ public static void AnimateEntityPickup(EntityUid entity, EntityCoordinates initi
entMan.DeleteEntity(animatableClone);
};
+ var despawn = entMan.EnsureComponent(animatableClone);
+ despawn.Lifetime = 0.25f;
+ entMan.System().SetLocalRotationNoLerp(animatableClone, initialAngle);
+
animations.Play(new Animation
{
Length = TimeSpan.FromMilliseconds(125),
@@ -53,7 +53,7 @@ public static void AnimateEntityPickup(EntityUid entity, EntityCoordinates initi
new AnimationTrackProperty.KeyFrame(initialPosition.Position, 0),
new AnimationTrackProperty.KeyFrame(finalPosition, 0.125f)
}
- }
+ },
}
}, "fancy_pickup_anim");
}
diff --git a/Content.Client/DamageState/DamageStateVisualizerSystem.cs b/Content.Client/DamageState/DamageStateVisualizerSystem.cs
index fa2a2c79fc7..8ed18e7e426 100644
--- a/Content.Client/DamageState/DamageStateVisualizerSystem.cs
+++ b/Content.Client/DamageState/DamageStateVisualizerSystem.cs
@@ -40,10 +40,10 @@ protected override void OnAppearanceChange(EntityUid uid, DamageStateVisualsComp
// So they don't draw over mobs anymore
if (data == MobState.Dead)
{
- if (sprite.DrawDepth > (int) DrawDepth.FloorObjects)
+ if (sprite.DrawDepth > (int) DrawDepth.DeadMobs)
{
component.OriginalDrawDepth = sprite.DrawDepth;
- sprite.DrawDepth = (int) DrawDepth.FloorObjects;
+ sprite.DrawDepth = (int) DrawDepth.DeadMobs;
}
}
else if (component.OriginalDrawDepth != null)
diff --git a/Content.Client/Disposal/DisposalUnitComponent.cs b/Content.Client/Disposal/DisposalUnitComponent.cs
index 1ca18bedeb2..5466ea59dd9 100644
--- a/Content.Client/Disposal/DisposalUnitComponent.cs
+++ b/Content.Client/Disposal/DisposalUnitComponent.cs
@@ -3,6 +3,7 @@
namespace Content.Client.Disposal;
[RegisterComponent]
+[ComponentReference(typeof(SharedDisposalUnitComponent))]
public sealed class DisposalUnitComponent : SharedDisposalUnitComponent
{
diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs
index cff09a56ae0..7822c8c660c 100644
--- a/Content.Client/Entry/EntryPoint.cs
+++ b/Content.Client/Entry/EntryPoint.cs
@@ -111,7 +111,7 @@ public override void Init()
_prototypeManager.RegisterIgnore("htnPrimitive");
_prototypeManager.RegisterIgnore("gameMap");
_prototypeManager.RegisterIgnore("gameMapPool");
- _prototypeManager.RegisterIgnore("faction");
+ _prototypeManager.RegisterIgnore("npcFaction");
_prototypeManager.RegisterIgnore("lobbyBackground");
_prototypeManager.RegisterIgnore("advertisementsPack");
_prototypeManager.RegisterIgnore("metabolizerType");
diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs
index 0d56d4617bd..f87f75892e5 100644
--- a/Content.Client/Hands/Systems/HandsSystem.cs
+++ b/Content.Client/Hands/Systems/HandsSystem.cs
@@ -110,16 +110,16 @@ private void HandleComponentState(EntityUid uid, HandsComponent component, ref C
#region PickupAnimation
private void HandlePickupAnimation(PickupAnimationEvent msg)
{
- PickupAnimation(msg.ItemUid, msg.InitialPosition, msg.FinalPosition);
+ PickupAnimation(msg.ItemUid, msg.InitialPosition, msg.FinalPosition, msg.InitialAngle);
}
- public override void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition,
+ public override void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition, Angle initialAngle,
EntityUid? exclude)
{
- PickupAnimation(item, initialPosition, finalPosition);
+ PickupAnimation(item, initialPosition, finalPosition, initialAngle);
}
- public void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition)
+ public void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, Vector2 finalPosition, Angle initialAngle)
{
if (!_gameTiming.IsFirstTimePredicted)
return;
@@ -127,7 +127,7 @@ public void PickupAnimation(EntityUid item, EntityCoordinates initialPosition, V
if (finalPosition.EqualsApprox(initialPosition.Position, tolerance: 0.1f))
return;
- ReusableAnimations.AnimateEntityPickup(item, initialPosition, finalPosition);
+ ReusableAnimations.AnimateEntityPickup(item, initialPosition, finalPosition, initialAngle);
}
#endregion
diff --git a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
index b8c0b1cd416..17ff91c84c9 100644
--- a/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
+++ b/Content.Client/HealthAnalyzer/UI/HealthAnalyzerWindow.xaml.cs
@@ -47,6 +47,12 @@ public void Populate(HealthAnalyzerScannedUserMessage msg)
text.Append($"{Loc.GetString("disease-scanner-not-diseased")}\n");
}
+ text.Append(String.Format("Temperature: {0}\n", float.IsNaN(msg.Temperature) ? "N/A" : $"{msg.Temperature - 273f:F1} °C"));
+
+
+ text.Append(String.Format("Blood Level: {0}\n", float.IsNaN(msg.BloodLevel) ? "N/A" : $"{msg.BloodLevel * 100:F1} %"));
+
+
// Damage
text.Append($"\n{Loc.GetString("health-analyzer-window-entity-damage-total-text", ("amount", damageable.TotalDamage))}\n");
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.Client/Instruments/InstrumentSystem.cs b/Content.Client/Instruments/InstrumentSystem.cs
index 009fa6bcc52..02e3cf0da3c 100644
--- a/Content.Client/Instruments/InstrumentSystem.cs
+++ b/Content.Client/Instruments/InstrumentSystem.cs
@@ -52,13 +52,44 @@ private void OnShutdown(EntityUid uid, InstrumentComponent component, ComponentS
EndRenderer(uid, false, component);
}
+ public void SetMaster(EntityUid uid, EntityUid? masterUid)
+ {
+ if (!TryComp(uid, out InstrumentComponent? instrument))
+ return;
+
+ RaiseNetworkEvent(new InstrumentSetMasterEvent(uid, masterUid));
+ }
+
+ public void SetFilteredChannel(EntityUid uid, int channel, bool value)
+ {
+ if (!TryComp(uid, out InstrumentComponent? instrument))
+ return;
+
+ if(value)
+ instrument.Renderer?.SendMidiEvent(RobustMidiEvent.AllNotesOff((byte)channel, 0), false);
+
+ RaiseNetworkEvent(new InstrumentSetFilteredChannelEvent(uid, channel, value));
+ }
+
public override void SetupRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
- if (component is not InstrumentComponent instrument || instrument.IsRendererAlive)
+ if (component is not InstrumentComponent instrument)
+ {
return;
+ }
+
+ if (instrument.IsRendererAlive)
+ {
+ if (fromStateChange)
+ {
+ UpdateRenderer(uid, instrument);
+ }
+
+ return;
+ }
instrument.SequenceDelay = 0;
instrument.SequenceStartTick = 0;
@@ -88,17 +119,39 @@ public void UpdateRenderer(EntityUid uid, InstrumentComponent? instrument = null
return;
instrument.Renderer.TrackingEntity = uid;
+
+ instrument.Renderer.FilteredChannels.SetAll(false);
+ instrument.Renderer.FilteredChannels.Or(instrument.FilteredChannels);
+
instrument.Renderer.DisablePercussionChannel = !instrument.AllowPercussion;
instrument.Renderer.DisableProgramChangeEvent = !instrument.AllowProgramChange;
+ for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
+ {
+ if(instrument.FilteredChannels[i])
+ instrument.Renderer.SendMidiEvent(RobustMidiEvent.AllNotesOff((byte)i, 0));
+ }
+
if (!instrument.AllowProgramChange)
{
instrument.Renderer.MidiBank = instrument.InstrumentBank;
instrument.Renderer.MidiProgram = instrument.InstrumentProgram;
}
+ UpdateRendererMaster(instrument);
+
instrument.Renderer.LoopMidi = instrument.LoopMidi;
- instrument.DirtyRenderer = false;
+ }
+
+ private void UpdateRendererMaster(InstrumentComponent instrument)
+ {
+ if (instrument.Renderer == null || instrument.Master == null)
+ return;
+
+ if (!TryComp(instrument.Master, out InstrumentComponent? masterInstrument) || masterInstrument.Renderer == null)
+ return;
+
+ instrument.Renderer.Master = masterInstrument.Renderer;
}
public override void EndRenderer(EntityUid uid, bool fromStateChange, SharedInstrumentComponent? component = null)
@@ -121,7 +174,8 @@ public override void EndRenderer(EntityUid uid, bool fromStateChange, SharedInst
return;
}
- instrument.Renderer?.StopAllNotes();
+ instrument.Renderer?.SystemReset();
+ instrument.Renderer?.ClearAllEvents();
var renderer = instrument.Renderer;
@@ -162,13 +216,14 @@ public bool OpenInput(EntityUid uid, InstrumentComponent? instrument = null)
SetupRenderer(uid, false, instrument);
- if (instrument.Renderer != null && instrument.Renderer.OpenInput())
- {
- instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
- return true;
- }
+ if (instrument.Renderer == null || !instrument.Renderer.OpenInput())
+ return false;
+
+ SetMaster(uid, null);
+ instrument.MidiEventBuffer.Clear();
+ instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
+ return true;
- return false;
}
public bool OpenMidi(EntityUid uid, ReadOnlySpan data, InstrumentComponent? instrument = null)
@@ -179,12 +234,10 @@ public bool OpenMidi(EntityUid uid, ReadOnlySpan data, InstrumentComponent
SetupRenderer(uid, false, instrument);
if (instrument.Renderer == null || !instrument.Renderer.OpenMidi(data))
- {
return false;
- }
+ SetMaster(uid, null);
instrument.MidiEventBuffer.Clear();
-
instrument.Renderer.OnMidiEvent += instrument.MidiEventBuffer.Add;
return true;
}
@@ -266,12 +319,24 @@ private void OnMidiEventRx(InstrumentMidiEventEvent midiEv)
instrument.SequenceDelay = Math.Max(instrument.SequenceDelay, delta);
- var currentTick = renderer.SequencerTick;
+ SendMidiEvents(midiEv.MidiEvent, instrument);
+ }
+
+ private void SendMidiEvents(IReadOnlyList midiEvents, InstrumentComponent instrument)
+ {
+ if (instrument.Renderer == null)
+ {
+ Log.Warning($"Tried to send Midi events to an instrument without a renderer.");
+ return;
+ }
+
+ var currentTick = instrument.Renderer.SequencerTick;
// ReSharper disable once ForCanBeConvertedToForeach
- for (uint i = 0; i < midiEv.MidiEvent.Length; i++)
+ for (uint i = 0; i < midiEvents.Count; i++)
{
- var ev = midiEv.MidiEvent[i];
+ // I am surprised this doesn't take uint...
+ var ev = midiEvents[(int)i];
var scheduled = ev.Tick + instrument.SequenceDelay;
@@ -306,12 +371,14 @@ public override void Update(float frameTime)
return;
}
- foreach (var instrument in EntityManager.EntityQuery(true))
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var instrument))
{
- if (instrument.DirtyRenderer && instrument.Renderer != null)
- UpdateRenderer(instrument.Owner, instrument);
+ // For cases where the master renderer was not created yet.
+ if (instrument is { Renderer.Master: null, Master: not null })
+ UpdateRendererMaster(instrument);
- if (!instrument.IsMidiOpen && !instrument.IsInputOpen)
+ if (instrument is { IsMidiOpen: false, IsInputOpen: false })
continue;
var now = _gameTiming.RealTime;
@@ -323,10 +390,11 @@ public override void Update(float frameTime)
instrument.SentWithinASec = 0;
}
- if (instrument.MidiEventBuffer.Count == 0) continue;
+ if (instrument.MidiEventBuffer.Count == 0)
+ continue;
- var max = instrument.RespectMidiLimits ?
- Math.Min(MaxMidiEventsPerBatch, MaxMidiEventsPerSecond - instrument.SentWithinASec)
+ var max = instrument.RespectMidiLimits
+ ? Math.Min(MaxMidiEventsPerBatch, MaxMidiEventsPerSecond - instrument.SentWithinASec)
: instrument.MidiEventBuffer.Count;
if (max <= 0)
@@ -357,7 +425,7 @@ public override void Update(float frameTime)
if (eventCount == 0)
continue;
- RaiseNetworkEvent(new InstrumentMidiEventEvent(instrument.Owner, events));
+ RaiseNetworkEvent(new InstrumentMidiEventEvent(uid, events));
instrument.SentWithinASec += eventCount;
diff --git a/Content.Client/Instruments/UI/BandMenu.xaml b/Content.Client/Instruments/UI/BandMenu.xaml
new file mode 100644
index 00000000000..620f38fa972
--- /dev/null
+++ b/Content.Client/Instruments/UI/BandMenu.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/Content.Client/Instruments/UI/BandMenu.xaml.cs b/Content.Client/Instruments/UI/BandMenu.xaml.cs
new file mode 100644
index 00000000000..a225fcdfd6e
--- /dev/null
+++ b/Content.Client/Instruments/UI/BandMenu.xaml.cs
@@ -0,0 +1,45 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Instruments.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class BandMenu : DefaultWindow
+{
+ private readonly InstrumentBoundUserInterface _owner;
+
+ public BandMenu(InstrumentBoundUserInterface owner) : base()
+ {
+ RobustXamlLoader.Load(this);
+
+ _owner = owner;
+ BandList.OnItemSelected += OnItemSelected;
+ RefreshButton.OnPressed += OnRefreshPressed;
+ }
+
+ private void OnRefreshPressed(BaseButton.ButtonEventArgs obj)
+ {
+ _owner.RefreshBands();
+ }
+
+ private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
+ {
+ _owner.Instruments.SetMaster(_owner.Owner, (EntityUid)args.ItemList[args.ItemIndex].Metadata!);
+ BandList.Clear();
+ Timer.Spawn(0, Close);
+ }
+
+ public void Populate((EntityUid, string)[] nearby)
+ {
+ BandList.Clear();
+
+ foreach (var (uid, name) in nearby)
+ {
+ var item = BandList.AddItem(name, null, true, uid);
+ item.Selected = _owner.Instrument?.Master == uid;
+ }
+ }
+}
diff --git a/Content.Client/Instruments/UI/ChannelsMenu.xaml b/Content.Client/Instruments/UI/ChannelsMenu.xaml
new file mode 100644
index 00000000000..1bf46476092
--- /dev/null
+++ b/Content.Client/Instruments/UI/ChannelsMenu.xaml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs b/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs
new file mode 100644
index 00000000000..2814d415365
--- /dev/null
+++ b/Content.Client/Instruments/UI/ChannelsMenu.xaml.cs
@@ -0,0 +1,66 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Audio.Midi;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Instruments.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class ChannelsMenu : DefaultWindow
+{
+ private readonly InstrumentBoundUserInterface _owner;
+
+ public ChannelsMenu(InstrumentBoundUserInterface owner) : base()
+ {
+ RobustXamlLoader.Load(this);
+ _owner = owner;
+
+ ChannelList.OnItemSelected += OnItemSelected;
+ ChannelList.OnItemDeselected += OnItemDeselected;
+ AllButton.OnPressed += OnAllPressed;
+ ClearButton.OnPressed += OnClearPressed;
+ }
+
+ private void OnItemSelected(ItemList.ItemListSelectedEventArgs args)
+ {
+ _owner.Instruments.SetFilteredChannel(_owner.Owner, (int)ChannelList[args.ItemIndex].Metadata!, false);
+ }
+
+ private void OnItemDeselected(ItemList.ItemListDeselectedEventArgs args)
+ {
+ _owner.Instruments.SetFilteredChannel(_owner.Owner, (int)ChannelList[args.ItemIndex].Metadata!, true);
+ }
+
+ private void OnAllPressed(BaseButton.ButtonEventArgs obj)
+ {
+ foreach (var item in ChannelList)
+ {
+ // TODO: Make this efficient jfc
+ item.Selected = true;
+ }
+ }
+
+ private void OnClearPressed(BaseButton.ButtonEventArgs obj)
+ {
+ foreach (var item in ChannelList)
+ {
+ // TODO: Make this efficient jfc
+ item.Selected = false;
+ }
+ }
+
+ public void Populate()
+ {
+ ChannelList.Clear();
+
+ for (int i = 0; i < RobustMidiEvent.MaxChannels; i++)
+ {
+ var item = ChannelList.AddItem(_owner.Loc.GetString("instrument-component-channel-name",
+ ("number", i)), null, true, i);
+
+ item.Selected = !_owner.Instrument?.FilteredChannels[i] ?? false;
+ }
+ }
+}
diff --git a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
index 630345418ed..23611c3fda6 100644
--- a/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
+++ b/Content.Client/Instruments/UI/InstrumentBoundUserInterface.cs
@@ -1,22 +1,56 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Instruments.UI;
+using Content.Shared.Interaction;
+using Robust.Client.Audio.Midi;
using Robust.Client.GameObjects;
+using Robust.Client.Player;
+using Robust.Client.UserInterface;
namespace Content.Client.Instruments.UI
{
public sealed class InstrumentBoundUserInterface : BoundUserInterface
{
- [ViewVariables]
- private InstrumentMenu? _instrumentMenu;
+ [Dependency] public readonly IEntityManager Entities = default!;
+ [Dependency] public readonly IMidiManager MidiManager = default!;
+ [Dependency] public readonly IFileDialogManager FileDialogManager = default!;
+ [Dependency] public readonly IPlayerManager PlayerManager = default!;
+ [Dependency] public readonly ILocalizationManager Loc = default!;
- [ViewVariables]
- public InstrumentComponent? Instrument { get; set; }
+ public readonly InstrumentSystem Instruments = default!;
+ public readonly ActionBlockerSystem ActionBlocker = default!;
+ public readonly SharedInteractionSystem Interactions = default!;
+
+ [ViewVariables] private InstrumentMenu? _instrumentMenu;
+ [ViewVariables] private BandMenu? _bandMenu;
+ [ViewVariables] private ChannelsMenu? _channelsMenu;
+
+ [ViewVariables] public InstrumentComponent? Instrument { get; private set; }
public InstrumentBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
+ IoCManager.InjectDependencies(this);
+
+ Instruments = Entities.System();
+ ActionBlocker = Entities.System();
+ Interactions = Entities.System();
+ }
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ switch (message)
+ {
+ case InstrumentBandResponseBuiMessage bandRx:
+ _bandMenu?.Populate(bandRx.Nearby);
+ break;
+ default:
+ break;
+ }
}
protected override void Open()
{
- if (!EntMan.TryGetComponent(Owner, out var instrument)) return;
+ if (!EntMan.TryGetComponent(Owner, out var instrument))
+ return;
Instrument = instrument;
_instrumentMenu = new InstrumentMenu(this);
@@ -28,8 +62,45 @@ protected override void Open()
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
- if (!disposing) return;
+ if (!disposing)
+ return;
_instrumentMenu?.Dispose();
+ _bandMenu?.Dispose();
+ _channelsMenu?.Dispose();
+ }
+
+ public void RefreshBands()
+ {
+ SendMessage(new InstrumentBandRequestBuiMessage());
+ }
+
+ public void OpenBandMenu()
+ {
+ _bandMenu ??= new BandMenu(this);
+
+ // Refresh cache...
+ RefreshBands();
+
+ _bandMenu.OpenCenteredLeft();
+ }
+
+ public void CloseBandMenu()
+ {
+ if(_bandMenu?.IsOpen ?? false)
+ _bandMenu.Close();
+ }
+
+ public void OpenChannelsMenu()
+ {
+ _channelsMenu ??= new ChannelsMenu(this);
+ _channelsMenu.Populate();
+ _channelsMenu.OpenCenteredRight();
+ }
+
+ public void CloseChannelsMenu()
+ {
+ if(_channelsMenu?.IsOpen ?? false)
+ _channelsMenu.Close();
}
}
}
diff --git a/Content.Client/Instruments/UI/InstrumentMenu.xaml b/Content.Client/Instruments/UI/InstrumentMenu.xaml
index e90864f50c9..691e3ce5026 100644
--- a/Content.Client/Instruments/UI/InstrumentMenu.xaml
+++ b/Content.Client/Instruments/UI/InstrumentMenu.xaml
@@ -4,14 +4,20 @@
-
+
+
+
-
+
+
+
diff --git a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs
index d0901315ae0..b50ab96251e 100644
--- a/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs
+++ b/Content.Client/Instruments/UI/InstrumentMenu.xaml.cs
@@ -1,20 +1,12 @@
-using System;
using System.IO;
using System.Numerics;
using System.Threading.Tasks;
-using Content.Client.Interactable;
-using Content.Shared.ActionBlocker;
-using Content.Shared.Interaction;
-using Robust.Client.Audio.Midi;
using Robust.Client.AutoGenerated;
-using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Containers;
-using Robust.Shared.GameObjects;
using Robust.Shared.Input;
-using Robust.Shared.IoC;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton;
using Range = Robust.Client.UserInterface.Controls.Range;
@@ -24,29 +16,26 @@ namespace Content.Client.Instruments.UI
[GenerateTypedNameReferences]
public sealed partial class InstrumentMenu : DefaultWindow
{
- [Dependency] private readonly IMidiManager _midiManager = default!;
- [Dependency] private readonly IFileDialogManager _fileDialogManager = default!;
-
private readonly InstrumentBoundUserInterface _owner;
public InstrumentMenu(InstrumentBoundUserInterface owner)
{
RobustXamlLoader.Load(this);
- IoCManager.InjectDependencies(this);
_owner = owner;
if (_owner.Instrument != null)
{
_owner.Instrument.OnMidiPlaybackEnded += InstrumentOnMidiPlaybackEnded;
- Title = IoCManager.Resolve().GetComponent(_owner.Instrument.Owner).EntityName;
+ Title = _owner.Entities.GetComponent(_owner.Owner).EntityName;
LoopButton.Disabled = !_owner.Instrument.IsMidiOpen;
LoopButton.Pressed = _owner.Instrument.LoopMidi;
+ ChannelsButton.Disabled = !_owner.Instrument.IsRendererAlive;
StopButton.Disabled = !_owner.Instrument.IsMidiOpen;
PlaybackSlider.MouseFilter = _owner.Instrument.IsMidiOpen ? MouseFilterMode.Pass : MouseFilterMode.Ignore;
}
- if (!_midiManager.IsAvailable)
+ if (!_owner.MidiManager.IsAvailable)
{
UnavailableOverlay.Visible = true;
// We return early as to not give the buttons behavior.
@@ -54,8 +43,11 @@ public InstrumentMenu(InstrumentBoundUserInterface owner)
}
InputButton.OnToggled += MidiInputButtonOnOnToggled;
+ BandButton.OnPressed += BandButtonOnPressed;
+ BandButton.OnToggled += BandButtonOnToggled;
FileButton.OnPressed += MidiFileButtonOnOnPressed;
LoopButton.OnToggled += MidiLoopButtonOnOnToggled;
+ ChannelsButton.OnPressed += ChannelsButtonOnPressed;
StopButton.OnPressed += MidiStopButtonOnPressed;
PlaybackSlider.OnValueChanged += PlaybackSliderSeek;
PlaybackSlider.OnKeyBindUp += PlaybackSliderKeyUp;
@@ -63,6 +55,27 @@ public InstrumentMenu(InstrumentBoundUserInterface owner)
MinSize = SetSize = new Vector2(400, 150);
}
+ private void BandButtonOnPressed(ButtonEventArgs obj)
+ {
+ if (!PlayCheck())
+ return;
+
+ _owner.OpenBandMenu();
+ }
+
+ private void BandButtonOnToggled(ButtonToggledEventArgs obj)
+ {
+ if (obj.Pressed)
+ return;
+
+ _owner.Instruments.SetMaster(_owner.Owner, null);
+ }
+
+ private void ChannelsButtonOnPressed(ButtonEventArgs obj)
+ {
+ _owner.OpenChannelsMenu();
+ }
+
private void InstrumentOnMidiPlaybackEnded()
{
MidiPlaybackSetButtonsDisabled(true);
@@ -70,6 +83,9 @@ private void InstrumentOnMidiPlaybackEnded()
public void MidiPlaybackSetButtonsDisabled(bool disabled)
{
+ if(disabled)
+ _owner.CloseChannelsMenu();
+
LoopButton.Disabled = disabled;
StopButton.Disabled = disabled;
@@ -79,8 +95,10 @@ public void MidiPlaybackSetButtonsDisabled(bool disabled)
private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj)
{
+ _owner.CloseBandMenu();
+
var filters = new FileDialogFilters(new FileDialogFilters.Group("mid", "midi"));
- await using var file = await _fileDialogManager.OpenFile(filters);
+ await using var file = await _owner.FileDialogManager.OpenFile(filters);
// did the instrument menu get closed while waiting for the user to select a file?
if (Disposed)
@@ -89,7 +107,8 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj)
// The following checks are only in place to prevent players from playing MIDI songs locally.
// There are equivalents for these checks on the server.
- if (file == null) return;
+ if (file == null)
+ return;
/*if (!_midiManager.IsMidiFile(filename))
{
@@ -107,7 +126,7 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj)
await Task.WhenAll(Timer.Delay(100), file.CopyToAsync(memStream));
if (_owner.Instrument is not {} instrument
- || !EntitySystem.Get().OpenMidi(instrument.Owner, memStream.GetBuffer().AsSpan(0, (int) memStream.Length), instrument))
+ || !_owner.Instruments.OpenMidi(_owner.Owner, memStream.GetBuffer().AsSpan(0, (int) memStream.Length), instrument))
return;
MidiPlaybackSetButtonsDisabled(false);
@@ -117,7 +136,7 @@ private async void MidiFileButtonOnOnPressed(ButtonEventArgs obj)
private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj)
{
- var instrumentSystem = EntitySystem.Get();
+ _owner.CloseBandMenu();
if (obj.Pressed)
{
@@ -126,55 +145,58 @@ private void MidiInputButtonOnOnToggled(ButtonToggledEventArgs obj)
MidiStopButtonOnPressed(null);
if(_owner.Instrument is {} instrument)
- instrumentSystem.OpenInput(instrument.Owner, instrument);
+ _owner.Instruments.OpenInput(_owner.Owner, instrument);
+ }
+ else if (_owner.Instrument is { } instrument)
+ {
+ _owner.Instruments.CloseInput(_owner.Owner, false, instrument);
+ _owner.CloseChannelsMenu();
}
- else if(_owner.Instrument is {} instrument)
- instrumentSystem.CloseInput(instrument.Owner, false, instrument);
}
private bool PlayCheck()
{
// TODO all of these checks should also be done server-side.
- var instrumentEnt = _owner.Instrument?.Owner;
+ var instrumentEnt = _owner.Owner;
var instrument = _owner.Instrument;
- // If either the entity or component are null, return.
- if (instrumentEnt == null || instrument == null)
+ if (instrument == null)
return false;
- var localPlayer = IoCManager.Resolve().LocalPlayer;
+ var localPlayer = _owner.PlayerManager.LocalPlayer;
// If we don't have a player or controlled entity, we return.
- if (localPlayer?.ControlledEntity == null) return false;
+ if (localPlayer?.ControlledEntity == null)
+ return false;
// By default, allow an instrument to play itself and skip all other checks
if (localPlayer.ControlledEntity == instrumentEnt)
return true;
// If we're a handheld instrument, we might be in a container. Get it just in case.
- instrumentEnt.Value.TryGetContainerMan(out var conMan);
+ instrumentEnt.TryGetContainerMan(out var conMan);
// If the instrument is handheld and we're not holding it, we return.
- if ((instrument.Handheld && (conMan == null
- || conMan.Owner != localPlayer.ControlledEntity))) return false;
+ if ((instrument.Handheld && (conMan == null || conMan.Owner != localPlayer.ControlledEntity)))
+ return false;
- var entSysMan = IoCManager.Resolve();
- if (!entSysMan.GetEntitySystem().CanInteract(localPlayer.ControlledEntity.Value, instrumentEnt))
+ if (!_owner.ActionBlocker.CanInteract(localPlayer.ControlledEntity.Value, instrumentEnt))
return false;
// We check that we're in range unobstructed just in case.
- return entSysMan.GetEntitySystem().InRangeUnobstructed(localPlayer.ControlledEntity.Value, instrumentEnt.Value);
+ return _owner.Interactions.InRangeUnobstructed(localPlayer.ControlledEntity.Value, instrumentEnt);
}
private void MidiStopButtonOnPressed(ButtonEventArgs? obj)
{
MidiPlaybackSetButtonsDisabled(true);
- if (_owner.Instrument is not { } instrument)
+ if (_owner.Instrument is not {} instrument)
return;
- EntitySystem.Get().CloseMidi(instrument.Owner, false, instrument);
+ _owner.Instruments.CloseMidi(_owner.Owner, false, instrument);
+ _owner.CloseChannelsMenu();
}
private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj)
@@ -183,29 +205,45 @@ private void MidiLoopButtonOnOnToggled(ButtonToggledEventArgs obj)
return;
_owner.Instrument.LoopMidi = obj.Pressed;
- _owner.Instrument.DirtyRenderer = true;
+ _owner.Instruments.UpdateRenderer(_owner.Owner, _owner.Instrument);
}
private void PlaybackSliderSeek(Range _)
{
// Do not seek while still grabbing.
- if (PlaybackSlider.Grabbed || _owner.Instrument is not {} instrument) return;
+ if (PlaybackSlider.Grabbed || _owner.Instrument is not {} instrument)
+ return;
- EntitySystem.Get().SetPlayerTick(instrument.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument);
+ _owner.Instruments.SetPlayerTick(_owner.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument);
}
private void PlaybackSliderKeyUp(GUIBoundKeyEventArgs args)
{
- if (args.Function != EngineKeyFunctions.UIClick || _owner.Instrument is not {} instrument) return;
+ if (args.Function != EngineKeyFunctions.UIClick || _owner.Instrument is not {} instrument)
+ return;
+
+ _owner.Instruments.SetPlayerTick(_owner.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument);
+ }
- EntitySystem.Get().SetPlayerTick(instrument.Owner, (int)Math.Ceiling(PlaybackSlider.Value), instrument);
+ public override void Close()
+ {
+ base.Close();
+ _owner.CloseBandMenu();
+ _owner.CloseChannelsMenu();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
- if (_owner.Instrument == null) return;
+ if (_owner.Instrument == null)
+ return;
+
+ var hasMaster = _owner.Instrument.Master != null;
+ BandButton.ToggleMode = hasMaster;
+ BandButton.Pressed = hasMaster;
+ BandButton.Disabled = _owner.Instrument.IsMidiOpen || _owner.Instrument.IsInputOpen;
+ ChannelsButton.Disabled = !_owner.Instrument.IsRendererAlive;
if (!_owner.Instrument.IsMidiOpen)
{
@@ -214,7 +252,8 @@ protected override void FrameUpdate(FrameEventArgs args)
return;
}
- if (PlaybackSlider.Grabbed) return;
+ if (PlaybackSlider.Grabbed)
+ return;
PlaybackSlider.MaxValue = _owner.Instrument.PlayerTotalTick;
PlaybackSlider.SetValueWithoutEvent(_owner.Instrument.PlayerTick);
diff --git a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
index 981524c3313..c6b9426d7f1 100644
--- a/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
+++ b/Content.Client/ParticleAccelerator/UI/ParticleAcceleratorControlMenu.cs
@@ -409,7 +409,7 @@ private void UpdatePowerState(ParticleAcceleratorPowerState state, bool enabled,
if (_maxStrength > 3 && enabled && assembled)
{
_shouldContinueAnimating = true;
- if (!_alarmControl.Visible)
+ if (!_alarmControl.HasRunningAnimation("warningAnim"))
_alarmControl.PlayAnimation(_alarmControlAnimation, "warningAnim");
}
else
diff --git a/Content.Client/StatusIcon/StatusIconOverlay.cs b/Content.Client/StatusIcon/StatusIconOverlay.cs
index d1b556a5c72..c5add7f9bd6 100644
--- a/Content.Client/StatusIcon/StatusIconOverlay.cs
+++ b/Content.Client/StatusIcon/StatusIconOverlay.cs
@@ -35,19 +35,23 @@ protected override void Draw(in OverlayDrawArgs args)
var scaleMatrix = Matrix3.CreateScale(new Vector2(1, 1));
var rotationMatrix = Matrix3.CreateRotation(-eyeRot);
- var query = _entity.AllEntityQueryEnumerator();
- while (query.MoveNext(out var uid, out var comp, out var sprite, out var xform))
+ var query = _entity.AllEntityQueryEnumerator();
+ while (query.MoveNext(out var uid, out var comp, out var sprite, out var xform, out var meta))
{
if (xform.MapID != args.MapId)
continue;
- var icons = _statusIcon.GetStatusIcons(uid);
+ var icons = _statusIcon.GetStatusIcons(uid, meta);
if (icons.Count == 0)
continue;
var bounds = comp.Bounds ?? sprite.Bounds;
var worldPos = _transform.GetWorldPosition(xform, xformQuery);
+
+ if (!bounds.Translated(worldPos).Intersects(args.WorldAABB))
+ continue;
+
var worldMatrix = Matrix3.CreateTranslation(worldPos);
Matrix3.Multiply(scaleMatrix, worldMatrix, out var scaledWorld);
Matrix3.Multiply(rotationMatrix, scaledWorld, out var matty);
diff --git a/Content.Client/StatusIcon/StatusIconSystem.cs b/Content.Client/StatusIcon/StatusIconSystem.cs
index c5ca10735af..a6caf4a2dbb 100644
--- a/Content.Client/StatusIcon/StatusIconSystem.cs
+++ b/Content.Client/StatusIcon/StatusIconSystem.cs
@@ -53,12 +53,17 @@ private void UpdateOverlayVisible()
_overlay.AddOverlay(new StatusIconOverlay());
}
- public List GetStatusIcons(EntityUid uid)
+ public List GetStatusIcons(EntityUid uid, MetaDataComponent? meta = null)
{
- if (!Exists(uid) || Terminating(uid))
- return new();
+ var list = new List();
+ if (!Resolve(uid, ref meta))
+ return list;
- var ev = new GetStatusIconsEvent(new());
+ if (meta.EntityLifeStage >= EntityLifeStage.Terminating)
+ return list;
+
+ var inContainer = (meta.Flags & MetaDataFlags.InContainer) != 0;
+ var ev = new GetStatusIconsEvent(list, inContainer);
RaiseLocalEvent(uid, ref ev);
return ev.StatusIcons;
}
diff --git a/Content.Client/Storage/ClientStorageComponent.cs b/Content.Client/Storage/ClientStorageComponent.cs
index dfd0f26c1a3..8f1697510ab 100644
--- a/Content.Client/Storage/ClientStorageComponent.cs
+++ b/Content.Client/Storage/ClientStorageComponent.cs
@@ -11,28 +11,9 @@ namespace Content.Client.Storage
[ComponentReference(typeof(SharedStorageComponent))]
public sealed class ClientStorageComponent : SharedStorageComponent
{
- [Dependency] private readonly IEntityManager _entityManager = default!;
private List _storedEntities = new();
public override IReadOnlyList StoredEntities => _storedEntities;
- ///
- /// Animate the newly stored entities in flying towards this storage's position
- ///
- ///
- public void HandleAnimatingInsertingEntities(AnimateInsertingEntitiesEvent msg)
- {
- for (var i = 0; msg.StoredEntities.Count > i; i++)
- {
- var entity = msg.StoredEntities[i];
- var initialPosition = msg.EntityPositions[i];
-
- if (_entityManager.EntityExists(entity))
- {
- ReusableAnimations.AnimateEntityPickup(entity, initialPosition, _entityManager.GetComponent(Owner).LocalPosition, _entityManager);
- }
- }
- }
-
public override bool Remove(EntityUid entity)
{
return false;
diff --git a/Content.Client/Storage/Systems/StorageSystem.cs b/Content.Client/Storage/Systems/StorageSystem.cs
index 37dc7a94ecf..145a7dd9f0d 100644
--- a/Content.Client/Storage/Systems/StorageSystem.cs
+++ b/Content.Client/Storage/Systems/StorageSystem.cs
@@ -30,7 +30,7 @@ public void HandleAnimatingInsertingEntities(AnimateInsertingEntitiesEvent msg)
var initialPosition = msg.EntityPositions[i];
if (EntityManager.EntityExists(entity) && transformComp != null)
{
- ReusableAnimations.AnimateEntityPickup(entity, initialPosition, transformComp.LocalPosition, EntityManager);
+ ReusableAnimations.AnimateEntityPickup(entity, initialPosition, transformComp.LocalPosition, msg.EntityAngles[i], EntityManager);
}
}
}
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