diff --git a/Content.Client/Administration/Logs/AdminLogSystem.cs b/Content.Client/Administration/Logs/AdminLogSystem.cs
new file mode 100644
index 00000000000..895fd629251
--- /dev/null
+++ b/Content.Client/Administration/Logs/AdminLogSystem.cs
@@ -0,0 +1,7 @@
+using Content.Shared.Administration.Logs;
+
+namespace Content.Client.Administration.Logs;
+
+public class AdminLogSystem : SharedAdminLogSystem
+{
+}
diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogImpactButton.cs b/Content.Client/Administration/UI/CustomControls/AdminLogImpactButton.cs
new file mode 100644
index 00000000000..be4b1983471
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/AdminLogImpactButton.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Administration.Logs;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class AdminLogImpactButton : Button
+{
+ public AdminLogImpactButton(LogImpact impact)
+ {
+ Impact = impact;
+ ToggleMode = true;
+ Pressed = true;
+ }
+
+ public LogImpact Impact { get; }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs
new file mode 100644
index 00000000000..029d414a679
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/AdminLogLabel.cs
@@ -0,0 +1,33 @@
+using Content.Shared.Administration.Logs;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class AdminLogLabel : RichTextLabel
+{
+ public AdminLogLabel(ref SharedAdminLog log, HSeparator separator)
+ {
+ Log = log;
+ Separator = separator;
+
+ SetMessage(log.Message);
+ OnVisibilityChanged += VisibilityChanged;
+ }
+
+ public SharedAdminLog Log { get; }
+
+ public HSeparator Separator { get; }
+
+ private void VisibilityChanged(Control control)
+ {
+ Separator.Visible = Visible;
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ OnVisibilityChanged -= VisibilityChanged;
+ }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogPlayerButton.cs b/Content.Client/Administration/UI/CustomControls/AdminLogPlayerButton.cs
new file mode 100644
index 00000000000..a6606a066a8
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/AdminLogPlayerButton.cs
@@ -0,0 +1,17 @@
+using System;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class AdminLogPlayerButton : Button
+{
+ public AdminLogPlayerButton(Guid id)
+ {
+ Id = id;
+ ClipText = true;
+ ToggleMode = true;
+ Pressed = true;
+ }
+
+ public Guid Id { get; }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/AdminLogTypeButton.cs b/Content.Client/Administration/UI/CustomControls/AdminLogTypeButton.cs
new file mode 100644
index 00000000000..0ad83eb0eab
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/AdminLogTypeButton.cs
@@ -0,0 +1,16 @@
+using Content.Shared.Administration.Logs;
+using Robust.Client.UserInterface.Controls;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class AdminLogTypeButton : Button
+{
+ public AdminLogTypeButton(LogType type)
+ {
+ Type = type;
+ ToggleMode = true;
+ Pressed = true;
+ }
+
+ public LogType Type { get; }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/HSeparator.cs b/Content.Client/Administration/UI/CustomControls/HSeparator.cs
new file mode 100644
index 00000000000..3e7005942dc
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/HSeparator.cs
@@ -0,0 +1,25 @@
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Maths;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class HSeparator : Control
+{
+ private static readonly Color SeparatorColor = Color.FromHex("#3D4059");
+
+ public HSeparator(Color color)
+ {
+ AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color,
+ ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2
+ }
+ });
+ }
+
+ public HSeparator() : this(SeparatorColor) { }
+}
diff --git a/Content.Client/Administration/UI/CustomControls/VSeparator.cs b/Content.Client/Administration/UI/CustomControls/VSeparator.cs
new file mode 100644
index 00000000000..b413cacd495
--- /dev/null
+++ b/Content.Client/Administration/UI/CustomControls/VSeparator.cs
@@ -0,0 +1,25 @@
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Shared.Maths;
+
+namespace Content.Client.Administration.UI.CustomControls;
+
+public class VSeparator : PanelContainer
+{
+ private static readonly Color SeparatorColor = Color.FromHex("#3D4059");
+
+ public VSeparator(Color color)
+ {
+ MinSize = (2, 5);
+
+ AddChild(new PanelContainer
+ {
+ PanelOverride = new StyleBoxFlat
+ {
+ BackgroundColor = color
+ }
+ });
+ }
+
+ public VSeparator() : this(SeparatorColor) { }
+}
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsEui.cs b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
new file mode 100644
index 00000000000..a40a248f3c9
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/AdminLogsEui.cs
@@ -0,0 +1,106 @@
+using Content.Client.Eui;
+using Content.Shared.Administration;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Eui;
+using JetBrains.Annotations;
+using static Content.Shared.Administration.AdminLogsEuiMsg;
+
+namespace Content.Client.Administration.UI.Logs;
+
+[UsedImplicitly]
+public class AdminLogsEui : BaseEui
+{
+ public AdminLogsEui()
+ {
+ Window = new AdminLogsWindow();
+ Window.OnClose += () => SendMessage(new Close());
+ Window.LogSearch.OnTextEntered += _ => RequestLogs();
+ Window.RefreshButton.OnPressed += _ => RequestLogs();
+ Window.NextButton.OnPressed += _ => NextLogs();
+ }
+
+ private AdminLogsWindow Window { get; }
+
+ private bool FirstState { get; set; } = true;
+
+ private void RequestLogs()
+ {
+ var round = Window.GetSelectedRoundId();
+ var types = Window.GetSelectedLogTypes();
+ var players = Window.GetSelectedPlayerIds();
+
+ var request = new LogsRequest(
+ round,
+ types,
+ null,
+ null,
+ null,
+ players,
+ null,
+ null,
+ DateOrder.Descending);
+
+ SendMessage(request);
+ }
+
+ private void NextLogs()
+ {
+ var request = new NextLogsRequest();
+ SendMessage(request);
+ }
+
+ private void TrySetFirstState(AdminLogsEuiState state)
+ {
+ if (!FirstState)
+ {
+ return;
+ }
+
+ FirstState = false;
+ Window.SetCurrentRound(state.RoundId);
+ Window.SetRoundSpinBox(state.RoundId);
+ }
+
+ public override void Opened()
+ {
+ Window.OpenCentered();
+ }
+
+ public override void HandleState(EuiStateBase state)
+ {
+ var s = (AdminLogsEuiState) state;
+
+ TrySetFirstState(s);
+
+ if (s.IsLoading)
+ {
+ return;
+ }
+
+ Window.SetCurrentRound(s.RoundId);
+ Window.SetPlayers(s.Players);
+ }
+
+ public override void HandleMessage(EuiMessageBase msg)
+ {
+ base.HandleMessage(msg);
+
+ switch (msg)
+ {
+ case NewLogs {Replace: true} newLogs:
+ Window.SetLogs(newLogs.Logs);
+ break;
+ case NewLogs {Replace: false} newLogs:
+ Window.AddLogs(newLogs.Logs);
+ break;
+ }
+ }
+
+ public override void Closed()
+ {
+ base.Closed();
+
+ Window.Close();
+ Window.Dispose();
+ }
+}
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml b/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml
new file mode 100644
index 00000000000..345e30cd27d
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml.cs b/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml.cs
new file mode 100644
index 00000000000..c085108d958
--- /dev/null
+++ b/Content.Client/Administration/UI/Logs/AdminLogsWindow.xaml.cs
@@ -0,0 +1,439 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Content.Client.Administration.UI.CustomControls;
+using Content.Shared.Administration.Logs;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Localization;
+using static Robust.Client.UserInterface.Controls.BaseButton;
+using static Robust.Client.UserInterface.Controls.LineEdit;
+
+namespace Content.Client.Administration.UI.Logs;
+
+[GenerateTypedNameReferences]
+public partial class AdminLogsWindow : SS14Window
+{
+ private readonly Comparer _adminLogTypeButtonComparer =
+ Comparer.Create((a, b) =>
+ string.Compare(a.Type.ToString(), b.Type.ToString(), StringComparison.Ordinal));
+
+ private readonly Comparer _adminLogPlayerButtonComparer =
+ Comparer.Create((a, b) =>
+ string.Compare(a.Text, b.Text, StringComparison.Ordinal));
+
+ public AdminLogsWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ TypeSearch.OnTextChanged += TypeSearchChanged;
+ PlayerSearch.OnTextChanged += PlayerSearchChanged;
+ LogSearch.OnTextChanged += LogSearchChanged;
+
+ SelectAllTypesButton.OnPressed += SelectAllTypes;
+
+ SelectNoTypesButton.OnPressed += SelectNoTypes;
+ SelectNoPlayersButton.OnPressed += SelectNoPlayers;
+
+ RoundSpinBox.IsValid = i => i > 0 && i <= CurrentRound;
+ RoundSpinBox.ValueChanged += RoundSpinBoxChanged;
+ RoundSpinBox.InitDefaultButtons();
+
+ ResetRoundButton.OnPressed += ResetRoundPressed;
+
+ SetImpacts(Enum.GetValues().OrderBy(impact => impact).ToArray());
+ SetTypes(Enum.GetValues());
+ }
+
+ private int CurrentRound { get; set; }
+
+ private HashSet SelectedTypes { get; } = new();
+
+ private HashSet SelectedPlayers { get; } = new();
+
+ private HashSet SelectedImpacts { get; } = new();
+
+ public void SetCurrentRound(int round)
+ {
+ CurrentRound = round;
+ ResetRoundButton.Text = Loc.GetString("admin-logs-reset-with-id", ("id", round));
+ UpdateResetButton();
+ }
+
+ public void SetRoundSpinBox(int round)
+ {
+ RoundSpinBox.Value = round;
+ UpdateResetButton();
+ }
+
+ private void RoundSpinBoxChanged(object? sender, ValueChangedEventArgs args)
+ {
+ UpdateResetButton();
+ }
+
+ private void UpdateResetButton()
+ {
+ ResetRoundButton.Disabled = RoundSpinBox.Value == CurrentRound;
+ }
+
+ private void ResetRoundPressed(ButtonEventArgs args)
+ {
+ RoundSpinBox.Value = CurrentRound;
+ }
+
+ private void TypeSearchChanged(LineEditEventArgs args)
+ {
+ UpdateTypes();
+ }
+
+ private void PlayerSearchChanged(LineEditEventArgs obj)
+ {
+ UpdatePlayers();
+ }
+
+ private void LogSearchChanged(LineEditEventArgs args)
+ {
+ UpdateLogs();
+ }
+
+ private void SelectAllTypes(ButtonEventArgs obj)
+ {
+ foreach (var control in TypesContainer.Children)
+ {
+ if (control is not AdminLogTypeButton type)
+ {
+ continue;
+ }
+
+ type.Pressed = true;
+ }
+
+ UpdateLogs();
+ }
+
+ private void SelectNoTypes(ButtonEventArgs obj)
+ {
+ foreach (var control in TypesContainer.Children)
+ {
+ if (control is not AdminLogTypeButton type)
+ {
+ continue;
+ }
+
+ type.Pressed = false;
+ type.Visible = ShouldShowType(type);
+ }
+
+ UpdateLogs();
+ }
+
+ private void SelectNoPlayers(ButtonEventArgs obj)
+ {
+ foreach (var control in PlayersContainer.Children)
+ {
+ if (control is not AdminLogPlayerButton player)
+ {
+ continue;
+ }
+
+ player.Pressed = false;
+ }
+
+ UpdateLogs();
+ }
+
+ public void UpdateTypes()
+ {
+ foreach (var control in TypesContainer.Children)
+ {
+ if (control is not AdminLogTypeButton type)
+ {
+ continue;
+ }
+
+ type.Visible = ShouldShowType(type);
+ }
+ }
+
+ private void UpdatePlayers()
+ {
+ foreach (var control in PlayersContainer.Children)
+ {
+ if (control is not AdminLogPlayerButton player)
+ {
+ continue;
+ }
+
+ player.Visible = ShouldShowPlayer(player);
+ }
+ }
+
+ private void UpdateLogs()
+ {
+ foreach (var child in LogsContainer.Children)
+ {
+ if (child is not AdminLogLabel log)
+ {
+ continue;
+ }
+
+ child.Visible = ShouldShowLog(log);
+ }
+ }
+
+ private bool ShouldShowType(AdminLogTypeButton button)
+ {
+ return button.Text != null &&
+ button.Text.Contains(TypeSearch.Text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool ShouldShowPlayer(AdminLogPlayerButton button)
+ {
+ return button.Text != null &&
+ button.Text.Contains(PlayerSearch.Text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private bool ShouldShowLog(AdminLogLabel label)
+ {
+ return SelectedTypes.Contains(label.Log.Type) &&
+ SelectedPlayers.Overlaps(label.Log.Players) &&
+ SelectedImpacts.Contains(label.Log.Impact) &&
+ label.Log.Message.Contains(LogSearch.Text, StringComparison.OrdinalIgnoreCase);
+ }
+
+ private void TypeButtonPressed(ButtonEventArgs args)
+ {
+ var button = (AdminLogTypeButton) args.Button;
+ if (button.Pressed)
+ {
+ SelectedTypes.Add(button.Type);
+ }
+ else
+ {
+ SelectedTypes.Remove(button.Type);
+ }
+
+ UpdateLogs();
+ }
+
+ private void PlayerButtonPressed(ButtonEventArgs args)
+ {
+ var button = (AdminLogPlayerButton) args.Button;
+ if (button.Pressed)
+ {
+ SelectedPlayers.Add(button.Id);
+ }
+ else
+ {
+ SelectedPlayers.Remove(button.Id);
+ }
+
+ UpdateLogs();
+ }
+
+ private void ImpactButtonPressed(ButtonEventArgs args)
+ {
+ var button = (AdminLogImpactButton) args.Button;
+ if (button.Pressed)
+ {
+ SelectedImpacts.Add(button.Impact);
+ }
+ else
+ {
+ SelectedImpacts.Remove(button.Impact);
+ }
+
+ UpdateLogs();
+ }
+
+ private void SetImpacts(LogImpact[] impacts)
+ {
+ LogImpactContainer.RemoveAllChildren();
+
+ foreach (var impact in impacts)
+ {
+ var button = new AdminLogImpactButton(impact)
+ {
+ Text = impact.ToString()
+ };
+
+ SelectedImpacts.Add(impact);
+ button.OnPressed += ImpactButtonPressed;
+
+ LogImpactContainer.AddChild(button);
+ }
+
+ switch (impacts.Length)
+ {
+ case 0:
+ return;
+ case 1:
+ LogImpactContainer.GetChild(0).StyleClasses.Add("OpenRight");
+ return;
+ }
+
+ for (var i = 0; i < impacts.Length - 1; i++)
+ {
+ LogImpactContainer.GetChild(i).StyleClasses.Add("ButtonSquare");
+ }
+
+ LogImpactContainer.GetChild(LogImpactContainer.ChildCount - 1).StyleClasses.Add("OpenLeft");
+ }
+
+ private void SetTypes(LogType[] types)
+ {
+ var newTypes = types.ToHashSet();
+ var buttons = new SortedSet(_adminLogTypeButtonComparer);
+
+ foreach (var control in TypesContainer.Children.ToArray())
+ {
+ if (control is not AdminLogTypeButton type ||
+ !newTypes.Remove(type.Type))
+ {
+ continue;
+ }
+
+ buttons.Add(type);
+ }
+
+ foreach (var type in newTypes)
+ {
+ var button = new AdminLogTypeButton(type)
+ {
+ Text = type.ToString()
+ };
+
+ SelectedTypes.Add(type);
+ button.OnPressed += TypeButtonPressed;
+
+ buttons.Add(button);
+ }
+
+ TypesContainer.RemoveAllChildren();
+
+ foreach (var type in buttons)
+ {
+ TypesContainer.AddChild(type);
+ }
+
+ UpdateLogs();
+ }
+
+ public void SetPlayers(Dictionary players)
+ {
+ var buttons = new SortedSet(_adminLogPlayerButtonComparer);
+
+ foreach (var control in PlayersContainer.Children.ToArray())
+ {
+ if (control is not AdminLogPlayerButton player ||
+ !players.Remove(player.Id))
+ {
+ continue;
+ }
+
+ buttons.Add(player);
+ }
+
+ foreach (var (id, name) in players)
+ {
+ var button = new AdminLogPlayerButton(id)
+ {
+ Text = name
+ };
+
+ SelectedPlayers.Add(id);
+ button.OnPressed += PlayerButtonPressed;
+
+ buttons.Add(button);
+ }
+
+ PlayersContainer.RemoveAllChildren();
+
+ foreach (var player in buttons)
+ {
+ PlayersContainer.AddChild(player);
+ }
+
+ UpdateLogs();
+ }
+
+ public void AddLogs(SharedAdminLog[] logs)
+ {
+ for (var i = 0; i < logs.Length; i++)
+ {
+ ref var log = ref logs[i];
+ var separator = new HSeparator();
+ var label = new AdminLogLabel(ref log, separator);
+ label.Visible = ShouldShowLog(label);
+
+ LogsContainer.AddChild(label);
+ LogsContainer.AddChild(separator);
+ }
+ }
+
+ public void SetLogs(SharedAdminLog[] logs)
+ {
+ LogsContainer.RemoveAllChildren();
+ AddLogs(logs);
+ }
+
+ public int GetSelectedRoundId()
+ {
+ return RoundSpinBox.Value;
+ }
+
+ public List GetSelectedLogTypes()
+ {
+ var types = new List();
+
+ foreach (var control in TypesContainer.Children)
+ {
+ if (control is not AdminLogTypeButton {Text: { }, Pressed: true} type)
+ {
+ continue;
+ }
+
+ types.Add(Enum.Parse(type.Text));
+ }
+
+ return types;
+ }
+
+ public Guid[] GetSelectedPlayerIds()
+ {
+ var players = new List();
+
+ foreach (var control in PlayersContainer.Children)
+ {
+ if (control is not AdminLogPlayerButton {Pressed: true} player)
+ {
+ continue;
+ }
+
+ players.Add(player.Id);
+ }
+
+ return players.ToArray();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ TypeSearch.OnTextChanged -= TypeSearchChanged;
+ PlayerSearch.OnTextChanged -= PlayerSearchChanged;
+ LogSearch.OnTextChanged -= LogSearchChanged;
+
+ SelectAllTypesButton.OnPressed -= SelectAllTypes;
+
+ SelectNoTypesButton.OnPressed -= SelectNoTypes;
+ SelectNoPlayersButton.OnPressed -= SelectNoPlayers;
+
+ RoundSpinBox.IsValid = null;
+ RoundSpinBox.ValueChanged -= RoundSpinBoxChanged;
+
+ ResetRoundButton.OnPressed -= ResetRoundPressed;
+ }
+}
diff --git a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
index c6660a64e8c..a931ed553eb 100644
--- a/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
+++ b/Content.Client/Administration/UI/Tabs/AdminTab/AdminTab.xaml
@@ -14,6 +14,7 @@
+
diff --git a/Content.Client/Administration/UI/Tabs/PlayerTab.xaml.cs b/Content.Client/Administration/UI/Tabs/PlayerTab.xaml.cs
index c2a4eb3b578..17c668cec49 100644
--- a/Content.Client/Administration/UI/Tabs/PlayerTab.xaml.cs
+++ b/Content.Client/Administration/UI/Tabs/PlayerTab.xaml.cs
@@ -1,6 +1,6 @@
using System.Collections.Generic;
+using Content.Client.Administration.UI.CustomControls;
using Content.Shared.Administration;
-using Content.Shared.Administration.Events;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Player;
@@ -138,37 +138,5 @@ private void RefreshPlayerList(IReadOnlyList players)
useAltColor ^= true;
}
}
-
- private static readonly Color SeparatorColor = Color.FromHex("#3D4059");
-
- private class VSeparator : PanelContainer
- {
- public VSeparator()
- {
- MinSize = (2, 5);
- AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat
- {
- BackgroundColor = SeparatorColor
- }
- });
- }
- }
-
- private class HSeparator : Control
- {
- public HSeparator()
- {
- AddChild(new PanelContainer
- {
- PanelOverride = new StyleBoxFlat
- {
- BackgroundColor = SeparatorColor,
- ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2
- }
- });
- }
- }
}
}
diff --git a/Content.IntegrationTests/ContentIntegrationTest.cs b/Content.IntegrationTests/ContentIntegrationTest.cs
index 8bcaeb3bcfb..d6cfec16798 100644
--- a/Content.IntegrationTests/ContentIntegrationTest.cs
+++ b/Content.IntegrationTests/ContentIntegrationTest.cs
@@ -318,13 +318,19 @@ await server.WaitPost(() =>
protected async Task WaitUntil(IntegrationInstance instance, Func func, int maxTicks = 600,
int tickStep = 1)
+ {
+ await WaitUntil(instance, async () => await Task.FromResult(func()), maxTicks);
+ }
+
+ protected async Task WaitUntil(IntegrationInstance instance, Func> func, int maxTicks = 600,
+ int tickStep = 1)
{
var ticksAwaited = 0;
bool passed;
await instance.WaitIdleAsync();
- while (!(passed = func()) && ticksAwaited < maxTicks)
+ while (!(passed = await func()) && ticksAwaited < maxTicks)
{
var ticksToRun = tickStep;
diff --git a/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs
new file mode 100644
index 00000000000..7fce4f1f09c
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Administration/Logs/AddTests.cs
@@ -0,0 +1,197 @@
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using Content.Server.Administration.Logs;
+using Content.Server.Database;
+using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Administration.Logs;
+
+[TestFixture]
+[TestOf(typeof(AdminLogSystem))]
+public class AddTests : ContentIntegrationTest
+{
+ [Test]
+ public async Task AddAndGetSingleLog()
+ {
+ var server = StartServer(new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.AdminLogsQueueSendDelay.Name] = "0"
+ },
+ Pool = true
+ });
+ await server.WaitIdleAsync();
+
+ var sEntities = server.ResolveDependency();
+ var sMaps = server.ResolveDependency();
+ var sSystems = server.ResolveDependency();
+
+ var sAdminLogSystem = sSystems.GetEntitySystem();
+
+ var guid = Guid.NewGuid();
+
+ await server.WaitPost(() =>
+ {
+ var coordinates = GetMainEntityCoordinates(sMaps);
+ var entity = sEntities.SpawnEntity(null, coordinates);
+
+ sAdminLogSystem.Add(LogType.Unknown, $"{entity:Entity} test log: {guid}");
+ });
+
+ await WaitUntil(server, async () =>
+ {
+ var logs = sAdminLogSystem.CurrentRoundJson(new LogFilter
+ {
+ Search = guid.ToString()
+ });
+
+ await foreach (var json in logs)
+ {
+ var root = json.RootElement;
+
+ // camelCased automatically
+ Assert.That(root.TryGetProperty("entity", out _), Is.True);
+
+ json.Dispose();
+
+ return true;
+ }
+
+ return false;
+ });
+ }
+
+ [Test]
+ public async Task AddAndGetUnformattedLog()
+ {
+ var server = StartServer(new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.AdminLogsQueueSendDelay.Name] = "0"
+ },
+ Pool = true
+ });
+ await server.WaitIdleAsync();
+
+ var sDatabase = server.ResolveDependency();
+ var sEntities = server.ResolveDependency();
+ var sMaps = server.ResolveDependency();
+ var sSystems = server.ResolveDependency();
+
+ var sAdminLogSystem = sSystems.GetEntitySystem();
+
+ var guid = Guid.NewGuid();
+
+ await server.WaitPost(() =>
+ {
+ var coordinates = GetMainEntityCoordinates(sMaps);
+ var entity = sEntities.SpawnEntity(null, coordinates);
+
+ sAdminLogSystem.Add(LogType.Unknown, $"{entity} test log: {guid}");
+ });
+
+ LogRecord log = null;
+
+ await WaitUntil(server, async () =>
+ {
+ var logs = sAdminLogSystem.CurrentRoundLogs(new LogFilter
+ {
+ Search = guid.ToString()
+ });
+
+ await foreach (var found in logs)
+ {
+ log = found;
+ return true;
+ }
+
+ return false;
+ });
+
+ await server.WaitPost(() =>
+ {
+ Task.Run(async () =>
+ {
+ var filter = new LogFilter
+ {
+ Round = log.RoundId,
+ Search = log.Message,
+ Types = new List {log.Type},
+ };
+
+ await foreach (var json in sDatabase.GetAdminLogsJson(filter))
+ {
+ var root = json.RootElement;
+
+ Assert.That(root.TryGetProperty("entity", out _), Is.True);
+ Assert.That(root.TryGetProperty("guid", out _), Is.True);
+
+ json.Dispose();
+ }
+ }).Wait();
+ });
+ }
+
+ [Test]
+ [TestCase(500, false)]
+ [TestCase(500, true)]
+ public async Task BulkAddLogs(int amount, bool parallel)
+ {
+ var server = StartServer(new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.AdminLogsQueueSendDelay.Name] = "0"
+ },
+ Pool = true
+ });
+ await server.WaitIdleAsync();
+
+ var sEntities = server.ResolveDependency();
+ var sMaps = server.ResolveDependency();
+ var sSystems = server.ResolveDependency();
+
+ var sAdminLogSystem = sSystems.GetEntitySystem();
+
+ await server.WaitPost(() =>
+ {
+ var coordinates = GetMainEntityCoordinates(sMaps);
+ var entity = sEntities.SpawnEntity(null, coordinates);
+
+ if (parallel)
+ {
+ Parallel.For(0, amount, _ =>
+ {
+ sAdminLogSystem.Add(LogType.Unknown, $"{entity:Entity} test log.");
+ });
+ }
+ else
+ {
+ for (var i = 0; i < amount; i++)
+ {
+ sAdminLogSystem.Add(LogType.Unknown, $"{entity:Entity} test log.");
+ }
+ }
+ });
+
+ await WaitUntil(server, async () =>
+ {
+ var messages = sAdminLogSystem.CurrentRoundLogs();
+ var count = 0;
+
+ await foreach (var _ in messages)
+ {
+ count++;
+ }
+
+ return count >= amount;
+ });
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs
new file mode 100644
index 00000000000..d0e09e88195
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Administration/Logs/FilterTests.cs
@@ -0,0 +1,108 @@
+using System;
+using System.Threading.Tasks;
+using Content.Server.Administration.Logs;
+using Content.Server.GameTicking;
+using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using NUnit.Framework;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Map;
+
+namespace Content.IntegrationTests.Tests.Administration.Logs;
+
+[TestFixture]
+[TestOf(typeof(AdminLogSystem))]
+public class FilterTests : ContentIntegrationTest
+{
+ [Test]
+ [TestCase(DateOrder.Ascending)]
+ [TestCase(DateOrder.Descending)]
+ public async Task Date(DateOrder order)
+ {
+ var server = StartServer(new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.AdminLogsQueueSendDelay.Name] = "0"
+ },
+ Pool = true
+ });
+ await server.WaitIdleAsync();
+
+ var sEntities = server.ResolveDependency();
+ var sMaps = server.ResolveDependency();
+ var sSystems = server.ResolveDependency();
+
+ var sAdminLogSystem = sSystems.GetEntitySystem();
+
+ var commonGuid = Guid.NewGuid();
+ var guids = new[] {Guid.NewGuid(), Guid.NewGuid()};
+
+ for (var i = 0; i < 2; i++)
+ {
+ await server.WaitPost(() =>
+ {
+ var coordinates = GetMainEntityCoordinates(sMaps);
+ var entity = sEntities.SpawnEntity(null, coordinates);
+
+ sAdminLogSystem.Add(LogType.Unknown, $"{entity:Entity} test log: {commonGuid} {guids[i]}");
+ });
+
+ await server.WaitRunTicks(60);
+ }
+
+ await WaitUntil(server, async () =>
+ {
+ var commonGuidStr = commonGuid.ToString();
+
+ string firstGuidStr;
+ string secondGuidStr;
+
+ switch (order)
+ {
+ case DateOrder.Ascending:
+ // Oldest first
+ firstGuidStr = guids[0].ToString();
+ secondGuidStr = guids[1].ToString();
+ break;
+ case DateOrder.Descending:
+ // Newest first
+ firstGuidStr = guids[1].ToString();
+ secondGuidStr = guids[0].ToString();
+ break;
+ default:
+ throw new ArgumentOutOfRangeException(nameof(order), order, null);
+ }
+
+ var firstFound = false;
+ var secondFound = false;
+
+ var both = sAdminLogSystem.CurrentRoundLogs(new LogFilter
+ {
+ Search = commonGuidStr,
+ DateOrder = order
+ });
+
+ await foreach (var log in both)
+ {
+ if (!log.Message.Contains(commonGuidStr))
+ {
+ continue;
+ }
+
+ if (!firstFound)
+ {
+ Assert.That(log.Message, Does.Contain(firstGuidStr));
+ firstFound = true;
+ continue;
+ }
+
+ Assert.That(log.Message, Does.Contain(secondGuidStr));
+ secondFound = true;
+ break;
+ }
+
+ return firstFound && secondFound;
+ });
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs
new file mode 100644
index 00000000000..46b6d17a30e
--- /dev/null
+++ b/Content.IntegrationTests/Tests/Administration/Logs/QueryTests.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using Content.Server.Administration.Logs;
+using Content.Server.GameTicking;
+using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using NUnit.Framework;
+using Robust.Server.Player;
+using Robust.Shared.GameObjects;
+
+namespace Content.IntegrationTests.Tests.Administration.Logs;
+
+[TestFixture]
+[TestOf(typeof(AdminLogSystem))]
+public class QueryTests : ContentIntegrationTest
+{
+ [Test]
+ public async Task QuerySingleLog()
+ {
+ var serverOptions = new ServerContentIntegrationOption
+ {
+ CVarOverrides =
+ {
+ [CCVars.AdminLogsQueueSendDelay.Name] = "0"
+ }
+ };
+ var (client, server) = await StartConnectedServerClientPair(serverOptions: serverOptions);
+
+ await Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync());
+
+ var sSystems = server.ResolveDependency();
+ var sPlayers = server.ResolveDependency();
+
+ var sAdminLogSystem = sSystems.GetEntitySystem();
+ var sGameTicker = sSystems.GetEntitySystem();
+
+ var date = DateTime.UtcNow;
+ var guid = Guid.NewGuid();
+
+ IPlayerSession player = default;
+
+ await server.WaitPost(() =>
+ {
+ player = sPlayers.GetAllPlayers().First();
+
+ sAdminLogSystem.Add(LogType.Unknown, $"{player.AttachedEntity:Entity} test log: {guid}");
+ });
+
+ var filter = new LogFilter
+ {
+ Round = sGameTicker.RoundId,
+ Search = guid.ToString(),
+ Types = new List {LogType.Unknown},
+ After = date,
+ AnyPlayers = new[] {player.UserId.UserId}
+ };
+
+ await WaitUntil(server, async () =>
+ {
+ await foreach (var _ in sAdminLogSystem.All(filter))
+ {
+ return true;
+ }
+
+ return false;
+ });
+ }
+}
diff --git a/Content.Server.Database/Content.Server.Database.csproj b/Content.Server.Database/Content.Server.Database.csproj
index b68dee84828..e0a676a3e41 100644
--- a/Content.Server.Database/Content.Server.Database.csproj
+++ b/Content.Server.Database/Content.Server.Database.csproj
@@ -20,5 +20,10 @@
+
+
+
+
+
diff --git a/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.Designer.cs b/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.Designer.cs
new file mode 100644
index 00000000000..9a55ce493c6
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.Designer.cs
@@ -0,0 +1,848 @@
+//
+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("20211120202701_AdminLogs")]
+ partial class AdminLogs
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("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("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("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.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")
+ .HasDatabaseName("IX_admin_rank_flag_admin_rank_id");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (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.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")
+ .HasDatabaseName("IX_job_profile_id");
+
+ 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.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("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", (string)null);
+
+ b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", 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("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", (string)null);
+
+ b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerBan", 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("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("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("server_ban", (string)null);
+
+ b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.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("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("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.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.HasKey("Id")
+ .HasName("PK_round");
+
+ b.ToTable("round", (string)null);
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.Property("PlayersId")
+ .HasColumnType("integer")
+ .HasColumnName("players_id");
+
+ b.Property("RoundsId")
+ .HasColumnType("integer")
+ .HasColumnName("rounds_id");
+
+ b.HasKey("PlayersId", "RoundsId")
+ .HasName("PK_player_round");
+
+ b.HasIndex("RoundsId")
+ .HasDatabaseName("IX_player_round_rounds_id");
+
+ b.ToTable("player_round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+ .WithMany("Admins")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+ b.Navigation("AdminRank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.Admin", "Admin")
+ .WithMany("Flags")
+ .HasForeignKey("AdminId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+ b.Navigation("Admin");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany("AdminLogs")
+ .HasForeignKey("RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_round_round_id");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogEntity", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminLog", null)
+ .WithMany("Entities")
+ .HasForeignKey("AdminLogId", "AdminLogRoundId")
+ .HasConstraintName("FK_admin_log_entity_admin_log_admin_log_id_admin_log_round_id");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminLogs")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_player_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.AdminLog", "Log")
+ .WithMany("Players")
+ .HasForeignKey("LogId", "RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_player_admin_log_log_id_round_id");
+
+ b.Navigation("Log");
+
+ b.Navigation("Player");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "Rank")
+ .WithMany("Flags")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+ b.Navigation("Rank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Antags")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_antag_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Jobs")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_job_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
+ {
+ b.HasOne("Content.Server.Database.PostgresServerBan", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.PostgresServerUnban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.HasOne("Content.Server.Database.Preference", "Preference")
+ .WithMany("Profiles")
+ .HasForeignKey("PreferenceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_preference_preference_id");
+
+ b.Navigation("Preference");
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", null)
+ .WithMany()
+ .HasForeignKey("PlayersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_player_players_id");
+
+ b.HasOne("Content.Server.Database.Round", null)
+ .WithMany()
+ .HasForeignKey("RoundsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_round_rounds_id");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Navigation("Entities");
+
+ b.Navigation("Players");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Navigation("Admins");
+
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Navigation("AdminLogs");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerBan", b =>
+ {
+ b.Navigation("Unban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Preference", b =>
+ {
+ b.Navigation("Profiles");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.Navigation("Antags");
+
+ b.Navigation("Jobs");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Navigation("AdminLogs");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.cs b/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.cs
new file mode 100644
index 00000000000..f024165d0ab
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20211120202701_AdminLogs.cs
@@ -0,0 +1,171 @@
+using System;
+using System.Text.Json;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ public partial class AdminLogs : Migration
+ {
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddUniqueConstraint(
+ name: "ak_player_user_id",
+ table: "player",
+ column: "user_id");
+
+ migrationBuilder.CreateTable(
+ name: "round",
+ columns: table => new
+ {
+ round_id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_round", x => x.round_id);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "admin_log",
+ columns: table => new
+ {
+ admin_log_id = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ round_id = table.Column(type: "integer", nullable: false),
+ type = table.Column(type: "integer", nullable: false),
+ date = table.Column(type: "timestamp with time zone", nullable: false),
+ message = table.Column(type: "text", nullable: false),
+ json = table.Column(type: "jsonb", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_admin_log", x => new { x.admin_log_id, x.round_id });
+ table.ForeignKey(
+ name: "FK_admin_log_round_round_id",
+ column: x => x.round_id,
+ principalTable: "round",
+ principalColumn: "round_id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "player_round",
+ columns: table => new
+ {
+ players_id = table.Column(type: "integer", nullable: false),
+ rounds_id = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_player_round", x => new { x.players_id, x.rounds_id });
+ table.ForeignKey(
+ name: "FK_player_round_player_players_id",
+ column: x => x.players_id,
+ principalTable: "player",
+ principalColumn: "player_id",
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_player_round_round_rounds_id",
+ column: x => x.rounds_id,
+ principalTable: "round",
+ principalColumn: "round_id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateTable(
+ name: "admin_log_entity",
+ columns: table => new
+ {
+ uid = table.Column(type: "integer", nullable: false)
+ .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
+ name = table.Column(type: "text", nullable: true),
+ admin_log_id = table.Column(type: "integer", nullable: true),
+ admin_log_round_id = table.Column(type: "integer", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_admin_log_entity", x => x.uid);
+ table.ForeignKey(
+ name: "FK_admin_log_entity_admin_log_admin_log_id_admin_log_round_id",
+ columns: x => new { x.admin_log_id, x.admin_log_round_id },
+ principalTable: "admin_log",
+ principalColumns: new[] { "admin_log_id", "round_id" });
+ });
+
+ migrationBuilder.CreateTable(
+ name: "admin_log_player",
+ columns: table => new
+ {
+ player_user_id = table.Column(type: "uuid", nullable: false),
+ log_id = table.Column(type: "integer", nullable: false),
+ round_id = table.Column(type: "integer", nullable: false)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_admin_log_player", x => new { x.player_user_id, x.log_id, x.round_id });
+ table.ForeignKey(
+ name: "FK_admin_log_player_admin_log_log_id_round_id",
+ columns: x => new { x.log_id, x.round_id },
+ principalTable: "admin_log",
+ principalColumns: new[] { "admin_log_id", "round_id" },
+ onDelete: ReferentialAction.Cascade);
+ table.ForeignKey(
+ name: "FK_admin_log_player_player_player_user_id",
+ column: x => x.player_user_id,
+ principalTable: "player",
+ principalColumn: "user_id",
+ onDelete: ReferentialAction.Cascade);
+ });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_admin_log_round_id",
+ table: "admin_log",
+ column: "round_id");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_admin_log_type",
+ table: "admin_log",
+ column: "type");
+
+ migrationBuilder.CreateIndex(
+ name: "IX_admin_log_entity_admin_log_id_admin_log_round_id",
+ table: "admin_log_entity",
+ columns: new[] { "admin_log_id", "admin_log_round_id" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_admin_log_player_log_id_round_id",
+ table: "admin_log_player",
+ columns: new[] { "log_id", "round_id" });
+
+ migrationBuilder.CreateIndex(
+ name: "IX_player_round_rounds_id",
+ table: "player_round",
+ column: "rounds_id");
+ }
+
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "admin_log_entity");
+
+ migrationBuilder.DropTable(
+ name: "admin_log_player");
+
+ migrationBuilder.DropTable(
+ name: "player_round");
+
+ migrationBuilder.DropTable(
+ name: "admin_log");
+
+ migrationBuilder.DropTable(
+ name: "round");
+
+ migrationBuilder.DropUniqueConstraint(
+ name: "ak_player_user_id",
+ table: "player");
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20211121123543_AdminLogsImpact.Designer.cs b/Content.Server.Database/Migrations/Postgres/20211121123543_AdminLogsImpact.Designer.cs
new file mode 100644
index 00000000000..fc54389ec2d
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20211121123543_AdminLogsImpact.Designer.cs
@@ -0,0 +1,852 @@
+//
+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("20211121123543_AdminLogsImpact")]
+ partial class AdminLogsImpact
+ {
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "6.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("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("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.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")
+ .HasDatabaseName("IX_admin_rank_flag_admin_rank_id");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (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.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")
+ .HasDatabaseName("IX_job_profile_id");
+
+ 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.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("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", (string)null);
+
+ b.HasCheckConstraint("LastSeenAddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= last_seen_address");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresConnectionLog", 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("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", (string)null);
+
+ b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerBan", 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("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("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("server_ban", (string)null);
+
+ b.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ b.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.PostgresServerUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.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("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("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.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.HasKey("Id")
+ .HasName("PK_round");
+
+ b.ToTable("round", (string)null);
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.Property("PlayersId")
+ .HasColumnType("integer")
+ .HasColumnName("players_id");
+
+ b.Property