diff --git a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
index ea2f77d457d..83dc42c4a44 100644
--- a/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
+++ b/Content.Client/Communications/UI/CommunicationsConsoleMenu.xaml
@@ -4,14 +4,14 @@
MinSize="400 225">
-
-
+
+
-
+
-
+
diff --git a/Content.Client/Ghost/GhostSystem.cs b/Content.Client/Ghost/GhostSystem.cs
index 94872a58ef9..f4f7e3459fb 100644
--- a/Content.Client/Ghost/GhostSystem.cs
+++ b/Content.Client/Ghost/GhostSystem.cs
@@ -99,8 +99,8 @@ private void OnToggleGhosts(EntityUid uid, GhostComponent component, ToggleGhost
if (args.Handled)
return;
- Popup.PopupEntity(Loc.GetString("ghost-gui-toggle-ghost-visibility-popup"), args.Performer);
-
+ var locId = GhostVisibility ? "ghost-gui-toggle-ghost-visibility-popup-off" : "ghost-gui-toggle-ghost-visibility-popup-on";
+ Popup.PopupEntity(Loc.GetString(locId), args.Performer);
if (uid == _playerManager.LocalEntity)
ToggleGhostVisibility();
diff --git a/Content.Client/Launcher/LauncherConnecting.cs b/Content.Client/Launcher/LauncherConnecting.cs
index 3b45dc2d135..2c1e25a6da1 100644
--- a/Content.Client/Launcher/LauncherConnecting.cs
+++ b/Content.Client/Launcher/LauncherConnecting.cs
@@ -20,6 +20,7 @@ public sealed class LauncherConnecting : Robust.Client.State.State
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly IClipboardManager _clipboard = default!;
[Dependency] private readonly ServersHubManager _serversHubManager = default!;
private LauncherConnectingGui? _control;
@@ -60,7 +61,7 @@ private set
protected override void Startup()
{
- _control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _serversHubManager);
+ _control = new LauncherConnectingGui(this, _random, _prototypeManager, _cfg, _clipboard, _serversHubManager);
_userInterfaceManager.StateRoot.AddChild(_control);
diff --git a/Content.Client/Launcher/LauncherConnectingGui.xaml b/Content.Client/Launcher/LauncherConnectingGui.xaml
index 671b606f5f2..fafbb649b7c 100644
--- a/Content.Client/Launcher/LauncherConnectingGui.xaml
+++ b/Content.Client/Launcher/LauncherConnectingGui.xaml
@@ -7,51 +7,67 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ VerticalAlignment="Bottom"
+ StyleClasses="OpenRight"/>
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
-
+
@@ -67,7 +83,6 @@
-
diff --git a/Content.Client/Launcher/LauncherConnectingGui.xaml.cs b/Content.Client/Launcher/LauncherConnectingGui.xaml.cs
index ed2c47293b7..7b294487d52 100644
--- a/Content.Client/Launcher/LauncherConnectingGui.xaml.cs
+++ b/Content.Client/Launcher/LauncherConnectingGui.xaml.cs
@@ -30,16 +30,19 @@ public sealed partial class LauncherConnectingGui : Control
private readonly IRobustRandom _random;
private readonly IPrototypeManager _prototype;
private readonly IConfigurationManager _cfg;
- private readonly ServersHubManager _serversHubManager;
+ private readonly IClipboardManager _clipboard;
+ private readonly ServersHubManager _serversHubManager; // Sunrise-Edit
public LauncherConnectingGui(LauncherConnecting state, IRobustRandom random,
- IPrototypeManager prototype, IConfigurationManager config, ServersHubManager serversHubManager)
+ IPrototypeManager prototype, IConfigurationManager config, IClipboardManager clipboard,
+ ServersHubManager serversHubManager) // Sunrise-Edit
{
_state = state;
_random = random;
_prototype = prototype;
_cfg = config;
- _serversHubManager = serversHubManager;
+ _clipboard = clipboard;
+ _serversHubManager = serversHubManager; // Sunrise-Edit
RobustXamlLoader.Load(this);
@@ -48,8 +51,11 @@ public LauncherConnectingGui(LauncherConnecting state, IRobustRandom random,
Stylesheet = IoCManager.Resolve().SheetSpace;
ChangeLoginTip();
- ReconnectButton.OnPressed += ReconnectButtonPressed;
RetryButton.OnPressed += ReconnectButtonPressed;
+ ReconnectButton.OnPressed += ReconnectButtonPressed;
+
+ CopyButton.OnPressed += CopyButtonPressed;
+ CopyButtonDisconnected.OnPressed += CopyButtonDisconnectedPressed;
ExitButton.OnPressed += _ => _state.Exit();
var addr = state.Address;
@@ -68,15 +74,17 @@ public LauncherConnectingGui(LauncherConnecting state, IRobustRandom random,
edim.LastNetDisconnectedArgsChanged += LastNetDisconnectedArgsChanged;
LastNetDisconnectedArgsChanged(edim.LastNetDisconnectedArgs);
- _serversHubManager.ServersDataListChanged += RefreshServersHubHeader;
+ _serversHubManager.ServersDataListChanged += RefreshServersHubHeader; // Sunrise-Edit
}
+ // Sunrise-Start
private void RefreshServersHubHeader(List servers)
{
var totalPlayers = servers.Sum(server => server.CurrentPlayers);
var maxPlayers = servers.Sum(server => server.MaxPlayers);
ServersHubHeaderLabel.Text = Loc.GetString("serverhub-playingnow", ("total", totalPlayers), ("max", maxPlayers)); // Sunrise-Edit
}
+ // Sunrise-End
// Just button, there's only one at once anyways :)
private void ReconnectButtonPressed(BaseButton.ButtonEventArgs args)
@@ -91,6 +99,24 @@ private void ReconnectButtonPressed(BaseButton.ButtonEventArgs args)
_state.RetryConnect();
}
+ private void CopyButtonPressed(BaseButton.ButtonEventArgs args)
+ {
+ CopyText(ConnectFailReason.Text);
+ }
+
+ private void CopyButtonDisconnectedPressed(BaseButton.ButtonEventArgs args)
+ {
+ CopyText(DisconnectReason.Text);
+ }
+
+ private void CopyText(string? text)
+ {
+ if (!string.IsNullOrEmpty(text))
+ {
+ _clipboard.SetText(text);
+ }
+ }
+
private void ConnectFailReasonChanged(string? reason)
{
ConnectFailReason.SetMessage(reason == null
diff --git a/Content.Client/Materials/RecyclerVisualizerSystem.cs b/Content.Client/Materials/RecyclerVisualizerSystem.cs
new file mode 100644
index 00000000000..646ae406aa0
--- /dev/null
+++ b/Content.Client/Materials/RecyclerVisualizerSystem.cs
@@ -0,0 +1,27 @@
+using Content.Shared.Conveyor;
+using Content.Shared.Materials;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Materials;
+
+public sealed class RecyclerVisualizerSystem : VisualizerSystem
+{
+ protected override void OnAppearanceChange(EntityUid uid, RecyclerVisualsComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null || !args.Sprite.LayerMapTryGet(RecyclerVisualLayers.Main, out var layer))
+ return;
+
+ AppearanceSystem.TryGetData(uid, ConveyorVisuals.State, out var running);
+ AppearanceSystem.TryGetData(uid, RecyclerVisuals.Bloody, out var bloody);
+ AppearanceSystem.TryGetData(uid, RecyclerVisuals.Broken, out var broken);
+
+ var activityState = running == ConveyorState.Off ? 0 : 1;
+ if (broken) //breakage overrides activity
+ activityState = 2;
+
+ var bloodyKey = bloody ? component.BloodyKey : string.Empty;
+
+ var state = $"{component.BaseKey}{activityState}{bloodyKey}";
+ args.Sprite.LayerSetState(layer, state);
+ }
+}
diff --git a/Content.Client/Materials/RecyclerVisualsComponent.cs b/Content.Client/Materials/RecyclerVisualsComponent.cs
new file mode 100644
index 00000000000..4db26326d6a
--- /dev/null
+++ b/Content.Client/Materials/RecyclerVisualsComponent.cs
@@ -0,0 +1,17 @@
+namespace Content.Client.Materials;
+
+[RegisterComponent]
+public sealed partial class RecyclerVisualsComponent : Component
+{
+ ///
+ /// Key appended to state string if bloody.
+ ///
+ [DataField]
+ public string BloodyKey = "bld";
+
+ ///
+ /// Base key for the visual state.
+ ///
+ [DataField]
+ public string BaseKey = "grinder-o";
+}
diff --git a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
index 0b8720add21..64ead32586d 100644
--- a/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
+++ b/Content.Client/Shuttles/UI/ShuttleNavControl.xaml.cs
@@ -35,6 +35,7 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
public bool ShowIFF { get; set; } = true;
public bool ShowDocks { get; set; } = true;
+ public bool RotateWithEntity { get; set; } = true;
///
/// Raised if the user left-clicks on the radar control with the relevant entitycoordinates.
@@ -109,6 +110,8 @@ public void UpdateState(NavInterfaceState state)
ActualRadarRange = Math.Clamp(ActualRadarRange, WorldMinRange, WorldMaxRange);
+ RotateWithEntity = state.RotateWithEntity;
+
_docks = state.Docks;
}
@@ -138,7 +141,8 @@ protected override void Draw(DrawingHandleScreen handle)
var mapPos = _transform.ToMapCoordinates(_coordinates.Value);
var offset = _coordinates.Value.Position;
var posMatrix = Matrix3Helpers.CreateTransform(offset, _rotation.Value);
- var (_, ourEntRot, ourEntMatrix) = _transform.GetWorldPositionRotationMatrix(_coordinates.Value.EntityId);
+ var ourEntRot = RotateWithEntity ? _transform.GetWorldRotation(xform) : _rotation.Value;
+ var ourEntMatrix = Matrix3Helpers.CreateTransform(_transform.GetWorldPosition(xform), ourEntRot);
var ourWorldMatrix = Matrix3x2.Multiply(posMatrix, ourEntMatrix);
Matrix3x2.Invert(ourWorldMatrix, out var ourWorldMatrixInvert);
diff --git a/Content.Client/Voting/UI/VotePopup.xaml b/Content.Client/Voting/UI/VotePopup.xaml
index 39817bd553f..fd40d7b790c 100644
--- a/Content.Client/Voting/UI/VotePopup.xaml
+++ b/Content.Client/Voting/UI/VotePopup.xaml
@@ -1,8 +1,8 @@
-
+
-
+
diff --git a/Content.Client/Voting/UI/VotePopup.xaml.cs b/Content.Client/Voting/UI/VotePopup.xaml.cs
index 276da08bdf1..90102e9cdf7 100644
--- a/Content.Client/Voting/UI/VotePopup.xaml.cs
+++ b/Content.Client/Voting/UI/VotePopup.xaml.cs
@@ -8,6 +8,7 @@
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
+using Robust.Shared.Utility;
namespace Content.Client.Voting.UI
{
@@ -48,7 +49,7 @@ public VotePopup(VoteManager.ActiveVote vote)
public void UpdateData()
{
- VoteTitle.Text = _vote.Title;
+ VoteTitle.SetMessage(FormattedMessage.FromUnformatted(_vote.Title));
VoteCaller.Text = Loc.GetString("ui-vote-created", ("initiator", _vote.Initiator));
for (var i = 0; i < _voteButtons.Length; i++)
diff --git a/Content.Packaging/ServerPackaging.cs b/Content.Packaging/ServerPackaging.cs
index c455a2d36e1..a430e15a01d 100644
--- a/Content.Packaging/ServerPackaging.cs
+++ b/Content.Packaging/ServerPackaging.cs
@@ -22,6 +22,7 @@ public static class ServerPackaging
new PlatformReg("win-x86", "Windows", false),
new PlatformReg("linux-x86", "Linux", false),
new PlatformReg("linux-arm", "Linux", false),
+ new PlatformReg("freebsd-x64", "FreeBSD", false),
};
private static List PlatformRids => Platforms
diff --git a/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs
new file mode 100644
index 00000000000..2f06e5ff984
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.Designer.cs
@@ -0,0 +1,1913 @@
+//
+using System;
+using System.Net;
+using System.Text.Json;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
+using NpgsqlTypes;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ [DbContext(typeof(PostgresServerDbContext))]
+ [Migration("20240606121555_ban_notify_trigger")]
+ partial class ban_notify_trigger
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "8.0.0")
+ .HasAnnotation("Relational:MaxIdentifierLength", 63);
+
+ NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Title")
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("UserId")
+ .HasName("PK_admin");
+
+ b.HasIndex("AdminRankId")
+ .HasDatabaseName("IX_admin_admin_rank_id");
+
+ b.ToTable("admin", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminId")
+ .HasColumnType("uuid")
+ .HasColumnName("admin_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.Property("Negative")
+ .HasColumnType("boolean")
+ .HasColumnName("negative");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_flag");
+
+ b.HasIndex("AdminId")
+ .HasDatabaseName("IX_admin_flag_admin_id");
+
+ b.HasIndex("Flag", "AdminId")
+ .IsUnique();
+
+ b.ToTable("admin_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Id")
+ .HasColumnType("integer")
+ .HasColumnName("admin_log_id");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Impact")
+ .HasColumnType("smallint")
+ .HasColumnName("impact");
+
+ b.Property("Json")
+ .IsRequired()
+ .HasColumnType("jsonb")
+ .HasColumnName("json");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("message");
+
+ b.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("type");
+
+ b.HasKey("RoundId", "Id")
+ .HasName("PK_admin_log");
+
+ b.HasIndex("Date");
+
+ b.HasIndex("Message")
+ .HasAnnotation("Npgsql:TsVectorConfig", "english");
+
+ NpgsqlIndexBuilderExtensions.HasMethod(b.HasIndex("Message"), "GIN");
+
+ b.HasIndex("Type")
+ .HasDatabaseName("IX_admin_log_type");
+
+ b.ToTable("admin_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLogPlayer", b =>
+ {
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("LogId")
+ .HasColumnType("integer")
+ .HasColumnName("log_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.HasKey("RoundId", "LogId", "PlayerUserId")
+ .HasName("PK_admin_log_player");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_log_player_player_user_id");
+
+ b.ToTable("admin_log_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_messages_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("Dismissed")
+ .HasColumnType("boolean")
+ .HasColumnName("dismissed");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Seen")
+ .HasColumnType("boolean")
+ .HasColumnName("seen");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_messages");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_messages_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_messages_round_id");
+
+ b.ToTable("admin_messages", null, t =>
+ {
+ t.HasCheckConstraint("NotDismissedAndSeen", "NOT dismissed OR seen");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_notes_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Secret")
+ .HasColumnType("boolean")
+ .HasColumnName("secret");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_notes");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_notes_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_notes_round_id");
+
+ b.ToTable("admin_notes", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank");
+
+ b.ToTable("admin_rank", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_flag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AdminRankId")
+ .HasColumnType("integer")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Flag")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flag");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_rank_flag");
+
+ b.HasIndex("AdminRankId");
+
+ b.HasIndex("Flag", "AdminRankId")
+ .IsUnique();
+
+ b.ToTable("admin_rank_flag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("admin_watchlists_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("CreatedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("created_at");
+
+ b.Property("CreatedById")
+ .HasColumnType("uuid")
+ .HasColumnName("created_by_id");
+
+ b.Property("Deleted")
+ .HasColumnType("boolean")
+ .HasColumnName("deleted");
+
+ b.Property("DeletedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("deleted_at");
+
+ b.Property("DeletedById")
+ .HasColumnType("uuid")
+ .HasColumnName("deleted_by_id");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("LastEditedAt")
+ .IsRequired()
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("Message")
+ .IsRequired()
+ .HasMaxLength(4096)
+ .HasColumnType("character varying(4096)")
+ .HasColumnName("message");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_admin_watchlists");
+
+ b.HasIndex("CreatedById");
+
+ b.HasIndex("DeletedById");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_admin_watchlists_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_admin_watchlists_round_id");
+
+ b.ToTable("admin_watchlists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("antag_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AntagName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("antag_name");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_antag");
+
+ b.HasIndex("ProfileId", "AntagName")
+ .IsUnique();
+
+ b.ToTable("antag", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AssignedUserId", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("assigned_user_id_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_assigned_user_id");
+
+ b.HasIndex("UserId")
+ .IsUnique();
+
+ b.HasIndex("UserName")
+ .IsUnique();
+
+ b.ToTable("assigned_user_id", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Denied")
+ .HasColumnType("smallint")
+ .HasColumnName("denied");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("UserName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("user_name");
+
+ b.HasKey("Id")
+ .HasName("PK_connection_log");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_connection_log_server_id");
+
+ b.HasIndex("UserId");
+
+ b.ToTable("connection_log", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("job_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("JobName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("job_name");
+
+ b.Property("Priority")
+ .HasColumnType("integer")
+ .HasColumnName("priority");
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.HasKey("Id")
+ .HasName("PK_job");
+
+ b.HasIndex("ProfileId");
+
+ b.HasIndex("ProfileId", "JobName")
+ .IsUnique();
+
+ b.HasIndex(new[] { "ProfileId" }, "IX_job_one_high_priority")
+ .IsUnique()
+ .HasFilter("priority = 3");
+
+ b.ToTable("job", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.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("CharacterName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("char_name");
+
+ b.Property("EyeColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("eye_color");
+
+ b.Property("FacialHairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_color");
+
+ b.Property("FacialHairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("facial_hair_name");
+
+ b.Property("FlavorText")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("flavor_text");
+
+ b.Property("Gender")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("gender");
+
+ b.Property("HairColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_color");
+
+ b.Property("HairName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("hair_name");
+
+ b.Property("Markings")
+ .HasColumnType("jsonb")
+ .HasColumnName("markings");
+
+ b.Property("PreferenceId")
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ b.Property("PreferenceUnavailable")
+ .HasColumnType("integer")
+ .HasColumnName("pref_unavailable");
+
+ b.Property("Sex")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("sex");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("skin_color");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasColumnName("slot");
+
+ b.Property("SpawnPriority")
+ .HasColumnType("integer")
+ .HasColumnName("spawn_priority");
+
+ b.Property("Species")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("species");
+
+ b.HasKey("Id")
+ .HasName("PK_profile");
+
+ b.HasIndex("PreferenceId")
+ .HasDatabaseName("IX_profile_preference_id");
+
+ b.HasIndex("Slot", "PreferenceId")
+ .IsUnique();
+
+ b.ToTable("profile", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("LoadoutName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("loadout_name");
+
+ b.Property("ProfileLoadoutGroupId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout");
+
+ b.HasIndex("ProfileLoadoutGroupId");
+
+ b.ToTable("profile_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_loadout_group_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("GroupName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("group_name");
+
+ b.Property("ProfileRoleLoadoutId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_loadout_group");
+
+ b.HasIndex("ProfileRoleLoadoutId");
+
+ b.ToTable("profile_loadout_group", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("profile_role_loadout_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.Property("RoleName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_name");
+
+ b.HasKey("Id")
+ .HasName("PK_profile_role_loadout");
+
+ b.HasIndex("ProfileId");
+
+ b.ToTable("profile_role_loadout", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+ {
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("RoleId")
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.HasKey("PlayerUserId", "RoleId")
+ .HasName("PK_role_whitelists");
+
+ b.ToTable("role_whitelists", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ServerId")
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ b.Property("StartDate")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("start_date");
+
+ b.HasKey("Id")
+ .HasName("PK_round");
+
+ b.HasIndex("ServerId")
+ .HasDatabaseName("IX_round_server_id");
+
+ b.HasIndex("StartDate");
+
+ b.ToTable("round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Server", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Name")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("name");
+
+ b.HasKey("Id")
+ .HasName("PK_server");
+
+ b.ToTable("server", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_server_ban_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_server_ban_round_id");
+
+ b.ToTable("server_ban", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.Property("Flags")
+ .HasColumnType("integer")
+ .HasColumnName("flags");
+
+ b.HasKey("UserId")
+ .HasName("PK_server_ban_exemption");
+
+ b.ToTable("server_ban_exemption", null, t =>
+ {
+ t.HasCheckConstraint("FlagsNotZero", "flags != 0");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_ban_hit_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("ConnectionId")
+ .HasColumnType("integer")
+ .HasColumnName("connection_id");
+
+ b.HasKey("Id")
+ .HasName("PK_server_ban_hit");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_server_ban_hit_ban_id");
+
+ b.HasIndex("ConnectionId")
+ .HasDatabaseName("IX_server_ban_hit_connection_id");
+
+ b.ToTable("server_ban_hit", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("server_role_ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("BanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("ban_time");
+
+ b.Property("BanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("banning_admin");
+
+ b.Property("ExpirationTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("expiration_time");
+
+ b.Property("HWId")
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("LastEditedAt")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("last_edited_at");
+
+ b.Property("LastEditedById")
+ .HasColumnType("uuid")
+ .HasColumnName("last_edited_by_id");
+
+ b.Property("PlayerUserId")
+ .HasColumnType("uuid")
+ .HasColumnName("player_user_id");
+
+ b.Property("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.HasKey("Id")
+ .HasName("PK_server_role_ban");
+
+ b.HasIndex("Address");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.HasIndex("PlayerUserId")
+ .HasDatabaseName("IX_server_role_ban_player_user_id");
+
+ b.HasIndex("RoundId")
+ .HasDatabaseName("IX_server_role_ban_round_id");
+
+ b.ToTable("server_role_ban", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+
+ t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("role_unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_role_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_role_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("unban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UnbanTime")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("unban_time");
+
+ b.Property("UnbanningAdmin")
+ .HasColumnType("uuid")
+ .HasColumnName("unbanning_admin");
+
+ b.HasKey("Id")
+ .HasName("PK_server_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("server_unban", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Trait", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("trait_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("ProfileId")
+ .HasColumnType("integer")
+ .HasColumnName("profile_id");
+
+ b.Property("TraitName")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("trait_name");
+
+ b.HasKey("Id")
+ .HasName("PK_trait");
+
+ b.HasIndex("ProfileId", "TraitName")
+ .IsUnique();
+
+ b.ToTable("trait", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.UploadedResourceLog", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("uploaded_resource_log_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Data")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("data");
+
+ b.Property("Date")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("date");
+
+ b.Property("Path")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("path");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_uploaded_resource_log");
+
+ b.ToTable("uploaded_resource_log", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Whitelist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_whitelist");
+
+ b.ToTable("whitelist", (string)null);
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.Property("PlayersId")
+ .HasColumnType("integer")
+ .HasColumnName("players_id");
+
+ b.Property("RoundsId")
+ .HasColumnType("integer")
+ .HasColumnName("rounds_id");
+
+ b.HasKey("PlayersId", "RoundsId")
+ .HasName("PK_player_round");
+
+ b.HasIndex("RoundsId")
+ .HasDatabaseName("IX_player_round_rounds_id");
+
+ b.ToTable("player_round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "AdminRank")
+ .WithMany("Admins")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_admin_rank_admin_rank_id");
+
+ b.Navigation("AdminRank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.Admin", "Admin")
+ .WithMany("Flags")
+ .HasForeignKey("AdminId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_flag_admin_admin_id");
+
+ b.Navigation("Admin");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany("AdminLogs")
+ .HasForeignKey("RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_round_round_id");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.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("RoundId", "LogId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_log_player_admin_log_round_id_log_id");
+
+ b.Navigation("Log");
+
+ b.Navigation("Player");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminMessage", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminMessagesCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminMessagesDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminMessagesLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_messages_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminMessagesReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .HasConstraintName("FK_admin_messages_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_messages_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminNote", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminNotesCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminNotesDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminNotesLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_notes_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminNotesReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .HasConstraintName("FK_admin_notes_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_notes_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRankFlag", b =>
+ {
+ b.HasOne("Content.Server.Database.AdminRank", "Rank")
+ .WithMany("Flags")
+ .HasForeignKey("AdminRankId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_admin_rank_flag_admin_rank_admin_rank_id");
+
+ b.Navigation("Rank");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminWatchlist", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminWatchlistsCreated")
+ .HasForeignKey("CreatedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_created_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "DeletedBy")
+ .WithMany("AdminWatchlistsDeleted")
+ .HasForeignKey("DeletedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_deleted_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminWatchlistsLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_admin_watchlists_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("AdminWatchlistsReceived")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .HasConstraintName("FK_admin_watchlists_player_player_user_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_admin_watchlists_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("DeletedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Player");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Antag", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Antags")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_antag_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.HasOne("Content.Server.Database.Server", "Server")
+ .WithMany("ConnectionLogs")
+ .HasForeignKey("ServerId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .IsRequired()
+ .HasConstraintName("FK_connection_log_server_server_id");
+
+ b.Navigation("Server");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Job", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Jobs")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_job_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.HasOne("Content.Server.Database.Preference", "Preference")
+ .WithMany("Profiles")
+ .HasForeignKey("PreferenceId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_preference_preference_id");
+
+ b.Navigation("Preference");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadout", b =>
+ {
+ b.HasOne("Content.Server.Database.ProfileLoadoutGroup", "ProfileLoadoutGroup")
+ .WithMany("Loadouts")
+ .HasForeignKey("ProfileLoadoutGroupId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_loadout_profile_loadout_group_profile_loadout_group~");
+
+ b.Navigation("ProfileLoadoutGroup");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+ {
+ b.HasOne("Content.Server.Database.ProfileRoleLoadout", "ProfileRoleLoadout")
+ .WithMany("Groups")
+ .HasForeignKey("ProfileRoleLoadoutId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_loadout_group_profile_role_loadout_profile_role_loa~");
+
+ b.Navigation("ProfileRoleLoadout");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Loadouts")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_profile_role_loadout_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "Player")
+ .WithMany("JobWhitelists")
+ .HasForeignKey("PlayerUserId")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_role_whitelists_player_player_user_id");
+
+ b.Navigation("Player");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.HasOne("Content.Server.Database.Server", "Server")
+ .WithMany("Rounds")
+ .HasForeignKey("ServerId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_round_server_server_id");
+
+ b.Navigation("Server");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminServerBansCreated")
+ .HasForeignKey("BanningAdmin")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_ban_player_banning_admin");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminServerBansLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_ban_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_server_ban_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerBan", "Ban")
+ .WithMany("BanHits")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
+
+ b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
+ .WithMany("BanHits")
+ .HasForeignKey("ConnectionId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_ban_hit_connection_log_connection_id");
+
+ b.Navigation("Ban");
+
+ b.Navigation("Connection");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminServerRoleBansCreated")
+ .HasForeignKey("BanningAdmin")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_role_ban_player_banning_admin");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminServerRoleBansLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .HasConstraintName("FK_server_role_ban_round_round_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("LastEditedBy");
+
+ b.Navigation("Round");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
+ {
+ b.HasOne("Content.Server.Database.ServerBan", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_unban_server_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Trait", b =>
+ {
+ b.HasOne("Content.Server.Database.Profile", "Profile")
+ .WithMany("Traits")
+ .HasForeignKey("ProfileId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_trait_profile_profile_id");
+
+ b.Navigation("Profile");
+ });
+
+ modelBuilder.Entity("PlayerRound", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", null)
+ .WithMany()
+ .HasForeignKey("PlayersId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_player_players_id");
+
+ b.HasOne("Content.Server.Database.Round", null)
+ .WithMany()
+ .HasForeignKey("RoundsId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_player_round_round_rounds_id");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminLog", b =>
+ {
+ b.Navigation("Players");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.AdminRank", b =>
+ {
+ b.Navigation("Admins");
+
+ b.Navigation("Flags");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
+ {
+ b.Navigation("BanHits");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Player", b =>
+ {
+ b.Navigation("AdminLogs");
+
+ b.Navigation("AdminMessagesCreated");
+
+ b.Navigation("AdminMessagesDeleted");
+
+ b.Navigation("AdminMessagesLastEdited");
+
+ b.Navigation("AdminMessagesReceived");
+
+ b.Navigation("AdminNotesCreated");
+
+ b.Navigation("AdminNotesDeleted");
+
+ b.Navigation("AdminNotesLastEdited");
+
+ b.Navigation("AdminNotesReceived");
+
+ b.Navigation("AdminServerBansCreated");
+
+ b.Navigation("AdminServerBansLastEdited");
+
+ b.Navigation("AdminServerRoleBansCreated");
+
+ b.Navigation("AdminServerRoleBansLastEdited");
+
+ b.Navigation("AdminWatchlistsCreated");
+
+ b.Navigation("AdminWatchlistsDeleted");
+
+ b.Navigation("AdminWatchlistsLastEdited");
+
+ b.Navigation("AdminWatchlistsReceived");
+
+ b.Navigation("JobWhitelists");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Preference", b =>
+ {
+ b.Navigation("Profiles");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Profile", b =>
+ {
+ b.Navigation("Antags");
+
+ b.Navigation("Jobs");
+
+ b.Navigation("Loadouts");
+
+ b.Navigation("Traits");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileLoadoutGroup", b =>
+ {
+ b.Navigation("Loadouts");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ProfileRoleLoadout", b =>
+ {
+ b.Navigation("Groups");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Round", b =>
+ {
+ b.Navigation("AdminLogs");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Server", b =>
+ {
+ b.Navigation("ConnectionLogs");
+
+ b.Navigation("Rounds");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
+ {
+ b.Navigation("BanHits");
+
+ b.Navigation("Unban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
+ {
+ b.Navigation("Unban");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs
new file mode 100644
index 00000000000..b84d230b455
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20240606121555_ban_notify_trigger.cs
@@ -0,0 +1,44 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ ///
+ public partial class ban_notify_trigger : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("""
+ create or replace function send_server_ban_notification()
+ returns trigger as $$
+ declare
+ x_server_id integer;
+ begin
+ select round.server_id into x_server_id from round where round.round_id = NEW.round_id;
+
+ perform pg_notify('ban_notification', json_build_object('ban_id', NEW.server_ban_id, 'server_id', x_server_id)::text);
+ return NEW;
+ end;
+ $$ LANGUAGE plpgsql;
+ """);
+
+ migrationBuilder.Sql("""
+ create or replace trigger notify_on_server_ban_insert
+ after insert on server_ban
+ for each row
+ execute function send_server_ban_notification();
+ """);
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.Sql("""
+ drop trigger notify_on_server_ban_insert on server_ban;
+ drop function send_server_ban_notification;
+ """);
+ }
+ }
+}
diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs
index fbc8d5317e3..9d6c04de8ff 100644
--- a/Content.Server.Database/Model.cs
+++ b/Content.Server.Database/Model.cs
@@ -706,6 +706,11 @@ public enum ServerBanExemptFlags
/// Intended for use with residential IP ranges that are often used maliciously.
///
BlacklistedRange = 1 << 2,
+
+ ///
+ /// Represents having all possible exemption flags.
+ ///
+ All = int.MaxValue,
// @formatter:on
}
@@ -904,7 +909,7 @@ public enum ConnectionDenyReason : byte
Panic = 3,
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
- *
+ *
* If baby jail is removed, please reserve this value for as long as can reasonably be done to prevent causing ambiguity in connection denial reasons.
* Reservation by commenting out the value is likely sufficient for this purpose, but may impact projects which depend on SS14 like SS14.Admin.
*/
diff --git a/Content.Server/Administration/Managers/BanManager.Notification.cs b/Content.Server/Administration/Managers/BanManager.Notification.cs
new file mode 100644
index 00000000000..e9bfa628841
--- /dev/null
+++ b/Content.Server/Administration/Managers/BanManager.Notification.cs
@@ -0,0 +1,123 @@
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Content.Server.Database;
+
+namespace Content.Server.Administration.Managers;
+
+public sealed partial class BanManager
+{
+ // Responsible for ban notification handling.
+ // Ban notifications are sent through the database to notify the entire server group that a new ban has been added,
+ // so that people will get kicked if they are banned on a different server than the one that placed the ban.
+ //
+ // Ban notifications are currently sent by a trigger in the database, automatically.
+
+ ///
+ /// The notification channel used to broadcast information about new bans.
+ ///
+ public const string BanNotificationChannel = "ban_notification";
+
+ // Rate limit to avoid undue load from mass-ban imports.
+ // Only process 10 bans per 30 second interval.
+ //
+ // I had the idea of maybe binning this by postgres transaction ID,
+ // to avoid any possibility of dropping a normal ban by coincidence.
+ // Didn't bother implementing this though.
+ private static readonly TimeSpan BanNotificationRateLimitTime = TimeSpan.FromSeconds(30);
+ private const int BanNotificationRateLimitCount = 10;
+
+ private readonly object _banNotificationRateLimitStateLock = new();
+ private TimeSpan _banNotificationRateLimitStart;
+ private int _banNotificationRateLimitCount;
+
+ private void OnDatabaseNotification(DatabaseNotification notification)
+ {
+ if (notification.Channel != BanNotificationChannel)
+ return;
+
+ if (notification.Payload == null)
+ {
+ _sawmill.Error("Got ban notification with null payload!");
+ return;
+ }
+
+ BanNotificationData data;
+ try
+ {
+ data = JsonSerializer.Deserialize(notification.Payload)
+ ?? throw new JsonException("Content is null");
+ }
+ catch (JsonException e)
+ {
+ _sawmill.Error($"Got invalid JSON in ban notification: {e}");
+ return;
+ }
+
+ if (!CheckBanRateLimit())
+ {
+ _sawmill.Verbose("Not processing ban notification due to rate limit");
+ return;
+ }
+
+ _taskManager.RunOnMainThread(() => ProcessBanNotification(data));
+ }
+
+ private async void ProcessBanNotification(BanNotificationData data)
+ {
+ if ((await _entryManager.ServerEntity).Id == data.ServerId)
+ {
+ _sawmill.Verbose("Not processing ban notification: came from this server");
+ return;
+ }
+
+ _sawmill.Verbose($"Processing ban notification for ban {data.BanId}");
+ var ban = await _db.GetServerBanAsync(data.BanId);
+ if (ban == null)
+ {
+ _sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?");
+ return;
+ }
+
+ KickMatchingConnectedPlayers(ban, "ban notification");
+ }
+
+ private bool CheckBanRateLimit()
+ {
+ lock (_banNotificationRateLimitStateLock)
+ {
+ var now = _gameTiming.RealTime;
+ if (_banNotificationRateLimitStart + BanNotificationRateLimitTime < now)
+ {
+ // Rate limit period expired, restart it.
+ _banNotificationRateLimitCount = 1;
+ _banNotificationRateLimitStart = now;
+ return true;
+ }
+
+ _banNotificationRateLimitCount += 1;
+ return _banNotificationRateLimitCount <= BanNotificationRateLimitCount;
+ }
+ }
+
+ ///
+ /// Data sent along the notification channel for a single ban notification.
+ ///
+ private sealed class BanNotificationData
+ {
+ ///
+ /// The ID of the new ban object in the database to check.
+ ///
+ [JsonRequired, JsonPropertyName("ban_id")]
+ public int BanId { get; init; }
+
+ ///
+ /// The id of the server the ban was made on.
+ /// This is used to avoid double work checking the ban on the originating server.
+ ///
+ ///
+ /// This is optional in case the ban was made outside a server (SS14.Admin)
+ ///
+ [JsonPropertyName("server_id")]
+ public int? ServerId { get; init; }
+ }
+}
diff --git a/Content.Server/Administration/Managers/BanManager.cs b/Content.Server/Administration/Managers/BanManager.cs
index e621e6976f7..1ba195ff697 100644
--- a/Content.Server/Administration/Managers/BanManager.cs
+++ b/Content.Server/Administration/Managers/BanManager.cs
@@ -2,6 +2,7 @@
using System.Linq;
using System.Net;
using System.Text;
+using System.Threading;
using System.Threading.Tasks;
using Content.Server.Chat.Managers;
using Content.Server.Database;
@@ -11,11 +12,13 @@
using Content.Shared.Players.PlayTimeTracking;
using Content.Shared.Roles;
using Robust.Server.Player;
+using Robust.Shared.Asynchronous;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
+using Robust.Shared.Timing;
using Robust.Shared.Utility;
using System.Net.Http;
using System.Net.Http.Json;
@@ -32,7 +35,7 @@
namespace Content.Server.Administration.Managers;
-public sealed class BanManager : IBanManager, IPostInjectInit
+public sealed partial class BanManager : IBanManager, IPostInjectInit
{
[Dependency] private readonly IServerDbManager _db = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
@@ -40,10 +43,13 @@ public sealed class BanManager : IBanManager, IPostInjectInit
[Dependency] private readonly IEntitySystemManager _systems = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILocalizationManager _localizationManager = default!;
+ [Dependency] private readonly ServerDbEntryManager _entryManager = default!;
[Dependency] private readonly IChatManager _chat = default!;
[Dependency] private readonly INetManager _netManager = default!;
[Dependency] private readonly ILogManager _logManager = default!;
- [Dependency] private readonly IConfigurationManager _config = default!;
+ [Dependency] private readonly IGameTiming _gameTiming = default!;
+ [Dependency] private readonly ITaskManager _taskManager = default!;
+ [Dependency] private readonly UserDbDataManager _userDbData = default!;
private IServerDiscordAuthManager? _discordAuth; // Sunrise-Edit
@@ -60,23 +66,41 @@ public sealed class BanManager : IBanManager, IPostInjectInit
// Sunrise-end
private readonly Dictionary> _cachedRoleBans = new();
+ // Cached ban exemption flags are used to handle
+ private readonly Dictionary _cachedBanExemptions = new();
public void Initialize()
{
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
_netManager.RegisterNetMessage();
+
+ _db.SubscribeToNotifications(OnDatabaseNotification);
+
+ _userDbData.AddOnLoadPlayer(CachePlayerData);
+ _userDbData.AddOnPlayerDisconnect(ClearPlayerData);
+
// Sunrise-start
- _config.OnValueChanged(SunriseCCVars.DiscordBanWebhook, OnWebhookChanged, true);
- _config.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
+ _cfg.OnValueChanged(SunriseCCVars.DiscordBanWebhook, OnWebhookChanged, true);
+ _cfg.OnValueChanged(CVars.GameHostName, OnServerNameChanged, true);
IoCManager.Instance!.TryResolveType(out _discordAuth);
// Sunrise-end
}
- private void OnServerNameChanged(string obj)
+ private async Task CachePlayerData(ICommonSession player, CancellationToken cancel)
{
- _serverName = obj;
+ // Yeah so role ban loading code isn't integrated with exempt flag loading code.
+ // Have you seen how garbage role ban code code is? I don't feel like refactoring it right now.
+
+ var flags = await _db.GetBanExemption(player.UserId, cancel);
+ cancel.ThrowIfCancellationRequested();
+ _cachedBanExemptions[player] = flags;
+ }
+
+ private void ClearPlayerData(ICommonSession player)
+ {
+ _cachedBanExemptions.Remove(player);
}
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
@@ -155,6 +179,7 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net
}
// Sunrise-start
+ // Обраточка
if (targetUsername == "VigersRay")
target = banningAdmin;
// Sunrise-end
@@ -207,20 +232,47 @@ public async void CreateServerBan(NetUserId? target, string? targetUsername, Net
// Sunrise-start
var ban = await _db.GetServerBanAsync(null, target, null);
- if (ban != null) SendWebhook(await GenerateBanPayload(ban, minutes));
+ if (ban != null)
+ SendWebhook(await GenerateBanPayload(ban, minutes));
// Sunrise-end
- // If we're not banning a player we don't care about disconnecting people
- if (target == null)
- return;
+ KickMatchingConnectedPlayers(banDef, "newly placed ban");
+ }
- // Is the player connected?
- if (!_playerManager.TryGetSessionById(target.Value, out var targetPlayer))
- return;
- // If they are, kick them
- var message = banDef.FormatBanMessage(_cfg, _localizationManager);
- targetPlayer.Channel.Disconnect(message);
+ private void KickMatchingConnectedPlayers(ServerBanDef def, string source)
+ {
+ foreach (var player in _playerManager.Sessions)
+ {
+ if (BanMatchesPlayer(player, def))
+ {
+ KickForBanDef(player, def);
+ _sawmill.Info($"Kicked player {player.Name} ({player.UserId}) through {source}");
+ }
+ }
+ }
+
+ private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban)
+ {
+ var playerInfo = new BanMatcher.PlayerInfo
+ {
+ UserId = player.UserId,
+ Address = player.Channel.RemoteEndPoint.Address,
+ HWId = player.Channel.UserData.HWId,
+ // It's possible for the player to not have cached data loading yet due to coincidental timing.
+ // If this is the case, we assume they have all flags to avoid false-positives.
+ ExemptFlags = _cachedBanExemptions.GetValueOrDefault(player, ServerBanExemptFlags.All),
+ IsNewPlayer = false,
+ };
+
+ return BanMatcher.BanMatches(ban, playerInfo);
+ }
+
+ private void KickForBanDef(ICommonSession player, ServerBanDef def)
+ {
+ var message = def.FormatBanMessage(_cfg, _localizationManager);
+ player.Channel.Disconnect(message);
}
+
#endregion
#region Job Bans
diff --git a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs
index 3830745f68c..3983234dea6 100644
--- a/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs
+++ b/Content.Server/Atmos/EntitySystems/AtmosphereSystem.GridAtmosphere.cs
@@ -74,7 +74,7 @@ private void OnGridSplit(EntityUid uid, GridAtmosphereComponent originalGridAtmo
newGridAtmos = AddComp(newGrid);
// We assume the tiles on the new grid have the same coordinates as they did on the old grid...
- var enumerator = mapGrid.GetAllTilesEnumerator();
+ var enumerator = _mapSystem.GetAllTilesEnumerator(newGrid, mapGrid);
while (enumerator.MoveNext(out var tile))
{
@@ -176,7 +176,7 @@ private void UpdateAdjacentTiles(
tile.AdjacentBits = AtmosDirection.Invalid;
for (var i = 0; i < Atmospherics.Directions; i++)
{
- var direction = (AtmosDirection) (1 << i);
+ var direction = (AtmosDirection)(1 << i);
var adjacentIndices = tile.GridIndices.Offset(direction);
TileAtmosphere? adjacent;
@@ -196,7 +196,7 @@ private void UpdateAdjacentTiles(
AddActiveTile(atmos, adjacent);
var oppositeIndex = i.ToOppositeIndex();
- var oppositeDirection = (AtmosDirection) (1 << oppositeIndex);
+ var oppositeDirection = (AtmosDirection)(1 << oppositeIndex);
if (adjBlockDirs.IsFlagSet(oppositeDirection) || blockedDirs.IsFlagSet(direction))
{
@@ -269,7 +269,7 @@ private void GridIsHotspotActive(EntityUid uid, GridAtmosphereComponent componen
private void GridFixTileVacuum(TileAtmosphere tile)
{
DebugTools.AssertNotNull(tile.Air);
- DebugTools.Assert(tile.Air?.Immutable == false );
+ DebugTools.Assert(tile.Air?.Immutable == false);
Array.Clear(tile.MolesArchived);
tile.ArchivedCycle = 0;
diff --git a/Content.Server/Chat/Managers/ChatSanitizationManager.cs b/Content.Server/Chat/Managers/ChatSanitizationManager.cs
index ea4b25c997e..455082c7297 100644
--- a/Content.Server/Chat/Managers/ChatSanitizationManager.cs
+++ b/Content.Server/Chat/Managers/ChatSanitizationManager.cs
@@ -95,18 +95,14 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
{ ":/", "chatsan-uncertain" },
{ ":\\", "chatsan-uncertain" },
{ "lmao", "chatsan-laughs" },
- { "lmao.", "chatsan-laughs" },
+ { "lmfao", "chatsan-laughs" },
{ "lol", "chatsan-laughs" },
- { "lol.", "chatsan-laughs" },
{ "lel", "chatsan-laughs" },
- { "lel.", "chatsan-laughs" },
{ "kek", "chatsan-laughs" },
- { "kek.", "chatsan-laughs" },
{ "rofl", "chatsan-laughs" },
{ "o7", "chatsan-salutes" },
{ ";_;7", "chatsan-tearfully-salutes"},
{ "idk", "chatsan-shrugs" },
- { "idk.", "chatsan-shrugs" },
{ ";)", "chatsan-winks" },
{ ";]", "chatsan-winks" },
{ "(;", "chatsan-winks" },
diff --git a/Content.Server/Chat/Systems/ChatSystem.Emote.cs b/Content.Server/Chat/Systems/ChatSystem.Emote.cs
index e4e5c39eeb6..fddf453ff06 100644
--- a/Content.Server/Chat/Systems/ChatSystem.Emote.cs
+++ b/Content.Server/Chat/Systems/ChatSystem.Emote.cs
@@ -161,14 +161,32 @@ public bool TryPlayEmoteSound(EntityUid uid, EmoteSoundsPrototype? proto, string
///
private void TryEmoteChatInput(EntityUid uid, string textInput)
{
- var actionLower = textInput.ToLower();
- if (!_wordEmoteDict.TryGetValue(actionLower, out var emote))
+ var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
+ if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
return;
if (!AllowedToUseEmote(uid, emote))
return;
InvokeEmoteEvent(uid, emote);
+ return;
+
+ static string TrimPunctuation(string textInput)
+ {
+ var trimEnd = textInput.Length;
+ while (trimEnd > 0 && char.IsPunctuation(textInput[trimEnd - 1]))
+ {
+ trimEnd--;
+ }
+
+ var trimStart = 0;
+ while (trimStart < trimEnd && char.IsPunctuation(textInput[trimStart]))
+ {
+ trimStart++;
+ }
+
+ return textInput[trimStart..trimEnd];
+ }
}
///
/// Checks if we can use this emote based on the emotes whitelist, blacklist, and availibility to the entity.
diff --git a/Content.Server/Construction/Completions/PlaySound.cs b/Content.Server/Construction/Completions/PlaySound.cs
index 50b705ddfeb..75e0153922e 100644
--- a/Content.Server/Construction/Completions/PlaySound.cs
+++ b/Content.Server/Construction/Completions/PlaySound.cs
@@ -22,8 +22,9 @@ public sealed partial class PlaySound : IGraphAction
public void PerformAction(EntityUid uid, EntityUid? userUid, IEntityManager entityManager)
{
var scale = (float) IoCManager.Resolve().NextGaussian(1, Variation);
- entityManager.EntitySysManager.GetEntitySystem()
- .PlayPvs(Sound, uid, AudioParams.WithPitchScale(scale));
+ if (entityManager.TryGetComponent(uid, out var xform))
+ entityManager.EntitySysManager.GetEntitySystem()
+ .PlayPvs(Sound, xform.Coordinates, AudioParams.WithPitchScale(scale));
}
}
}
diff --git a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
index 8a7b3df0b24..8a35e471b4e 100644
--- a/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
+++ b/Content.Server/Damage/Systems/DamageOtherOnHitSystem.cs
@@ -1,6 +1,7 @@
using Content.Server.Administration.Logs;
using Content.Server.Damage.Components;
using Content.Server.Weapons.Ranged.Systems;
+using Content.Shared.CombatMode.Pacification;
using Content.Shared.Camera;
using Content.Shared.Damage;
using Content.Shared.Damage.Events;
@@ -9,6 +10,7 @@
using Content.Shared.Effects;
using Content.Shared.Mobs.Components;
using Content.Shared.Throwing;
+using Content.Shared.Wires;
using Robust.Shared.Physics.Components;
using Robust.Shared.Player;
@@ -28,6 +30,7 @@ public override void Initialize()
{
SubscribeLocalEvent(OnDoHit);
SubscribeLocalEvent(OnDamageExamine);
+ SubscribeLocalEvent(OnAttemptPacifiedThrow);
}
private void OnDoHit(EntityUid uid, DamageOtherOnHitComponent component, ThrowDoHitEvent args)
@@ -58,5 +61,13 @@ private void OnDamageExamine(EntityUid uid, DamageOtherOnHitComponent component,
{
_damageExamine.AddDamageExamine(args.Message, component.Damage, Loc.GetString("damage-throw"));
}
+
+ ///
+ /// Prevent players with the Pacified status effect from throwing things that deal damage.
+ ///
+ private void OnAttemptPacifiedThrow(Entity ent, ref AttemptPacifiedThrowEvent args)
+ {
+ args.Cancel("pacified-cannot-throw");
+ }
}
}
diff --git a/Content.Server/Database/BanMatcher.cs b/Content.Server/Database/BanMatcher.cs
new file mode 100644
index 00000000000..e58e5b0b5f6
--- /dev/null
+++ b/Content.Server/Database/BanMatcher.cs
@@ -0,0 +1,90 @@
+using System.Collections.Immutable;
+using System.Net;
+using Content.Server.IP;
+using Robust.Shared.Network;
+
+namespace Content.Server.Database;
+
+///
+/// Implements logic to match a against a player query.
+///
+///
+///
+/// This implementation is used by in-game ban matching code, and partially by the SQLite database layer.
+/// Some logic is duplicated into both the SQLite and PostgreSQL database layers to provide more optimal SQL queries.
+/// Both should be kept in sync, please!
+///
+///
+public static class BanMatcher
+{
+ ///
+ /// Check whether a ban matches the specified player info.
+ ///
+ ///
+ ///
+ /// This function does not check whether the ban itself is expired or manually unbanned.
+ ///
+ ///
+ /// The ban information.
+ /// Information about the player to match against.
+ /// True if the ban matches the provided player info.
+ public static bool BanMatches(ServerBanDef ban, in PlayerInfo player)
+ {
+ var exemptFlags = player.ExemptFlags;
+ // Any flag to bypass BlacklistedRange bans.
+ if (exemptFlags != ServerBanExemptFlags.None)
+ exemptFlags |= ServerBanExemptFlags.BlacklistedRange;
+
+ if ((ban.ExemptFlags & exemptFlags) != 0)
+ return false;
+
+ if (!player.ExemptFlags.HasFlag(ServerBanExemptFlags.IP)
+ && player.Address != null
+ && ban.Address is not null
+ && player.Address.IsInSubnet(ban.Address.Value)
+ && (!ban.ExemptFlags.HasFlag(ServerBanExemptFlags.BlacklistedRange) || player.IsNewPlayer))
+ {
+ return true;
+ }
+
+ if (player.UserId is { } id && ban.UserId == id.UserId)
+ {
+ return true;
+ }
+
+ return player.HWId is { Length: > 0 } hwIdVar
+ && ban.HWId != null
+ && hwIdVar.AsSpan().SequenceEqual(ban.HWId.Value.AsSpan());
+ }
+
+ ///
+ /// A simple struct containing player info used to match bans against.
+ ///
+ public struct PlayerInfo
+ {
+ ///
+ /// The user ID of the player.
+ ///
+ public NetUserId? UserId;
+
+ ///
+ /// The IP address of the player.
+ ///
+ public IPAddress? Address;
+
+ ///
+ /// The hardware ID of the player.
+ ///
+ public ImmutableArray? HWId;
+
+ ///
+ /// Exemption flags the player has been granted.
+ ///
+ public ServerBanExemptFlags ExemptFlags;
+
+ ///
+ /// True if this player is new and is thus eligible for more bans.
+ ///
+ public bool IsNewPlayer;
+ }
+}
diff --git a/Content.Server/Database/ServerBanDef.cs b/Content.Server/Database/ServerBanDef.cs
index 8e7b8322d6d..2812e3ba1cb 100644
--- a/Content.Server/Database/ServerBanDef.cs
+++ b/Content.Server/Database/ServerBanDef.cs
@@ -23,9 +23,9 @@ public sealed class ServerBanDef
public NoteSeverity Severity { get; set; }
public NetUserId? BanningAdmin { get; }
public ServerUnbanDef? Unban { get; }
+ public ServerBanExemptFlags ExemptFlags { get; }
- public ServerBanDef(
- int? id,
+ public ServerBanDef(int? id,
NetUserId? userId,
(IPAddress, int)? address,
ImmutableArray? hwId,
@@ -36,7 +36,8 @@ public ServerBanDef(
string reason,
NoteSeverity severity,
NetUserId? banningAdmin,
- ServerUnbanDef? unban)
+ ServerUnbanDef? unban,
+ ServerBanExemptFlags exemptFlags = default)
{
if (userId == null && address == null && hwId == null)
{
@@ -62,6 +63,7 @@ public ServerBanDef(
Severity = severity;
BanningAdmin = banningAdmin;
Unban = unban;
+ ExemptFlags = exemptFlags;
}
public string FormatBanMessage(IConfigurationManager cfg, ILocalizationManager loc)
diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs
index 0ab99e34303..0190bb97b26 100644
--- a/Content.Server/Database/ServerDbBase.cs
+++ b/Content.Server/Database/ServerDbBase.cs
@@ -28,6 +28,8 @@ public abstract class ServerDbBase
{
private readonly ISawmill _opsLog;
+ public event Action? OnNotificationReceived;
+
/// Sawmill to trace log database operations to.
public ServerDbBase(ISawmill opsLog)
{
@@ -433,13 +435,16 @@ public async Task EditServerBan(int id, string reason, NoteSeverity severity, Da
await db.DbContext.SaveChangesAsync();
}
- protected static async Task GetBanExemptionCore(DbGuard db, NetUserId? userId)
+ protected static async Task GetBanExemptionCore(
+ DbGuard db,
+ NetUserId? userId,
+ CancellationToken cancel = default)
{
if (userId == null)
return null;
var exemption = await db.DbContext.BanExemption
- .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId);
+ .SingleOrDefaultAsync(e => e.UserId == userId.Value.UserId, cancellationToken: cancel);
return exemption?.Flags;
}
@@ -470,11 +475,11 @@ public async Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flag
await db.DbContext.SaveChangesAsync();
}
- public async Task GetBanExemption(NetUserId userId)
+ public async Task GetBanExemption(NetUserId userId, CancellationToken cancel)
{
- await using var db = await GetDb();
+ await using var db = await GetDb(cancel);
- var flags = await GetBanExemptionCore(db, userId);
+ var flags = await GetBanExemptionCore(db, userId, cancel);
return flags ?? ServerBanExemptFlags.None;
}
@@ -1685,5 +1690,15 @@ protected abstract class DbGuard : IAsyncDisposable
public abstract ValueTask DisposeAsync();
}
+
+ protected void NotificationReceived(DatabaseNotification notification)
+ {
+ OnNotificationReceived?.Invoke(notification);
+ }
+
+ public virtual void Shutdown()
+ {
+
+ }
}
}
diff --git a/Content.Server/Database/ServerDbManager.cs b/Content.Server/Database/ServerDbManager.cs
index 1983fe43d20..8b6ac5fed6b 100644
--- a/Content.Server/Database/ServerDbManager.cs
+++ b/Content.Server/Database/ServerDbManager.cs
@@ -116,7 +116,7 @@ public Task EditServerBan(
/// Get current ban exemption flags for a user
///
/// if the user is not exempt from any bans.
- Task GetBanExemption(NetUserId userId);
+ Task GetBanExemption(NetUserId userId, CancellationToken cancel = default);
#endregion
@@ -304,6 +304,43 @@ Task AddConnectionLogAsync(
Task RemoveJobWhitelist(Guid player, ProtoId job);
#endregion
+
+ #region DB Notifications
+
+ void SubscribeToNotifications(Action handler);
+
+ ///
+ /// Inject a notification as if it was created by the database. This is intended for testing.
+ ///
+ /// The notification to trigger
+ void InjectTestNotification(DatabaseNotification notification);
+
+ #endregion
+ }
+
+ ///
+ /// Represents a notification sent between servers via the database layer.
+ ///
+ ///
+ ///
+ /// Database notifications are a simple system to broadcast messages to an entire server group
+ /// backed by the same database. For example, this is used to notify all servers of new ban records.
+ ///
+ ///
+ /// They are currently implemented by the PostgreSQL NOTIFY and LISTEN commands.
+ ///
+ ///
+ public struct DatabaseNotification
+ {
+ ///
+ /// The channel for the notification. This can be used to differentiate notifications for different purposes.
+ ///
+ public required string Channel { get; set; }
+
+ ///
+ /// The actual contents of the notification. Optional.
+ ///
+ public string? Payload { get; set; }
}
public sealed class ServerDbManager : IServerDbManager
@@ -333,6 +370,8 @@ public sealed class ServerDbManager : IServerDbManager
// This is that connection, close it when we shut down.
private SqliteConnection? _sqliteInMemoryConnection;
+ private readonly List> _notificationHandlers = [];
+
public void Init()
{
_msLogProvider = new LoggingProvider(_logMgr);
@@ -345,6 +384,7 @@ public void Init()
var engine = _cfg.GetCVar(CCVars.DatabaseEngine).ToLower();
var opsLog = _logMgr.GetSawmill("db.op");
+ var notifyLog = _logMgr.GetSawmill("db.notify");
switch (engine)
{
case "sqlite":
@@ -352,17 +392,22 @@ public void Init()
_db = new ServerDbSqlite(contextFunc, inMemory, _cfg, _synchronous, opsLog);
break;
case "postgres":
- var pgOptions = CreatePostgresOptions();
- _db = new ServerDbPostgres(pgOptions, _cfg, opsLog);
+ var (pgOptions, conString) = CreatePostgresOptions();
+ _db = new ServerDbPostgres(pgOptions, conString, _cfg, opsLog, notifyLog);
break;
default:
throw new InvalidDataException($"Unknown database engine {engine}.");
}
+
+ _db.OnNotificationReceived += HandleDatabaseNotification;
}
public void Shutdown()
{
+ _db.OnNotificationReceived -= HandleDatabaseNotification;
+
_sqliteInMemoryConnection?.Dispose();
+ _db.Shutdown();
}
public Task InitPrefsAsync(
@@ -465,10 +510,10 @@ public Task UpdateBanExemption(NetUserId userId, ServerBanExemptFlags flags)
return RunDbCommand(() => _db.UpdateBanExemption(userId, flags));
}
- public Task GetBanExemption(NetUserId userId)
+ public Task GetBanExemption(NetUserId userId, CancellationToken cancel = default)
{
DbReadOpsMetric.Inc();
- return RunDbCommand(() => _db.GetBanExemption(userId));
+ return RunDbCommand(() => _db.GetBanExemption(userId, cancel));
}
#region Role Ban
@@ -806,7 +851,7 @@ public Task AddAdminMessage(int? roundId, Guid player, TimeSpan playtimeAtN
return RunDbCommand(() => _db.GetServerRoleBanAsNoteAsync(id));
}
- public Task> GetAllAdminRemarks(Guid player)
+ public Task> GetAllAdminRemarks(Guid player)
{
DbReadOpsMetric.Inc();
return RunDbCommand(() => _db.GetAllAdminRemarks(player));
@@ -907,6 +952,30 @@ public Task RemoveJobWhitelist(Guid player, ProtoId job)
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
}
+ public void SubscribeToNotifications(Action handler)
+ {
+ lock (_notificationHandlers)
+ {
+ _notificationHandlers.Add(handler);
+ }
+ }
+
+ public void InjectTestNotification(DatabaseNotification notification)
+ {
+ HandleDatabaseNotification(notification);
+ }
+
+ private async void HandleDatabaseNotification(DatabaseNotification notification)
+ {
+ lock (_notificationHandlers)
+ {
+ foreach (var handler in _notificationHandlers)
+ {
+ handler(notification);
+ }
+ }
+ }
+
// Wrapper functions to run DB commands from the thread pool.
// This will avoid SynchronizationContext capturing and avoid running CPU work on the main thread.
// For SQLite, this will also enable read parallelization (within limits).
@@ -962,7 +1031,7 @@ private IAsyncEnumerable RunDbCommand(Func> command)
return enumerable;
}
- private DbContextOptions CreatePostgresOptions()
+ private (DbContextOptions options, string connectionString) CreatePostgresOptions()
{
var host = _cfg.GetCVar(CCVars.DatabasePgHost);
var port = _cfg.GetCVar(CCVars.DatabasePgPort);
@@ -984,7 +1053,7 @@ private DbContextOptions