diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
index ddc9752b1ed..5b8a1cdec3d 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.Appearance.cs
@@ -215,6 +215,8 @@ private void SetSex(Sex newSex)
break;
}
+ UpdateTTSVoicesControls(); // Art-TTS
+
UpdateGenderControls();
_markingsModel.SetOrganSexes(newSex);
ReloadPreview();
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
index 84c5b75d1cb..b4af1bc5760 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml
@@ -92,7 +92,20 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
index 3b444bf4162..3a83a41bb81 100644
--- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
+++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs
@@ -2,6 +2,7 @@
using Content.Client.Message;
using Content.Client.Players.PlayTimeTracking;
using Content.Client.Sprite;
+using Content.Shared._Art.ArtCVar; // Art-TTS
using Content.Shared.CCVar;
using Content.Shared.GameTicking;
using Content.Shared.Humanoid;
@@ -191,6 +192,14 @@ public HumanoidProfileEditor(
#endregion Gender
+ // Art-TTS End
+ if (configurationManager.GetCVar(ArtCVars.TTSClientEnabled))
+ {
+ TTSContainer.Visible = true;
+ InitializeVoice();
+ }
+ // Art-TTS End
+
RefreshSpecies();
SpeciesButton.OnItemSelected += args =>
@@ -288,6 +297,8 @@ public HumanoidProfileEditor(
RefreshFlavorText();
+ UpdateTTSVoicesControls(); // Art-TTS
+
#region Dummy
SpriteRotateLeft.OnPressed += _ =>
@@ -366,6 +377,7 @@ public void SetProfile(HumanoidCharacterProfile? profile, int? slot)
IsDirty = false;
JobOverride = null;
+ UpdateTTSVoicesControls(); // Art-TTS
UpdateNameEdit();
UpdateFlavorTextEdit();
UpdateSexControls();
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml b/Content.Client/Options/UI/Tabs/AudioTab.xaml
index 5764755bb9a..47c9cc8861b 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml
@@ -8,6 +8,7 @@
+
@@ -15,6 +16,7 @@
+
diff --git a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
index d57f36e74f8..c8e1b36c5d9 100644
--- a/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
+++ b/Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
@@ -1,5 +1,6 @@
using Content.Client.Administration.Managers;
using Content.Client.Audio;
+using Content.Shared._Art.ArtCVar; // Art-TTS
using Content.Shared.CCVar;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
@@ -28,6 +29,13 @@ public AudioTab()
scale: ContentAudioSystem.MasterVolumeMultiplier);
masterVolume.ImmediateValueChanged += OnMasterVolumeSliderChanged;
+ // Art-TTS Start
+ Control.AddOptionPercentSlider(
+ ArtCVars.TTSVolume,
+ SliderVolumeTts,
+ scale: ContentAudioSystem.TtsMultiplier);
+ // Art-TTS End
+
Control.AddOptionPercentSlider(
CVars.MidiVolume,
SliderVolumeMidi,
@@ -59,6 +67,7 @@ public AudioTab()
_cfg.GetCVar(CCVars.MinMaxAmbientSourcesConfigured),
_cfg.GetCVar(CCVars.MaxMaxAmbientSourcesConfigured));
+ Control.AddOptionCheckBox(ArtCVars.TTSClientEnabled, TtsClientCheckBox); // Art-TTS
Control.AddOptionCheckBox(CCVars.LobbyMusicEnabled, LobbyMusicCheckBox);
Control.AddOptionCheckBox(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
diff --git a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
index b2b374cac5b..c45e69101d7 100644
--- a/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
+++ b/Content.Client/VoiceMask/VoiceMaskBoundUserInterface.cs
@@ -26,6 +26,7 @@ protected override void Open()
_window.OnNameChange += OnNameSelected;
_window.OnVerbChange += verb => SendMessage(new VoiceMaskChangeVerbMessage(verb));
+ _window.OnVoiceChange += voice => SendMessage(new VoiceMaskChangeVoiceMessage(voice)); // Art-TTS
_window.OnToggle += OnToggle;
_window.OnAccentToggle += OnAccentToggle;
}
@@ -52,7 +53,7 @@ protected override void UpdateState(BoundUserInterfaceState state)
return;
}
- _window.UpdateState(cast.Name, cast.Verb, cast.Active, cast.AccentHide);
+ _window.UpdateState(cast.Name, cast.Voice, cast.Verb, cast.Active, cast.AccentHide); // Art-TTS
}
protected override void Dispose(bool disposing)
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
index 18416757b9e..0c54ffb8019 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml
@@ -14,5 +14,15 @@
+
+
+
+
+
+
+
diff --git a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
index a5e70362831..4cd06fc6497 100644
--- a/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
+++ b/Content.Client/VoiceMask/VoiceMaskNameChangeWindow.xaml.cs
@@ -28,6 +28,8 @@ public VoiceMaskNameChangeWindow()
OnNameChange?.Invoke(NameSelector.Text);
};
+ ReloadVoices(); // Art-TTS
+
SpeechVerbSelector.OnItemSelected += args =>
{
OnVerbChange?.Invoke((string?) args.Button.GetItemMetadata(args.Id));
@@ -69,7 +71,7 @@ private void AddVerb(string name, string? verb)
SpeechVerbSelector.SelectId(id);
}
- public void UpdateState(string name, string? verb, bool active, bool accentHide)
+ public void UpdateState(string name, string voice, string? verb, bool active, bool accentHide) // Art-TTS
{
NameSelector.Text = name;
_verb = verb;
@@ -84,5 +86,10 @@ public void UpdateState(string name, string? verb, bool active, bool accentHide)
break;
}
}
+ // Art-TTS Start
+ var voiceIdx = _voices.FindIndex(v => v.ID == voice);
+ if (voiceIdx != -1)
+ VoiceSelector.Select(voiceIdx);
+ // Art-TTS End
}
}
diff --git a/Content.Client/_Art/TTS/ContentAudioSystem.cs b/Content.Client/_Art/TTS/ContentAudioSystem.cs
new file mode 100644
index 00000000000..fe11a1ed336
--- /dev/null
+++ b/Content.Client/_Art/TTS/ContentAudioSystem.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Audio;
+
+namespace Content.Client.Audio;
+
+public sealed partial class ContentAudioSystem : SharedContentAudioSystem
+{
+ public const float TtsMultiplier = 3f;
+}
diff --git a/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs b/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
new file mode 100644
index 00000000000..c5d3b57e147
--- /dev/null
+++ b/Content.Client/_Art/TTS/HumanoidProfileEditor.TTS.cs
@@ -0,0 +1,71 @@
+using System.Linq;
+using Content.Client._Art.TTS;
+using Content.Client.Lobby;
+// using Content.Corvax.Interfaces.Shared;
+using Content.Shared._Art.TTS;
+using Content.Shared.Preferences;
+
+namespace Content.Client.Lobby.UI;
+
+public sealed partial class HumanoidProfileEditor
+{
+ private List _voiceList = new();
+
+ private void InitializeVoice()
+ {
+ _voiceList = _prototypeManager
+ .EnumeratePrototypes()
+ .OrderBy(o => Loc.GetString(o.Name))
+ .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
+ .ToList();
+
+ VoiceButton.OnItemSelected += args =>
+ {
+ VoiceButton.SelectId(args.Id);
+ SetVoice(_voiceList[args.Id].ID);
+ };
+
+ VoicePlayButton.OnPressed += _ => PlayPreviewTTS();
+ }
+
+ private void UpdateTTSVoicesControls()
+ {
+ if (Profile is null)
+ return;
+
+ VoiceButton.Clear();
+
+ var firstVoiceChoiceId = 1;
+ for (var i = 0; i < _voiceList.Count; i++)
+ {
+ var voice = _voiceList[i];
+
+ var name = Loc.GetString(voice.Name);
+ VoiceButton.AddItem(name, i);
+
+ if (firstVoiceChoiceId == 1)
+ firstVoiceChoiceId = i;
+ }
+
+ var voiceChoiceId = _voiceList.FindIndex(x => x.ID == Profile.Voice);
+ if (!VoiceButton.TrySelectId(voiceChoiceId) &&
+ VoiceButton.TrySelectId(firstVoiceChoiceId))
+ {
+ SetVoice(_voiceList[firstVoiceChoiceId].ID);
+ }
+ }
+
+ private void PlayPreviewTTS()
+ {
+ if (Profile is null)
+ return;
+
+ _entManager.System().RequestPreviewTTS(Profile.Voice);
+ }
+
+ private void SetVoice(string newVoice)
+ {
+ Profile = Profile?.WithVoice(newVoice);
+ IsDirty = true;
+ }
+}
diff --git a/Content.Client/_Art/TTS/TTSSystem.cs b/Content.Client/_Art/TTS/TTSSystem.cs
new file mode 100644
index 00000000000..4de7cb05a32
--- /dev/null
+++ b/Content.Client/_Art/TTS/TTSSystem.cs
@@ -0,0 +1,191 @@
+using Content.Shared.Chat;
+using Content.Shared._Art.ArtCVar;
+using Content.Shared._Art.TTS;
+using Robust.Client.Audio;
+using Robust.Shared.Audio.Components;
+using Robust.Shared.Audio.Systems;
+using Robust.Shared.Audio;
+using Robust.Shared.Configuration;
+using Robust.Shared.Utility;
+using System.IO;
+
+namespace Content.Client._Art.TTS;
+
+internal record struct TTSQueueElem(AudioStream Audio, bool IsWhisper, NetEntity Source);
+
+///
+/// Plays TTS audio in world
+///
+// ReSharper disable once InconsistentNaming
+public sealed class TTSSystem : EntitySystem
+{
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+ [Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly IAudioManager _audioLoader = default!;
+
+ private ISawmill _sawmill = default!;
+ private bool _enabled = true;
+
+ ///
+ /// Reducing the volume of the TTS when whispering. Will be converted to logarithm.
+ ///
+ private const float WhisperFade = 4f;
+
+ ///
+ /// The volume at which the TTS sound will not be heard.
+ ///
+ private const float MinimalVolume = -10f;
+
+ ///
+ /// Maximum queued tts talks per entity.
+ ///
+ private const int MaxQueuedSounds = 20;
+
+ private float _volume = 0.0f;
+ internal List _toDelete = new();
+
+ // Author -> Queue of sounds from different sources
+ private Dictionary> _queue = new();
+ // Author -> currently playing sound
+ private Dictionary _playing = new();
+
+ public override void Initialize()
+ {
+ _sawmill = Logger.GetSawmill("tts");
+ _cfg.OnValueChanged(ArtCVars.TTSVolume, OnTtsVolumeChanged, true);
+ _cfg.OnValueChanged(ArtCVars.TTSClientEnabled, OnTtsClientOptionChanged, true);
+ SubscribeNetworkEvent(OnPlayTTS);
+ }
+
+ public override void Shutdown()
+ {
+ base.Shutdown();
+ _cfg.UnsubValueChanged(ArtCVars.TTSVolume, OnTtsVolumeChanged);
+ _cfg.UnsubValueChanged(ArtCVars.TTSClientEnabled, OnTtsClientOptionChanged);
+ }
+
+ public override void FrameUpdate(float frameTime)
+ {
+ if (!_enabled) return;
+ _toDelete.Clear();
+ foreach (var (uid, comp) in _playing)
+ {
+ if (comp != null && !comp.Deleted)
+ {
+ if (!comp.Playing)
+ _toDelete.Add(uid);
+ }
+ else
+ {
+ _toDelete.Add(uid);
+ }
+ }
+ foreach (var uid in _toDelete)
+ {
+ _playing.Remove(uid);
+ }
+
+ _toDelete.Clear();
+ foreach (var (author, queue) in _queue)
+ {
+ if (queue.Count <= 0)
+ { // If author doesn't want to tell anything, ignore it.
+ _toDelete.Add(author);
+ continue;
+ }
+ if (_playing.ContainsKey(author)) continue; // If author is still talking right now.
+ if (!queue.TryDequeue(out var elem)) continue; // Just in case if queue cleared.
+ if (!TryGetEntity(elem.Source, out var local_source))
+ { // If entity is outside PVS.
+ continue;
+ }
+ _playing[author] = PlayTTSFromUid(local_source, elem.Audio, elem.IsWhisper);
+ }
+ foreach (var author in _toDelete)
+ {
+ _queue.Remove(author);
+ }
+ }
+
+ public AudioComponent? PlayTTSFromUid(EntityUid? uid, AudioStream audioStream, bool isWhisper)
+ {
+ var audioParams = AudioParams.Default
+ .WithVolume(AdjustVolume(isWhisper))
+ .WithMaxDistance(AdjustDistance(isWhisper));
+ (EntityUid Entity, AudioComponent Component)? stream;
+
+ _sawmill.Verbose($"Playing TTS audio {audioStream.Length} bytes from {uid} entity");
+
+ if (uid is not null)
+ {
+ stream = _audio.PlayEntity(audioStream, uid.Value, null, audioParams);
+ }
+ else
+ {
+ stream = _audio.PlayGlobal(audioStream, null, audioParams);
+ }
+
+ return stream?.Component;
+ }
+
+ public void RequestPreviewTTS(string voiceId)
+ {
+ RaiseNetworkEvent(new RequestPreviewTTSEvent(voiceId));
+ }
+
+ private void OnTtsClientOptionChanged(bool option)
+ {
+ _enabled = option;
+ RaiseNetworkEvent(new ClientOptionTTSEvent(option));
+ }
+
+ private void OnTtsVolumeChanged(float volume)
+ {
+ _volume = volume;
+ }
+
+ private void OnPlayTTS(PlayTTSEvent ev)
+ {
+ if (!_enabled) return;
+ var source = ev.SourceUid ?? NetEntity.Invalid;
+ var author = ev.Author ?? source;
+ if (!_queue.ContainsKey(author))
+ _queue[author] = new();
+
+ if (_queue[author].Count >= MaxQueuedSounds)
+ return;
+
+ var audioStream = _audioLoader.LoadAudioWav(new MemoryStream(ev.Data));
+
+ if (!author.Valid)
+ {
+ PlayTTSFromUid(null, audioStream, ev.IsWhisper);
+ }
+ else
+ {
+ _queue[author].Enqueue(new TTSQueueElem
+ {
+ Audio = audioStream,
+ IsWhisper = ev.IsWhisper,
+ Source = source,
+ });
+ }
+ }
+
+ private float AdjustVolume(bool isWhisper)
+ {
+ var volume = MinimalVolume + SharedAudioSystem.GainToVolume(_volume * 3.0f);
+
+ if (isWhisper)
+ {
+ volume -= SharedAudioSystem.GainToVolume(WhisperFade);
+ }
+
+ return volume;
+ }
+
+ private float AdjustDistance(bool isWhisper)
+ {
+ return isWhisper ? TTSConfig.WhisperMuffledRange : TTSConfig.VoiceRange;
+ }
+}
diff --git a/Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs b/Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs
new file mode 100644
index 00000000000..35b9de09f89
--- /dev/null
+++ b/Content.Client/_Art/TTS/VoiceMaskNameChangeWindow.xaml.cs
@@ -0,0 +1,42 @@
+using System.Linq;
+using Content.Shared._Art.ArtCVar;
+using Robust.Shared.Configuration;
+using Robust.Shared.Prototypes;
+using Content.Shared._Art.TTS;
+using Content.Client.UserInterface.Controls;
+
+namespace Content.Client.VoiceMask;
+
+public sealed partial class VoiceMaskNameChangeWindow : FancyWindow
+{
+ public Action? OnVoiceChange;
+ private List _voices = new();
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+ [Dependency] private readonly IConfigurationManager _cfg = default!;
+
+ private void ReloadVoices()
+ {
+ if (_cfg is null)
+ return;
+ TTSContainer.Visible = _cfg.GetCVar(ArtCVars.TTSClientEnabled);
+ if (!_cfg.GetCVar(ArtCVars.TTSClientEnabled))
+ return;
+ VoiceSelector.OnItemSelected += args =>
+ {
+ VoiceSelector.SelectId(args.Id);
+ if (VoiceSelector.SelectedMetadata != null)
+ OnVoiceChange?.Invoke((string)VoiceSelector.SelectedMetadata);
+ };
+ _voices = _proto
+ .EnumeratePrototypes()
+ .OrderBy(o => Loc.GetString(o.Name))
+ .OrderBy(o => ((o.Gender == "male") ? 0b01 : 0) + ((o.Gender == "female") ? 0b10 : 0))
+ .ToList();
+ for (var i = 0; i < _voices.Count; i++)
+ {
+ var name = Loc.GetString(_voices[i].Name);
+ VoiceSelector.AddItem(name);
+ VoiceSelector.SetItemMetadata(i, _voices[i].ID);
+ }
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
index ad93e083fbd..18a6738b4d7 100644
--- a/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
+++ b/Content.IntegrationTests/Tests/Preferences/ServerDbSqliteTests.cs
@@ -48,10 +48,11 @@ private static HumanoidCharacterProfile CharlieCharlieson()
FlavorText = "The biggest boy around.",
Species = "Human",
Age = 21,
+ Voice = "papich",
Appearance = new(
Color.Azure,
Color.Beige,
- new ())
+ new())
};
}
diff --git a/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.Designer.cs
new file mode 100644
index 00000000000..db03aa4fbea
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.Designer.cs
@@ -0,0 +1,2134 @@
+//
+using System;
+using System.Collections.Generic;
+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("20260327081423_AddTtsVoice")]
+ partial class AddTtsVoice
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "10.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("Deadminned")
+ .HasColumnType("boolean")
+ .HasColumnName("deadminned");
+
+ b.Property("Suspended")
+ .HasColumnType("boolean")
+ .HasColumnName("suspended");
+
+ 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")
+ .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")
+ .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.Ban", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ 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("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("PlaytimeAtNote")
+ .HasColumnType("interval")
+ .HasColumnName("playtime_at_note");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Type")
+ .HasColumnType("smallint")
+ .HasColumnName("type");
+
+ b.HasKey("Id")
+ .HasName("PK_ban");
+
+ b.HasIndex("BanningAdmin");
+
+ b.HasIndex("LastEditedById");
+
+ b.ToTable("ban", null, t =>
+ {
+ t.HasCheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_address_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_address");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_address_ban_id");
+
+ b.ToTable("ban_address", null, t =>
+ {
+ t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
+ });
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_hwid_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_hwid");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_hwid_ban_id");
+
+ b.ToTable("ban_hwid", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_player_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("UserId")
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_player");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_player_ban_id");
+
+ b.HasIndex("UserId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_player", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRole", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_role_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("RoleId")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_id");
+
+ b.Property("RoleType")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("role_type");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_role");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_role_ban_id");
+
+ b.HasIndex("RoleType", "RoleId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_role", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRound", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_round_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("BanId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_id");
+
+ b.Property("RoundId")
+ .HasColumnType("integer")
+ .HasColumnName("round_id");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_round");
+
+ b.HasIndex("BanId")
+ .HasDatabaseName("IX_ban_round_ban_id");
+
+ b.HasIndex("RoundId", "BanId")
+ .IsUnique();
+
+ b.ToTable("ban_round", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ban_template_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("AutoDelete")
+ .HasColumnType("boolean")
+ .HasColumnName("auto_delete");
+
+ b.Property("ExemptFlags")
+ .HasColumnType("integer")
+ .HasColumnName("exempt_flags");
+
+ b.Property("Hidden")
+ .HasColumnType("boolean")
+ .HasColumnName("hidden");
+
+ b.Property("Length")
+ .HasColumnType("interval")
+ .HasColumnName("length");
+
+ b.Property("Reason")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("reason");
+
+ b.Property("Severity")
+ .HasColumnType("integer")
+ .HasColumnName("severity");
+
+ b.Property("Title")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("title");
+
+ b.HasKey("Id")
+ .HasName("PK_ban_template");
+
+ b.ToTable("ban_template", (string)null);
+ });
+
+ modelBuilder.Entity("Content.Server.Database.Blacklist", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("uuid")
+ .HasColumnName("user_id");
+
+ b.HasKey("UserId")
+ .HasName("PK_blacklist");
+
+ b.ToTable("blacklist", (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("ServerId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("server_id");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.Property("Trust")
+ .HasColumnType("real")
+ .HasColumnName("trust");
+
+ 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("Time");
+
+ 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.IPIntelCache", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasColumnName("ipintel_cache_id");
+
+ NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
+
+ b.Property("Address")
+ .IsRequired()
+ .HasColumnType("inet")
+ .HasColumnName("address");
+
+ b.Property("Score")
+ .HasColumnType("real")
+ .HasColumnName("score");
+
+ b.Property("Time")
+ .HasColumnType("timestamp with time zone")
+ .HasColumnName("time");
+
+ b.HasKey("Id")
+ .HasName("PK_ipintel_cache");
+
+ b.ToTable("ipintel_cache", (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");
+
+ 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("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.PrimitiveCollection>("ConstructionFavorites")
+ .IsRequired()
+ .HasColumnType("text[]")
+ .HasColumnName("construction_favorites");
+
+ 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("OrganMarkings")
+ .HasColumnType("jsonb")
+ .HasColumnName("organ_markings");
+
+ b.Property("PreferenceId")
+ .HasColumnType("integer")
+ .HasColumnName("preference_id");
+
+ b.Property("PreferenceUnavailable")
+ .HasColumnType("integer")
+ .HasColumnName("pref_unavailable");
+
+ b.Property("Sex")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("sex");
+
+ b.Property("SkinColor")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("skin_color");
+
+ b.Property("Slot")
+ .HasColumnType("integer")
+ .HasColumnName("slot");
+
+ b.Property("SpawnPriority")
+ .HasColumnType("integer")
+ .HasColumnName("spawn_priority");
+
+ b.Property("Species")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("species");
+
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+
+ 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("EntityName")
+ .HasMaxLength(256)
+ .HasColumnType("character varying(256)")
+ .HasColumnName("entity_name");
+
+ 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.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.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.Unban", 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_unban");
+
+ b.HasIndex("BanId")
+ .IsUnique();
+
+ b.ToTable("unban", (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.Ban", b =>
+ {
+ b.HasOne("Content.Server.Database.Player", "CreatedBy")
+ .WithMany("AdminServerBansCreated")
+ .HasForeignKey("BanningAdmin")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_ban_player_banning_admin");
+
+ b.HasOne("Content.Server.Database.Player", "LastEditedBy")
+ .WithMany("AdminServerBansLastEdited")
+ .HasForeignKey("LastEditedById")
+ .HasPrincipalKey("UserId")
+ .OnDelete(DeleteBehavior.SetNull)
+ .HasConstraintName("FK_ban_player_last_edited_by_id");
+
+ b.Navigation("CreatedBy");
+
+ b.Navigation("LastEditedBy");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanAddress", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("Addresses")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_address_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanHwid", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("Hwids")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_hwid_ban_ban_id");
+
+ b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+ {
+ b1.Property("BanHwidId")
+ .HasColumnType("integer")
+ .HasColumnName("ban_hwid_id");
+
+ b1.Property("Hwid")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b1.Property("Type")
+ .HasColumnType("integer")
+ .HasColumnName("hwid_type");
+
+ b1.HasKey("BanHwidId");
+
+ b1.ToTable("ban_hwid");
+
+ b1.WithOwner()
+ .HasForeignKey("BanHwidId")
+ .HasConstraintName("FK_ban_hwid_ban_hwid_ban_hwid_id");
+ });
+
+ b.Navigation("Ban");
+
+ b.Navigation("HWId")
+ .IsRequired();
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanPlayer", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("Players")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_player_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRole", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("Roles")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_role_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ modelBuilder.Entity("Content.Server.Database.BanRound", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("Rounds")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_round_ban_ban_id");
+
+ b.HasOne("Content.Server.Database.Round", "Round")
+ .WithMany()
+ .HasForeignKey("RoundId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_ban_round_round_round_id");
+
+ b.Navigation("Ban");
+
+ b.Navigation("Round");
+ });
+
+ 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.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
+ {
+ b1.Property("ConnectionLogId")
+ .HasColumnType("integer")
+ .HasColumnName("connection_log_id");
+
+ b1.Property("Hwid")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("hwid");
+
+ b1.Property("Type")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("hwid_type");
+
+ b1.HasKey("ConnectionLogId");
+
+ b1.ToTable("connection_log");
+
+ b1.WithOwner()
+ .HasForeignKey("ConnectionLogId")
+ .HasConstraintName("FK_connection_log_connection_log_connection_log_id");
+ });
+
+ b.Navigation("HWId");
+
+ 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.Player", b =>
+ {
+ b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 =>
+ {
+ b1.Property("PlayerId")
+ .HasColumnType("integer")
+ .HasColumnName("player_id");
+
+ b1.Property("Hwid")
+ .IsRequired()
+ .HasColumnType("bytea")
+ .HasColumnName("last_seen_hwid");
+
+ b1.Property("Type")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("integer")
+ .HasDefaultValue(0)
+ .HasColumnName("last_seen_hwid_type");
+
+ b1.HasKey("PlayerId");
+
+ b1.ToTable("player");
+
+ b1.WithOwner()
+ .HasForeignKey("PlayerId")
+ .HasConstraintName("FK_player_player_player_id");
+ });
+
+ b.Navigation("LastSeenHWId");
+ });
+
+ 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.ServerBanHit", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithMany("BanHits")
+ .HasForeignKey("BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_server_ban_hit_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.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("Content.Server.Database.Unban", b =>
+ {
+ b.HasOne("Content.Server.Database.Ban", "Ban")
+ .WithOne("Unban")
+ .HasForeignKey("Content.Server.Database.Unban", "BanId")
+ .OnDelete(DeleteBehavior.Cascade)
+ .IsRequired()
+ .HasConstraintName("FK_unban_ban_ban_id");
+
+ b.Navigation("Ban");
+ });
+
+ 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.Ban", b =>
+ {
+ b.Navigation("Addresses");
+
+ b.Navigation("BanHits");
+
+ b.Navigation("Hwids");
+
+ b.Navigation("Players");
+
+ b.Navigation("Roles");
+
+ b.Navigation("Rounds");
+
+ b.Navigation("Unban");
+ });
+
+ 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("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");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.cs b/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.cs
new file mode 100644
index 00000000000..bde00feb8be
--- /dev/null
+++ b/Content.Server.Database/Migrations/Postgres/20260327081423_AddTtsVoice.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Postgres
+{
+ ///
+ public partial class AddTtsVoice : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "voice",
+ table: "profile",
+ type: "text",
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "voice",
+ table: "profile");
+ }
+ }
+}
diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
index 1c85aac946e..70d33dc8cf9 100644
--- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
+++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs
@@ -1125,6 +1125,11 @@ protected override void BuildModel(ModelBuilder modelBuilder)
.HasColumnType("text")
.HasColumnName("species");
+ b.Property("Voice")
+ .IsRequired()
+ .HasColumnType("text")
+ .HasColumnName("voice");
+
b.HasKey("Id")
.HasName("PK_profile");
diff --git a/Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.Designer.cs
new file mode 100644
index 00000000000..7f6587d7dfa
--- /dev/null
+++ b/Content.Server.Database/Migrations/Sqlite/20260327081359_AddTtsVoice.Designer.cs
@@ -0,0 +1,2053 @@
+//
+using System;
+using Content.Server.Database;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace Content.Server.Database.Migrations.Sqlite
+{
+ [DbContext(typeof(SqliteServerDbContext))]
+ [Migration("20260327081359_AddTtsVoice")]
+ partial class AddTtsVoice
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder.HasAnnotation("ProductVersion", "10.0.0");
+
+ modelBuilder.Entity("Content.Server.Database.Admin", b =>
+ {
+ b.Property("UserId")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("TEXT")
+ .HasColumnName("user_id");
+
+ b.Property("AdminRankId")
+ .HasColumnType("INTEGER")
+ .HasColumnName("admin_rank_id");
+
+ b.Property("Deadminned")
+ .HasColumnType("INTEGER")
+ .HasColumnName("deadminned");
+
+ b.Property