diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 8ae1fc9bcc2..9be1007af1b 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -78,6 +78,8 @@ public void UpdateSprite(Entity en // end Goobstation: port EE height/width sliders sprite[_sprite.LayerMapReserve((entity.Owner, sprite), HumanoidVisualLayers.Eyes)].Color = humanoidAppearance.EyeColor; + + RaiseLocalEvent(entity.Owner, new HumanoidAppearanceUpdatedEvent()); // Arcane } private static bool IsHidden(HumanoidAppearanceComponent humanoid, HumanoidVisualLayers layer) @@ -265,6 +267,7 @@ public override void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profil humanoid.Height = profile.Height; // Goobstation: port EE height/width sliders humanoid.Width = profile.Width; // Goobstation: port EE height/width sliders humanoid.CustomSpeciesName = profile.CustomSpeciesName; // Arcane + humanoid.EarsAboveHair = profile.Appearance.EarsAboveHair; // Arcane UpdateSprite((uid, humanoid, Comp(uid))); } diff --git a/Content.Client/Humanoid/MarkingPicker.xaml b/Content.Client/Humanoid/MarkingPicker.xaml index 2244e3acee3..486fc647209 100644 --- a/Content.Client/Humanoid/MarkingPicker.xaml +++ b/Content.Client/Humanoid/MarkingPicker.xaml @@ -35,6 +35,9 @@ SPDX-License-Identifier: MIT + + + diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs index 847f8d2d5a2..6733f502136 100644 --- a/Content.Client/Humanoid/MarkingPicker.xaml.cs +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -28,6 +28,7 @@ public sealed partial class MarkingPicker : Control public Action? OnMarkingRemoved; public Action? OnMarkingColorChange; public Action? OnMarkingRankChange; + public Action? OnEarsAboveHairChange; // Arcane private List _currentMarkingColors = new(); @@ -71,6 +72,26 @@ public string IgnoreCategories public bool Forced { get; set; } + // Arcane-Start + private bool _showEarsAboveHairOption; + + public bool ShowEarsAboveHairOption + { + get => _showEarsAboveHairOption; + set + { + _showEarsAboveHairOption = value; + UpdateEarsAboveHairVisibility(); + } + } + + public bool EarsAboveHair + { + get => CEarsAboveHair.Pressed; + set => CEarsAboveHair.Pressed = value; + } + // Arcane-End + private bool _ignoreSpecies; public bool IgnoreSpecies @@ -147,6 +168,18 @@ public MarkingPicker() CMarkingRankDown.OnPressed += _ => SwapMarkingDown(); CMarkingSearch.OnTextChanged += args => Populate(args.Text); + + // Arcane-Start + CEarsAboveHair.OnToggled += args => OnEarsAboveHairChange?.Invoke(args.Pressed); + UpdateEarsAboveHairVisibility(); + } + + private void UpdateEarsAboveHairVisibility() + { + CEarsAboveHair.Visible = _showEarsAboveHairOption && + (_selectedMarkingCategory == MarkingCategories.HeadTop || + _selectedMarkingCategory == MarkingCategories.HeadSide); + // Arcane-End } private void SetupCategoryButtons() @@ -180,6 +213,7 @@ private void SetupCategoryButtons() { _selectedMarkingCategory = MarkingCategories.Chest; } + UpdateEarsAboveHairVisibility(); // Arcane } private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}"); @@ -388,6 +422,7 @@ private void OnCategoryChange(OptionButton.ItemSelectedEventArgs category) Populate(CMarkingSearch.Text); PopulateUsed(); UpdatePoints(); + UpdateEarsAboveHairVisibility(); // Arcane } // TODO: This should be using ColorSelectorSliders once that's merged, so @@ -576,4 +611,4 @@ private void MarkingRemove() CMarkingColors.Visible = false; OnMarkingRemoved?.Invoke(_currentMarkings); } -} \ No newline at end of file +} diff --git a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs index 6147c672d84..2cb0c5f616d 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -722,6 +722,10 @@ public HumanoidProfileEditor( Markings.OnMarkingRemoved += OnMarkingChange; Markings.OnMarkingColorChange += OnMarkingChange; Markings.OnMarkingRankChange += OnMarkingChange; + // Arcane-Start + Markings.OnEarsAboveHairChange += OnEarsAboveHairChange; + Markings.ShowEarsAboveHairOption = true; + // Arcane-End #endregion Markings @@ -1931,6 +1935,17 @@ private void OnMarkingChange(MarkingSet markings) ReloadProfilePreview(); } + // Arcane-Start + private void OnEarsAboveHairChange(bool newValue) + { + if (Profile is null) + return; + + Profile = Profile.WithCharacterAppearance(Profile.Appearance.WithEarsAboveHair(newValue)); + ReloadProfilePreview(); + } + // Arcane-End + private void OnSkinColorOnValueChanged() { if (Profile is null) return; @@ -2329,6 +2344,7 @@ private void UpdateMarkings() Markings.SetData(Profile.Appearance.Markings, Profile.Species, Profile.Sex, Profile.Appearance.SkinColor, Profile.Appearance.EyeColor ); + Markings.EarsAboveHair = Profile.Appearance.EarsAboveHair; // Arcane } private void UpdateGenderControls() diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs new file mode 100644 index 00000000000..5901bbd40e3 --- /dev/null +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -0,0 +1,821 @@ +using System.Collections.Generic; +using Content.Client.Humanoid; +using Content.Client.Inventory; +using Content.Shared.Clothing; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Content.Shared.Humanoid.Prototypes; +using Content.Shared.Inventory; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Prototypes; +using Robust.Shared.Utility; +using static Robust.Client.GameObjects.SpriteComponent; + +namespace Content.Client._Arcane.DirectionalLayering; + +/// +/// Reorders a humanoid sprite's hair and neck layers based on whether the entity is facing the camera. +/// Species differ in their layer layouts (humans keep "mask" directly above the hair while arachnids keep a +/// whole face cluster between the hair and "HeadTop"), so the block boundaries are discovered from the sprite +/// instead of being hard-coded and the ordering is corrected against the camera rather than the world. +/// +public sealed class DirectionalLayeringSystem : EntitySystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly MarkingManager _markingManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + + private readonly Dictionary _cache = new(); + + private Angle _lastEyeRotation = Angle.Zero; + + private static readonly ProtoId HarpySpecies = "Harpy"; + + /// + /// Per-entity snapshot of the layers that make up the hair, neck and tail blocks, plus the per-entity markers + /// used when moving those blocks between the species' default and camera-facing layouts. + /// + private sealed class OrderingCache + { + public List HairKeys = new(); + public object? HairFrontAnchor; + public List CloakKeys = new(); + public List TailKeys = new(); + public object? TailAnchor; + public bool TailAnchorCaptured; + } + + private enum DirectionalView + { + Front, + Side, + Back, + } + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnAppearanceUpdated); + SubscribeLocalEvent(OnEquipmentVisualsUpdated); + SubscribeLocalEvent(OnRemove); + } + + public override void Shutdown() + { + _cache.Clear(); + base.Shutdown(); + } + + public override void FrameUpdate(float frameTime) + { + var eyeRotation = _eyeManager.CurrentEye.Rotation; + if (eyeRotation.EqualsApprox(_lastEyeRotation)) + return; + + _lastEyeRotation = eyeRotation; + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var humanoid, out var sprite, out _)) + { + ApplyOrdering((uid, humanoid, sprite)); + } + } + + private void OnMove(EntityUid uid, HumanoidAppearanceComponent component, ref MoveEvent args) + { + if (args.OldRotation.GetCardinalDir() == args.NewRotation.GetCardinalDir()) + return; + + if (TryComp(uid, out SpriteComponent? sprite)) + ApplyOrdering((uid, component, sprite)); + } + + private void OnStartup(EntityUid uid, HumanoidAppearanceComponent component, ComponentStartup args) + { + if (TryComp(uid, out SpriteComponent? sprite)) + ApplyOrdering((uid, component, sprite)); + } + + private void OnAppearanceUpdated(EntityUid uid, HumanoidAppearanceComponent component, HumanoidAppearanceUpdatedEvent args) + { + if (TryComp(uid, out SpriteComponent? sprite)) + ApplyOrdering((uid, component, sprite)); + } + + private void OnEquipmentVisualsUpdated(EquipmentVisualsUpdatedEvent args) + { + if (args.Slot != "neck" && args.Slot != "back" || + !TryComp(args.Equipee, out HumanoidAppearanceComponent? humanoid) || + !TryComp(args.Equipee, out SpriteComponent? sprite)) + { + return; + } + + ApplyOrdering((args.Equipee, humanoid, sprite)); + } + + private void OnRemove(EntityUid uid, HumanoidAppearanceComponent component, ComponentRemove args) + { + _cache.Remove(uid); + } + + private void ApplyOrdering(Entity ent) + { + if (!TryComp(ent.Owner, out TransformComponent? xform)) + return; + + // The renderer picks the RSI direction from `worldRotation + eyeRotation`, so the facing has to be + // determined relative to the camera, not the absolute world rotation. + var view = GetView(_transform.GetWorldRotation(ent.Owner) + _eyeManager.CurrentEye.Rotation); + var cache = GetCache(ent); + + // Tail and cloak first, so the back view's "hair between the head-trim cluster and the tail" layout gets + // the tail above the hair without the two blocks fighting over the same index. + EnsureTailAndCloakLayout(ent, view, cache); + EnsureHairLayout(ent, view == DirectionalView.Back, cache); + } + + private bool TryGetLayerIndex(Entity ent, object key, out int index) + { + var sprite = ent.Comp2; + index = 0; + return key switch + { + Enum enumKey => _sprite.LayerMapTryGet((ent.Owner, sprite), enumKey, out index, false), + string stringKey => _sprite.LayerMapTryGet((ent.Owner, sprite), stringKey, out index, false), + _ => false, + }; + } + + private void SetLayerIndex(Entity ent, object key, int index) + { + var sprite = ent.Comp2; + switch (key) + { + case Enum enumKey: + _sprite.LayerMapSet((ent.Owner, sprite), enumKey, index); + break; + case string stringKey: + _sprite.LayerMapSet((ent.Owner, sprite), stringKey, index); + break; + } + } + + /// + /// Gets or rebuilds the block snapshot for the entity. Memberships are re-derived whenever the marking or + /// neck layout differs from what is cached; anchors are invalidated together with the membership. + /// + private OrderingCache GetCache(Entity ent) + { + var hairKeys = UniqueKeys(GetHairBlockKeys(ent)); + var cloakKeys = UniqueKeys(GetCloakBlockKeys(ent)); + var tailKeys = UniqueKeys(GetTailBlockKeys(ent)); + + if (_cache.TryGetValue(ent.Owner, out var cache) && + SameKeys(cache.HairKeys, hairKeys) && + SameKeys(cache.CloakKeys, cloakKeys) && + SameKeys(cache.TailKeys, tailKeys)) + { + return cache; + } + + _cache.TryGetValue(ent.Owner, out var previous); + cache = new OrderingCache + { + HairKeys = hairKeys, + CloakKeys = cloakKeys, + TailKeys = tailKeys, + // The species-level anchor the tail rests above never changes with the markings, so keep it across + // membership rebuilds once it has been captured from a native (base) ordering. + TailAnchor = previous?.TailAnchor, + TailAnchorCaptured = previous?.TailAnchorCaptured ?? false, + }; + _cache[ent.Owner] = cache; + return cache; + } + + private static bool SameKeys(List a, List b) + { + if (a.Count != b.Count) + return false; + + foreach (var key in a) + { + var found = false; + foreach (var other in b) + { + if (Equals(key, other)) + { + found = true; + break; + } + } + + if (!found) + return false; + } + + return true; + } + + private static List UniqueKeys(List keys) + { + var unique = new List(keys.Count); + foreach (var key in keys) + { + var found = false; + foreach (var other in unique) + { + if (Equals(key, other)) + { + found = true; + break; + } + } + + if (!found) + unique.Add(key); + } + + return unique; + } + + private List GetHairBlockKeys(Entity ent) + { + var hairKeys = new List { HumanoidVisualLayers.Hair }; + + foreach (var markingList in ent.Comp1.MarkingSet.Markings.Values) + { + foreach (var marking in markingList) + { + if (!_markingManager.TryGetMarking(marking, out var prototype) || + prototype.MarkingCategory != MarkingCategories.Hair) + { + continue; + } + + foreach (var sprite in prototype.Sprites) + { + if (sprite is SpriteSpecifier.Rsi rsi) + hairKeys.Add($"{marking.MarkingId}-{rsi.RsiState}"); + } + } + } + + return hairKeys; + } + + private List GetCloakBlockKeys(Entity ent) + { + var cloakKeys = new List(); + if (TryComp(ent.Owner, out InventorySlotsComponent? slots) && + slots.VisualLayerKeys.TryGetValue("neck", out var neckKeys)) + { + cloakKeys.AddRange(neckKeys); + } + + return cloakKeys; + } + + /// + /// The tail/wings visual layers and their marking layers, restricted to keys the sprite actually has (the + /// "Wings" visual layer only exists for species that define it). + /// + private List GetTailBlockKeys(Entity ent) + { + var tailKeys = new List + { + HumanoidVisualLayers.Tail, + HumanoidVisualLayers.Wings, + }; + + foreach (var (category, markings) in ent.Comp1.MarkingSet.Markings) + { + if (category != MarkingCategories.Tail && category != MarkingCategories.Wings) + continue; + + foreach (var marking in markings) + { + if (!_markingManager.TryGetMarking(marking, out var prototype)) + continue; + + foreach (var sprite in prototype.Sprites) + { + if (sprite is SpriteSpecifier.Rsi rsi) + tailKeys.Add($"{marking.MarkingId}-{rsi.RsiState}"); + } + } + } + + for (var i = tailKeys.Count - 1; i >= 0; i--) + { + if (!TryGetLayerIndex(ent, tailKeys[i], out _)) + tailKeys.RemoveAt(i); + } + + return tailKeys; + } + + private void EnsureHairLayout( + Entity ent, + bool backView, + OrderingCache cache) + { + if (cache.HairKeys.Count == 0 || + !TryGetClusterTop(ent, out var clusterTop, out var clusterTopIdx) || + !TryGetBlockExtent(ent, cache.HairKeys, out var blockStart, out var blockEnd)) + { + return; + } + + if (backView) + { + // The back of the head goes over the ears/head-top cluster: hair lands directly above it. + if (blockStart == clusterTopIdx + 1) + return; + + if (!TryExtractBlock(ent, cache.HairKeys, out var block, out _)) + return; + + if (!TryGetLayerIndex(ent, clusterTop, out var anchorIdx)) + return; + + InsertBlock(ent, cache.HairKeys, block, anchorIdx + 1); + } + else + { + if (cache.HairFrontAnchor is not { } anchor) + { + // No anchor known and the hair is stuck above the whole cluster (e.g. markings changed during a + // back view). Reset it directly below the mask, or below the HeadTop base when no mask is worn, so + // the front anchor can be re-discovered on the next pass. + if (blockEnd > clusterTopIdx) + { + object resetAnchorKey = "mask"; + var hasResetAnchor = TryGetLayerIndex(ent, "mask", out var resetAnchorIdx); + if (!hasResetAnchor) + { + resetAnchorKey = HumanoidVisualLayers.HeadTop; + hasResetAnchor = TryGetLayerIndex(ent, HumanoidVisualLayers.HeadTop, out resetAnchorIdx); + } + + if (hasResetAnchor && + TryExtractBlock(ent, cache.HairKeys, out var resetBlock, out _) && + TryGetLayerIndex(ent, resetAnchorKey, out resetAnchorIdx)) + { + InsertBlock(ent, cache.HairKeys, resetBlock, resetAnchorIdx - resetBlock.Count); + } + + return; + } + + // First contact with the default front layout: remember which head-trim base layer sits right above + // the hair. HeadTop/HeadSide marking layers are excluded so the anchor can never become an ear + // marking interleaved with the hair block. + if (TryGetFrontAnchor(ent, blockEnd, out var frontAnchor)) + { + cache.HairFrontAnchor = frontAnchor; + } + + return; + } + + // Front face visible: hair below the marker that the species puts above it. + if (!TryGetLayerIndex(ent, anchor, out var anchorIdx)) + return; + + if (blockEnd == anchorIdx - 1) + return; + + if (!TryExtractBlock(ent, cache.HairKeys, out var block, out _)) + return; + + if (!TryGetLayerIndex(ent, anchor, out anchorIdx)) + return; + + InsertBlock(ent, cache.HairKeys, block, anchorIdx - block.Count); + } + } + + /// + /// Reorders the tail/wings block against the cloak (neck) block per view. From the back the tail is native: + /// it rests on its species anchor (usually the head-trim cluster top, or the head itself for species whose + /// base order puts the tail above the head) with the cloak tucked directly below it, so the tail hides the + /// cloak. From the front and the sides the cloak spreads over the shoulders directly below the head and the + /// tail is tucked under the cloak, so the tail only renders above the cloak when the back faces the camera. + /// + private void EnsureTailAndCloakLayout( + Entity ent, + DirectionalView view, + OrderingCache cache) + { + if (cache.CloakKeys.Count == 0 || + cache.TailKeys.Count == 0 || + !TryGetLayerIndex(ent, "head", out _) || + !TryGetBlockExtent(ent, cache.TailKeys, out var tailStart, out _) || + !TryGetBlockExtent(ent, cache.CloakKeys, out _, out var cloakTop)) + { + return; + } + + // The native base layout has the cloak below the tail, which is also the back view's layout. + var cloakBelowTail = cloakTop < tailStart; + if (!cache.TailAnchorCaptured && cloakBelowTail) + { + if (TryGetTailAnchor(ent, tailStart, out var anchor, out _)) + { + cache.TailAnchor = anchor; + cache.TailAnchorCaptured = true; + } + } + + var wantCloakBelowTail = view == DirectionalView.Back; + + if (ent.Comp1.Species == HarpySpecies) + { + // Harpies draw big back wings on the Tail layer above the head in their base order, so the cloak (neck) + // is already below them on every view; keep the native layout rather than burying the wings under it. + return; + } + + // A backpack (or any other back-slot item) renders above the cloak on every view, so even when the back view + // already has the cloak below the tail, fall through to lower the cloak below the "back" layer if it is up. + var cloakBelowBack = + !TryGetLayerIndex(ent, "back", out var backIdx) || + cloakTop < backIdx; + + if (cloakBelowTail == wantCloakBelowTail && (view != DirectionalView.Back || cloakBelowBack)) + return; + + var tailCount = cache.TailKeys.Count; + var cloakCount = cache.CloakKeys.Count; + + if (!TryExtractBlock(ent, cache.TailKeys, out var tailBlock, out _)) + return; + + if (!TryExtractBlock(ent, cache.CloakKeys, out var cloakBlock, out var cloakStart)) + { + // The cloak failed to come out; put the tail back where it was rather than dropping it from the sprite. + InsertBlock(ent, cache.TailKeys, tailBlock, tailStart); + return; + } + + if (wantCloakBelowTail) + { + // Back view: tail back on its native spot, directly above its anchor; cloak right below the tail, but + // never at or above the backpack ("back") layer. + if (cache.TailAnchorCaptured && TryGetLayerIndex(ent, cache.TailAnchor!, out var anchorIdx)) + { + var backTailTarget = anchorIdx + 1; + InsertBlock(ent, cache.TailKeys, tailBlock, backTailTarget); + var backCloakTarget = Math.Min(backTailTarget - cloakCount, GetCloakCeiling(ent)); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backCloakTarget); + return; + } + + // Anchor unknown (rare, e.g. the cache was rebuilt mid-front-view): fall back to the head-trim cluster. + if (TryGetClusterTop(ent, out _, out var clusterIdx)) + { + var backTailTarget = clusterIdx + 1; + InsertBlock(ent, cache.TailKeys, tailBlock, backTailTarget); + var backCloakTarget = Math.Min(backTailTarget - cloakCount, GetCloakCeiling(ent)); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backCloakTarget); + return; + } + + InsertBlock(ent, cache.TailKeys, tailBlock, tailStart); + InsertBlock(ent, cache.CloakKeys, cloakBlock, cloakStart); + return; + } + + // Front/side view: cloak spread below the head, tail tucked directly under the cloak; the cloak never goes + // at or above the backpack ("back") layer. + if (!TryGetLayerIndex(ent, "head", out var headIdx)) + { + InsertBlock(ent, cache.TailKeys, tailBlock, tailStart); + InsertBlock(ent, cache.CloakKeys, cloakBlock, cloakStart); + return; + } + + var cloakTarget = Math.Min(headIdx - cloakCount, GetCloakCeiling(ent)); + var tailTarget = Math.Max(0, cloakTarget - tailCount); + // Cloak goes in first: it targets the "back" bookmark's slot, and the tail sits below it. Inserting the tail + // first would shift the belt/outerClothing layers up onto the cloak's index, drawing them over it. + InsertBlock(ent, cache.CloakKeys, cloakBlock, cloakTarget); + InsertBlock(ent, cache.TailKeys, tailBlock, tailTarget); + } + + /// + /// The current top of the head-trim cluster: the highest of the "mask", "HeadSide" and "HeadTop" base layers + /// and any HeadTop/HeadSide marking layers (e.g. ears). The hair block is moved above this cluster when the + /// back of the head faces the camera, so the ears end up under the hair instead of floating over it. + /// + private bool TryGetClusterTop( + Entity ent, + out object key, + out int index) + { + key = HumanoidVisualLayers.HeadTop; + index = 0; + var found = false; + + foreach (var candidate in GetHeadTrimClusterKeys(ent)) + { + if (!TryGetLayerIndex(ent, candidate, out var candidateIdx)) + continue; + + if (!found || candidateIdx > index) + { + found = true; + key = candidate; + index = candidateIdx; + } + } + + return found; + } + + /// + /// The base and marking layers that form the head-trim cluster above the hair: "mask", "HeadSide", "HeadTop" + /// and their marking layers. When a character opts in to ears-above-hair, the ear marking layers are excluded + /// from the cluster so the back-view hair placement lands below them instead of covering them. + /// + private List GetHeadTrimClusterKeys(Entity ent) + { + var candidates = new List + { + "mask", + HumanoidVisualLayers.HeadSide, + HumanoidVisualLayers.HeadTop, + }; + + if (ent.Comp1.EarsAboveHair) + return candidates; + + foreach (var (category, markings) in ent.Comp1.MarkingSet.Markings) + { + if (category != MarkingCategories.HeadTop && category != MarkingCategories.HeadSide) + continue; + + foreach (var marking in markings) + { + if (!_markingManager.TryGetMarking(marking, out var prototype)) + continue; + + foreach (var sprite in prototype.Sprites) + { + if (sprite is SpriteSpecifier.Rsi rsi) + candidates.Add($"{marking.MarkingId}-{rsi.RsiState}"); + } + } + } + + return candidates; + } + + /// + /// The nearest of the "mask", "HeadSide" and "HeadTop" base layers above the given index. Marking layers are + /// excluded so the front hair anchor can never fall on an ear marking that sits inside the hair's z-range. + /// + private bool TryGetFrontAnchor( + Entity ent, + int above, + out object anchor) + { + anchor = null!; + var best = int.MaxValue; + object bestKey = null!; + + if (TryGetLayerIndex(ent, HumanoidVisualLayers.HeadSide, out var index) && + index > above && index < best) + { + best = index; + bestKey = HumanoidVisualLayers.HeadSide; + } + + if (TryGetLayerIndex(ent, HumanoidVisualLayers.HeadTop, out index) && + index > above && index < best) + { + best = index; + bestKey = HumanoidVisualLayers.HeadTop; + } + + if (TryGetLayerIndex(ent, "mask", out index) && + index > above && index < best) + { + best = index; + bestKey = "mask"; + } + + if (best == int.MaxValue) + return false; + + anchor = bestKey; + return true; + } + + /// + /// The species-level layers a tail/wings block can rest directly above in the native base ordering. Species + /// differ: most put the tail just above the head-trim cluster, harpies put it above the head itself. + /// + private static readonly object[] TailAnchorCandidates = + { + "head", + HumanoidVisualLayers.HeadTop, + HumanoidVisualLayers.HeadSide, + "maskalt", + "mask", + HumanoidVisualLayers.FacialHair, + HumanoidVisualLayers.SnoutCover, + "neck", + HumanoidVisualLayers.TailOversuit, + "back", + "belt", + "outerClothing", + HumanoidVisualLayers.Eyes, + "ears", + HumanoidVisualLayers.Snout, + "id", + }; + + /// + /// The candidate layer with the highest index strictly below : the layer the current + /// tail block rests above. + /// + private bool TryGetTailAnchor( + Entity ent, + int below, + out object key, + out int index) + { + key = null!; + index = int.MinValue; + var found = false; + + foreach (var candidate in TailAnchorCandidates) + { + if (!TryGetLayerIndex(ent, candidate, out var candidateIdx) || candidateIdx >= below) + continue; + + if (!found || candidateIdx > index) + { + found = true; + key = candidate; + index = candidateIdx; + } + } + + return found; + } + + /// + /// The index at which the cloak block may start so it lands directly below the backpack ("back") layer: + /// inserting there pushes the backpack (and everything above it) up, keeping it above the cloak while the + /// cloak stays above whatever sits below the backpack (belt, outer clothing). Only applied while a backpack + /// is actually worn; the static "back" bookmark exists on every humanoid even when empty. Returns the current + /// layer count otherwise. + /// + private int GetCloakCeiling(Entity ent) + { + // The backpack always renders above the cloak: the cloak block is inserted at the "back" layer's index. + // A backIdx - cloakCount target would land on the belt's slot, shifting the belt above the cloak. + if (TryComp(ent.Owner, out InventorySlotsComponent? slots) && + slots.VisualLayerKeys.TryGetValue("back", out var backKeys) && + backKeys.Count > 0 && + TryGetLayerIndex(ent, "back", out var backIdx)) + { + return Math.Max(0, backIdx); + } + + return int.MaxValue; + } + + private bool TryGetBlockExtent( + Entity ent, + List keys, + out int start, + out int end) + { + start = int.MaxValue; + end = int.MinValue; + + foreach (var key in keys) + { + if (!TryGetLayerIndex(ent, key, out var index)) + return false; + + start = Math.Min(start, index); + end = Math.Max(end, index); + } + + return true; + } + + /// + /// Captures the block's layers out of the sprite, keeping their relative order, and removes them. + /// + private bool TryExtractBlock( + Entity ent, + List keys, + out List<(object Key, Layer Layer)> block, + out int start) + { + var sprite = ent.Comp2; + block = new List<(object, Layer)>(keys.Count); + var indices = new List<(object Key, int Index)>(keys.Count); + start = int.MaxValue; + + // Duplicate keys (e.g. the same marking listed twice) resolve to the same layer index. Resolve each + // index once, otherwise the second removal would delete the layer directly above the block (a clothing + // layer, say) instead of a duplicate, and wipe its key from the layer map. + foreach (var key in keys) + { + if (!TryGetLayerIndex(ent, key, out var index)) + return false; + + var duplicate = false; + foreach (var existing in indices) + { + if (existing.Index == index) + { + duplicate = true; + break; + } + } + + if (duplicate) + continue; + + start = Math.Min(start, index); + indices.Add((key, index)); + } + + // Remove from the top-most layer down so the captured indices stay valid while removing. + indices.Sort((a, b) => b.Index.CompareTo(a.Index)); + for (var i = 0; i < indices.Count; i++) + { + if (!_sprite.RemoveLayer((ent.Owner, sprite), indices[i].Index, out var layer, false)) + { + for (var j = block.Count - 1; j >= 0; j--) + { + _sprite.AddLayer((ent.Owner, sprite), block[j].Layer, indices[j].Index); + SetLayerIndex(ent, block[j].Key, indices[j].Index); + } + + block.Clear(); + return false; + } + + block.Add((indices[i].Key, layer!)); + } + + // block was built from top to bottom; restore the on-screen draw order. + block.Reverse(); + return true; + } + + /// + /// Inserts the previously extracted block so it occupies onward, and re-registers + /// every layer map key that belongs to it. + /// + private void InsertBlock( + Entity ent, + List keys, + List<(object Key, Layer Layer)> block, + int targetStart) + { + var sprite = ent.Comp2; + + for (var i = 0; i < block.Count; i++) + { + _sprite.AddLayer((ent.Owner, sprite), block[i].Layer, targetStart + i); + SetLayerIndex(ent, block[i].Key, targetStart + i); + } + } + + /// + /// Mirrors for 4-directional layers (what humanoid body parts use), including + /// the anti-flicker direction bias, so this system's ordering matches the sprite states that actually render. + /// + private static DirectionalView GetView(Angle angle) + { + var ang = angle.Reduced().FlipPositive().Theta; + var mod = (Math.Floor(ang / MathHelper.PiOver2) % 2) - 0.5; + var modTheta = ang + mod * DirectionBias; + var quadrant = (int) Math.Round(modTheta / MathHelper.PiOver2) % 4; + + return quadrant switch + { + 0 => DirectionalView.Front, + 2 => DirectionalView.Back, + _ => DirectionalView.Side, + }; + } +} diff --git a/Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs b/Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs new file mode 100644 index 00000000000..b0e554e1f52 --- /dev/null +++ b/Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs @@ -0,0 +1,13 @@ +using Robust.Shared.GameObjects; + +namespace Content.Client.Humanoid; + +/// +/// Raised (broadcast) after a humanoid's sprite layers have been rebuilt by +/// . +/// Systems that reorder sprite layers against the camera (e.g. directional layering) react to this instead of +/// guessing when the appearance changed. +/// +public sealed class HumanoidAppearanceUpdatedEvent : EntityEventArgs +{ +} diff --git a/Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.Designer.cs b/Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.Designer.cs new file mode 100644 index 00000000000..481a70717f4 --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.Designer.cs @@ -0,0 +1,2915 @@ +// +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("20260915201057_EarsAboveHair")] + partial class EarsAboveHair + { + /// + 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.DBJobAlternateTitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("dbjob_alternate_title_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AlternateTitle") + .IsRequired() + .HasColumnType("text") + .HasColumnName("alternate_title"); + + b.Property("ProfileId") + .HasColumnType("integer") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_dbjob_alternate_title"); + + b.HasIndex("ProfileId", "RoleName") + .IsUnique(); + + b.ToTable("dbjob_alternate_title", (string)null); + }); + + 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("LastRolledAntag") + .HasColumnType("interval") + .HasColumnName("last_rolled_antag"); + + 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("ServerCurrency") + .HasColumnType("integer") + .HasColumnName("server_currency"); + + 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.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("polls_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Active") + .HasColumnType("boolean") + .HasColumnName("active"); + + b.Property("AllowMultipleChoices") + .HasColumnType("boolean") + .HasColumnName("allow_multiple_choices"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("uuid") + .HasColumnName("created_by_id"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("end_time"); + + b.Property("StartTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("start_time"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_polls"); + + b.HasIndex("Active") + .HasDatabaseName("IX_polls_active"); + + b.HasIndex("CreatedById") + .HasDatabaseName("IX_polls_created_by_id"); + + b.HasIndex("EndTime") + .HasDatabaseName("IX_polls_end_time"); + + b.HasIndex("StartTime") + .HasDatabaseName("IX_polls_start_time"); + + b.ToTable("polls", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("poll_options_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DisplayOrder") + .HasColumnType("integer") + .HasColumnName("display_order"); + + b.Property("OptionText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("option_text"); + + b.Property("PollId") + .HasColumnType("integer") + .HasColumnName("poll_id"); + + b.HasKey("Id") + .HasName("PK_poll_options"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_options_poll_id"); + + b.ToTable("poll_options", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollSeen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("poll_seen_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PollId") + .HasColumnType("integer") + .HasColumnName("poll_id"); + + b.Property("SeenAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("seen_at"); + + b.HasKey("Id") + .HasName("PK_poll_seen"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_poll_seen_player_user_id"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_seen_poll_id"); + + b.HasIndex("PollId", "PlayerUserId") + .IsUnique(); + + b.ToTable("poll_seen", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("poll_votes_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("PlayerUserId") + .HasColumnType("uuid") + .HasColumnName("player_user_id"); + + b.Property("PollId") + .HasColumnType("integer") + .HasColumnName("poll_id"); + + b.Property("PollOptionId") + .HasColumnType("integer") + .HasColumnName("poll_option_id"); + + b.Property("VotedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("voted_at"); + + b.HasKey("Id") + .HasName("PK_poll_votes"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_poll_votes_player_user_id"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_votes_poll_id"); + + b.HasIndex("PollOptionId") + .HasDatabaseName("IX_poll_votes_poll_option_id"); + + b.HasIndex("PollId", "PlayerUserId", "PollOptionId") + .IsUnique(); + + b.ToTable("poll_votes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("preference_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection>("ConstructionFavorites") + .IsRequired() + .HasColumnType("text[]") + .HasColumnName("construction_favorites"); + + b.Property("GhostId") + .IsRequired() + .HasColumnType("text") + .HasColumnName("ghost_id"); + + 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("BarkVoice") + .IsRequired() + .HasColumnType("text") + .HasColumnName("bark_voice"); + + b.Property("CharacterFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("character_flavor_text"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("char_name"); + + b.Property("CustomSpeciesName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("custom_species_name"); + + b.Property("EarsAboveHair") + .HasColumnType("boolean") + .HasColumnName("ears_above_hair"); + + b.Property("ErpPreference") + .HasColumnType("integer") + .HasColumnName("erp_preference"); + + 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("GreenFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("green_flavor_text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("hair_name"); + + b.Property("Height") + .HasColumnType("real") + .HasColumnName("height"); + + b.Property("LinksFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("links_flavor_text"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("NSFWFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nsfwflavor_text"); + + b.Property("NSFWLinksFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nsfwlinks_flavor_text"); + + b.Property("NSFWOOCFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nsfwoocflavor_text"); + + b.Property("NSFWTagsFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("nsfwtags_flavor_text"); + + b.Property("OOCFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("oocflavor_text"); + + b.Property("PreferenceId") + .HasColumnType("integer") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("integer") + .HasColumnName("pref_unavailable"); + + b.Property("RedFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("red_flavor_text"); + + 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("TagsFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("tags_flavor_text"); + + b.Property("Voice") + .IsRequired() + .HasColumnType("text") + .HasColumnName("voice"); + + b.Property("Width") + .HasColumnType("real") + .HasColumnName("width"); + + b.Property("YellowFlavorText") + .IsRequired() + .HasColumnType("text") + .HasColumnName("yellow_flavor_text"); + + 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.RMCDiscordAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("rmc_discord_accounts_id"); + + b.Property("HasPlayerRole") + .HasColumnType("boolean") + .HasColumnName("has_player_role"); + + b.Property("PlayerRoleUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("player_role_updated_at"); + + b.Property("RolesUpdatedAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("roles_updated_at"); + + b.HasKey("Id") + .HasName("PK_rmc_discord_accounts"); + + b.ToTable("rmc_discord_accounts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCDiscordAccountRole", b => + { + b.Property("DiscordId") + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("role_id"); + + b.HasKey("DiscordId", "RoleId") + .HasName("PK_rmc_discord_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("IX_rmc_discord_account_roles_role_id"); + + b.ToTable("rmc_discord_account_roles", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b => + { + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("DiscordId") + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_linked_accounts"); + + b.HasIndex("DiscordId") + .IsUnique(); + + b.ToTable("rmc_linked_accounts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("rmc_linked_accounts_logs_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("At") + .HasColumnType("timestamp with time zone") + .HasColumnName("at"); + + b.Property("DiscordId") + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_id"); + + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.HasKey("Id") + .HasName("PK_rmc_linked_accounts_logs"); + + b.HasIndex("At") + .HasDatabaseName("IX_rmc_linked_accounts_logs_at"); + + b.HasIndex("DiscordId") + .HasDatabaseName("IX_rmc_linked_accounts_logs_discord_id"); + + b.HasIndex("PlayerId") + .HasDatabaseName("IX_rmc_linked_accounts_logs_player_id"); + + b.ToTable("rmc_linked_accounts_logs", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b => + { + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("Code") + .HasColumnType("uuid") + .HasColumnName("code"); + + b.Property("CreationTime") + .HasColumnType("timestamp with time zone") + .HasColumnName("creation_time"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_linking_codes"); + + b.HasIndex("Code") + .HasDatabaseName("IX_rmc_linking_codes_code"); + + b.ToTable("rmc_linking_codes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.Property("PlayerId") + .HasColumnType("uuid") + .HasColumnName("player_id"); + + b.Property("GhostColor") + .HasColumnType("integer") + .HasColumnName("ghost_color"); + + b.Property("GhostHat") + .HasColumnType("text") + .HasColumnName("ghost_hat"); + + b.Property("GhostMask") + .HasColumnType("text") + .HasColumnName("ghost_mask"); + + b.Property("GhostParticles") + .HasColumnType("text") + .HasColumnName("ghost_particles"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_patrons"); + + b.ToTable("rmc_patrons", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b => + { + b.Property("PatronId") + .HasColumnType("uuid") + .HasColumnName("patron_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)") + .HasColumnName("message"); + + b.HasKey("PatronId") + .HasName("PK_rmc_patron_lobby_messages"); + + b.ToTable("rmc_patron_lobby_messages", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b => + { + b.Property("PatronId") + .HasColumnType("uuid") + .HasColumnName("patron_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("name"); + + b.HasKey("PatronId") + .HasName("PK_rmc_patron_round_end_nt_shoutouts"); + + b.ToTable("rmc_patron_round_end_nt_shoutouts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("rmc_patron_tiers_id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DiscordRole") + .HasColumnType("numeric(20,0)") + .HasColumnName("discord_role"); + + b.Property("GhostColor") + .HasColumnType("boolean") + .HasColumnName("ghost_color"); + + b.Property("GhostCosmetics") + .HasColumnType("boolean") + .HasColumnName("ghost_cosmetics"); + + b.Property("GhostParticles") + .HasColumnType("boolean") + .HasColumnName("ghost_particles"); + + b.Property("Icon") + .HasColumnType("text") + .HasColumnName("icon"); + + b.Property("LobbyMessage") + .HasColumnType("boolean") + .HasColumnName("lobby_message"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Priority") + .HasColumnType("integer") + .HasColumnName("priority"); + + b.Property("RoundEndShoutout") + .HasColumnType("boolean") + .HasColumnName("round_end_shoutout"); + + b.Property("ShowOnCredits") + .HasColumnType("boolean") + .HasColumnName("show_on_credits"); + + b.HasKey("Id") + .HasName("PK_rmc_patron_tiers"); + + b.HasIndex("DiscordRole") + .IsUnique(); + + b.HasIndex("LobbyMessage") + .HasDatabaseName("IX_rmc_patron_tiers_lobby_message"); + + b.HasIndex("RoundEndShoutout") + .HasDatabaseName("IX_rmc_patron_tiers_round_end_shoutout"); + + b.ToTable("rmc_patron_tiers", (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.DBJobAlternateTitle", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("AltTitles") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_dbjob_alternate_title_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.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.Poll", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_polls_player_created_by_id"); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany("Options") + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_options_polls_poll_id"); + + b.Navigation("Poll"); + }); + + modelBuilder.Entity("Content.Server.Database.PollSeen", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany() + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_seen_player_player_user_id"); + + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany() + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_seen_polls_poll_id"); + + b.Navigation("Player"); + + b.Navigation("Poll"); + }); + + modelBuilder.Entity("Content.Server.Database.PollVote", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany() + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany("Votes") + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_polls_poll_id"); + + b.HasOne("Content.Server.Database.PollOption", "PollOption") + .WithMany("Votes") + .HasForeignKey("PollOptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_poll_options_poll_option_id"); + + b.Navigation("Player"); + + b.Navigation("Poll"); + + b.Navigation("PollOption"); + }); + + 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.RMCDiscordAccountRole", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithMany("Roles") + .HasForeignKey("DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_discord_account_roles_rmc_discord_accounts_discord_id"); + + b.Navigation("Discord"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithOne("LinkedAccount") + .HasForeignKey("Content.Server.Database.RMCLinkedAccount", "DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_rmc_discord_accounts_discord_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("LinkedAccount") + .HasForeignKey("Content.Server.Database.RMCLinkedAccount", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_player_player_id"); + + b.Navigation("Discord"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithMany("LinkedAccountLogs") + .HasForeignKey("DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("LinkedAccountLogs") + .HasForeignKey("PlayerId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_logs_player_player_id1"); + + b.Navigation("Discord"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("LinkingCodes") + .HasForeignKey("Content.Server.Database.RMCLinkingCodes", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linking_codes_player_player_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("Patron") + .HasForeignKey("Content.Server.Database.RMCPatron", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patrons_player_player_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b => + { + b.HasOne("Content.Server.Database.RMCPatron", "Patron") + .WithOne("LobbyMessage") + .HasForeignKey("Content.Server.Database.RMCPatronLobbyMessage", "PatronId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patron_lobby_messages_rmc_patrons_patron_id"); + + b.Navigation("Patron"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b => + { + b.HasOne("Content.Server.Database.RMCPatron", "Patron") + .WithOne("RoundEndNTShoutout") + .HasForeignKey("Content.Server.Database.RMCPatronRoundEndNTShoutout", "PatronId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id"); + + b.Navigation("Patron"); + }); + + 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"); + + b.Navigation("LinkedAccount"); + + b.Navigation("LinkedAccountLogs"); + + b.Navigation("LinkingCodes"); + + b.Navigation("Patron"); + }); + + modelBuilder.Entity("Content.Server.Database.Poll", b => + { + b.Navigation("Options"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.Navigation("Votes"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("AltTitles"); + + 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.RMCDiscordAccount", b => + { + b.Navigation("LinkedAccount") + .IsRequired(); + + b.Navigation("LinkedAccountLogs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.Navigation("LobbyMessage"); + + b.Navigation("RoundEndNTShoutout"); + }); + + 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/20260915201057_EarsAboveHair.cs b/Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.cs new file mode 100644 index 00000000000..d1472f917ec --- /dev/null +++ b/Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Postgres +{ + /// + public partial class EarsAboveHair : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ears_above_hair", + table: "profile", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ears_above_hair", + table: "profile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs index a6bc2643234..514e202f8e4 100644 --- a/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Postgres/PostgresServerDbContextModelSnapshot.cs @@ -1285,6 +1285,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("text") .HasColumnName("custom_species_name"); + b.Property("EarsAboveHair") + .HasColumnType("boolean") + .HasColumnName("ears_above_hair"); + b.Property("ErpPreference") .HasColumnType("integer") .HasColumnName("erp_preference"); diff --git a/Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.Designer.cs b/Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.Designer.cs new file mode 100644 index 00000000000..07bd0f9b58a --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.Designer.cs @@ -0,0 +1,2820 @@ +// +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("20260915201119_EarsAboveHair")] + partial class EarsAboveHair + { + /// + 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("Suspended") + .HasColumnType("INTEGER") + .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"); + + b.Property("AdminId") + .HasColumnType("TEXT") + .HasColumnName("admin_id"); + + b.Property("Flag") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("flag"); + + b.Property("Negative") + .HasColumnType("INTEGER") + .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("TEXT") + .HasColumnName("date"); + + b.Property("Impact") + .HasColumnType("INTEGER") + .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("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("TEXT") + .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"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("Dismissed") + .HasColumnType("INTEGER") + .HasColumnName("dismissed"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Seen") + .HasColumnType("INTEGER") + .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"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("RoundId") + .HasColumnType("INTEGER") + .HasColumnName("round_id"); + + b.Property("Secret") + .HasColumnType("INTEGER") + .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"); + + 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"); + + 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"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Deleted") + .HasColumnType("INTEGER") + .HasColumnName("deleted"); + + b.Property("DeletedAt") + .HasColumnType("TEXT") + .HasColumnName("deleted_at"); + + b.Property("DeletedById") + .HasColumnType("TEXT") + .HasColumnName("deleted_by_id"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(4096) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .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"); + + 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"); + + b.Property("UserId") + .HasColumnType("TEXT") + .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"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("BanTime") + .HasColumnType("TEXT") + .HasColumnName("ban_time"); + + b.Property("BanningAdmin") + .HasColumnType("TEXT") + .HasColumnName("banning_admin"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("ExpirationTime") + .HasColumnType("TEXT") + .HasColumnName("expiration_time"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("LastEditedAt") + .HasColumnType("TEXT") + .HasColumnName("last_edited_at"); + + b.Property("LastEditedById") + .HasColumnType("TEXT") + .HasColumnName("last_edited_by_id"); + + b.Property("PlaytimeAtNote") + .HasColumnType("TEXT") + .HasColumnName("playtime_at_note"); + + b.Property("Reason") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("reason"); + + b.Property("Severity") + .HasColumnType("INTEGER") + .HasColumnName("severity"); + + b.Property("Type") + .HasColumnType("INTEGER") + .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"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .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", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.BanHwid", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ban_hwid_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"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UserId") + .HasColumnType("TEXT") + .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"); + + 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"); + + 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"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER") + .HasColumnName("auto_delete"); + + b.Property("ExemptFlags") + .HasColumnType("INTEGER") + .HasColumnName("exempt_flags"); + + b.Property("Hidden") + .HasColumnType("INTEGER") + .HasColumnName("hidden"); + + b.Property("Length") + .HasColumnType("TEXT") + .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("TEXT") + .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"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Denied") + .HasColumnType("INTEGER") + .HasColumnName("denied"); + + b.Property("ServerId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0) + .HasColumnName("server_id"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.Property("Trust") + .HasColumnType("REAL") + .HasColumnName("trust"); + + b.Property("UserId") + .HasColumnType("TEXT") + .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", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.DBJobAlternateTitle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("dbjob_alternate_title_id"); + + b.Property("AlternateTitle") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("alternate_title"); + + b.Property("ProfileId") + .HasColumnType("INTEGER") + .HasColumnName("profile_id"); + + b.Property("RoleName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("role_name"); + + b.HasKey("Id") + .HasName("PK_dbjob_alternate_title"); + + b.HasIndex("ProfileId", "RoleName") + .IsUnique(); + + b.ToTable("dbjob_alternate_title", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.IPIntelCache", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("ipintel_cache_id"); + + b.Property("Address") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("address"); + + b.Property("Score") + .HasColumnType("REAL") + .HasColumnName("score"); + + b.Property("Time") + .HasColumnType("TEXT") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("PK_ipintel_cache"); + + b.HasIndex("Address") + .IsUnique(); + + b.ToTable("ipintel_cache", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("job_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"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("TimeSpent") + .HasColumnType("TEXT") + .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"); + + b.Property("FirstSeenTime") + .HasColumnType("TEXT") + .HasColumnName("first_seen_time"); + + b.Property("LastReadRules") + .HasColumnType("TEXT") + .HasColumnName("last_read_rules"); + + b.Property("LastRolledAntag") + .HasColumnType("TEXT") + .HasColumnName("last_rolled_antag"); + + b.Property("LastSeenAddress") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_address"); + + b.Property("LastSeenTime") + .HasColumnType("TEXT") + .HasColumnName("last_seen_time"); + + b.Property("LastSeenUserName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("last_seen_user_name"); + + b.Property("ServerCurrency") + .HasColumnType("INTEGER") + .HasColumnName("server_currency"); + + b.Property("UserId") + .HasColumnType("TEXT") + .HasColumnName("user_id"); + + b.HasKey("Id") + .HasName("PK_player"); + + b.HasAlternateKey("UserId") + .HasName("ak_player_user_id"); + + b.HasIndex("LastSeenUserName"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("player", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("polls_id"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasColumnName("active"); + + b.Property("AllowMultipleChoices") + .HasColumnType("INTEGER") + .HasColumnName("allow_multiple_choices"); + + b.Property("CreatedAt") + .HasColumnType("TEXT") + .HasColumnName("created_at"); + + b.Property("CreatedById") + .HasColumnType("TEXT") + .HasColumnName("created_by_id"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("TEXT") + .HasColumnName("description"); + + b.Property("EndTime") + .HasColumnType("TEXT") + .HasColumnName("end_time"); + + b.Property("StartTime") + .HasColumnType("TEXT") + .HasColumnName("start_time"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT") + .HasColumnName("title"); + + b.HasKey("Id") + .HasName("PK_polls"); + + b.HasIndex("Active") + .HasDatabaseName("IX_polls_active"); + + b.HasIndex("CreatedById") + .HasDatabaseName("IX_polls_created_by_id"); + + b.HasIndex("EndTime") + .HasDatabaseName("IX_polls_end_time"); + + b.HasIndex("StartTime") + .HasDatabaseName("IX_polls_start_time"); + + b.ToTable("polls", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("poll_options_id"); + + b.Property("DisplayOrder") + .HasColumnType("INTEGER") + .HasColumnName("display_order"); + + b.Property("OptionText") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("option_text"); + + b.Property("PollId") + .HasColumnType("INTEGER") + .HasColumnName("poll_id"); + + b.HasKey("Id") + .HasName("PK_poll_options"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_options_poll_id"); + + b.ToTable("poll_options", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollSeen", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("poll_seen_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PollId") + .HasColumnType("INTEGER") + .HasColumnName("poll_id"); + + b.Property("SeenAt") + .HasColumnType("TEXT") + .HasColumnName("seen_at"); + + b.HasKey("Id") + .HasName("PK_poll_seen"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_poll_seen_player_user_id"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_seen_poll_id"); + + b.HasIndex("PollId", "PlayerUserId") + .IsUnique(); + + b.ToTable("poll_seen", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("poll_votes_id"); + + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .HasColumnName("player_user_id"); + + b.Property("PollId") + .HasColumnType("INTEGER") + .HasColumnName("poll_id"); + + b.Property("PollOptionId") + .HasColumnType("INTEGER") + .HasColumnName("poll_option_id"); + + b.Property("VotedAt") + .HasColumnType("TEXT") + .HasColumnName("voted_at"); + + b.HasKey("Id") + .HasName("PK_poll_votes"); + + b.HasIndex("PlayerUserId") + .HasDatabaseName("IX_poll_votes_player_user_id"); + + b.HasIndex("PollId") + .HasDatabaseName("IX_poll_votes_poll_id"); + + b.HasIndex("PollOptionId") + .HasDatabaseName("IX_poll_votes_poll_option_id"); + + b.HasIndex("PollId", "PlayerUserId", "PollOptionId") + .IsUnique(); + + b.ToTable("poll_votes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("AdminOOCColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("admin_ooc_color"); + + b.PrimitiveCollection("ConstructionFavorites") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("construction_favorites"); + + b.Property("GhostId") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("ghost_id"); + + b.Property("SelectedCharacterSlot") + .HasColumnType("INTEGER") + .HasColumnName("selected_character_slot"); + + b.Property("UserId") + .HasColumnType("TEXT") + .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"); + + b.Property("Age") + .HasColumnType("INTEGER") + .HasColumnName("age"); + + b.Property("BarkVoice") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("bark_voice"); + + b.Property("CharacterFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("character_flavor_text"); + + b.Property("CharacterName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("char_name"); + + b.Property("CustomSpeciesName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("custom_species_name"); + + b.Property("EarsAboveHair") + .HasColumnType("INTEGER") + .HasColumnName("ears_above_hair"); + + b.Property("ErpPreference") + .HasColumnType("INTEGER") + .HasColumnName("erp_preference"); + + 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("GreenFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("green_flavor_text"); + + b.Property("HairColor") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_color"); + + b.Property("HairName") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("hair_name"); + + b.Property("Height") + .HasColumnType("REAL") + .HasColumnName("height"); + + b.Property("LinksFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("links_flavor_text"); + + b.Property("Markings") + .HasColumnType("jsonb") + .HasColumnName("markings"); + + b.Property("NSFWFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("nsfwflavor_text"); + + b.Property("NSFWLinksFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("nsfwlinks_flavor_text"); + + b.Property("NSFWOOCFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("nsfwoocflavor_text"); + + b.Property("NSFWTagsFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("nsfwtags_flavor_text"); + + b.Property("OOCFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("oocflavor_text"); + + b.Property("PreferenceId") + .HasColumnType("INTEGER") + .HasColumnName("preference_id"); + + b.Property("PreferenceUnavailable") + .HasColumnType("INTEGER") + .HasColumnName("pref_unavailable"); + + b.Property("RedFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("red_flavor_text"); + + 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("TagsFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("tags_flavor_text"); + + b.Property("Voice") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("voice"); + + b.Property("Width") + .HasColumnType("REAL") + .HasColumnName("width"); + + b.Property("YellowFlavorText") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("yellow_flavor_text"); + + 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"); + + 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"); + + 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"); + + b.Property("EntityName") + .HasMaxLength(256) + .HasColumnType("TEXT") + .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.RMCDiscordAccount", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("rmc_discord_accounts_id"); + + b.Property("HasPlayerRole") + .HasColumnType("INTEGER") + .HasColumnName("has_player_role"); + + b.Property("PlayerRoleUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("player_role_updated_at"); + + b.Property("RolesUpdatedAt") + .HasColumnType("TEXT") + .HasColumnName("roles_updated_at"); + + b.HasKey("Id") + .HasName("PK_rmc_discord_accounts"); + + b.ToTable("rmc_discord_accounts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCDiscordAccountRole", b => + { + b.Property("DiscordId") + .HasColumnType("INTEGER") + .HasColumnName("discord_id"); + + b.Property("RoleId") + .HasColumnType("INTEGER") + .HasColumnName("role_id"); + + b.HasKey("DiscordId", "RoleId") + .HasName("PK_rmc_discord_account_roles"); + + b.HasIndex("RoleId") + .HasDatabaseName("IX_rmc_discord_account_roles_role_id"); + + b.ToTable("rmc_discord_account_roles", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b => + { + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("DiscordId") + .HasColumnType("INTEGER") + .HasColumnName("discord_id"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_linked_accounts"); + + b.HasIndex("DiscordId") + .IsUnique(); + + b.ToTable("rmc_linked_accounts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("rmc_linked_accounts_logs_id"); + + b.Property("At") + .HasColumnType("TEXT") + .HasColumnName("at"); + + b.Property("DiscordId") + .HasColumnType("INTEGER") + .HasColumnName("discord_id"); + + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.HasKey("Id") + .HasName("PK_rmc_linked_accounts_logs"); + + b.HasIndex("At") + .HasDatabaseName("IX_rmc_linked_accounts_logs_at"); + + b.HasIndex("DiscordId") + .HasDatabaseName("IX_rmc_linked_accounts_logs_discord_id"); + + b.HasIndex("PlayerId") + .HasDatabaseName("IX_rmc_linked_accounts_logs_player_id"); + + b.ToTable("rmc_linked_accounts_logs", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b => + { + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("Code") + .HasColumnType("TEXT") + .HasColumnName("code"); + + b.Property("CreationTime") + .HasColumnType("TEXT") + .HasColumnName("creation_time"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_linking_codes"); + + b.HasIndex("Code") + .HasDatabaseName("IX_rmc_linking_codes_code"); + + b.ToTable("rmc_linking_codes", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.Property("PlayerId") + .HasColumnType("TEXT") + .HasColumnName("player_id"); + + b.Property("GhostColor") + .HasColumnType("INTEGER") + .HasColumnName("ghost_color"); + + b.Property("GhostHat") + .HasColumnType("TEXT") + .HasColumnName("ghost_hat"); + + b.Property("GhostMask") + .HasColumnType("TEXT") + .HasColumnName("ghost_mask"); + + b.Property("GhostParticles") + .HasColumnType("TEXT") + .HasColumnName("ghost_particles"); + + b.HasKey("PlayerId") + .HasName("PK_rmc_patrons"); + + b.ToTable("rmc_patrons", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b => + { + b.Property("PatronId") + .HasColumnType("TEXT") + .HasColumnName("patron_id"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("TEXT") + .HasColumnName("message"); + + b.HasKey("PatronId") + .HasName("PK_rmc_patron_lobby_messages"); + + b.ToTable("rmc_patron_lobby_messages", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b => + { + b.Property("PatronId") + .HasColumnType("TEXT") + .HasColumnName("patron_id"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.HasKey("PatronId") + .HasName("PK_rmc_patron_round_end_nt_shoutouts"); + + b.ToTable("rmc_patron_round_end_nt_shoutouts", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronTier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasColumnName("rmc_patron_tiers_id"); + + b.Property("DiscordRole") + .HasColumnType("INTEGER") + .HasColumnName("discord_role"); + + b.Property("GhostColor") + .HasColumnType("INTEGER") + .HasColumnName("ghost_color"); + + b.Property("GhostCosmetics") + .HasColumnType("INTEGER") + .HasColumnName("ghost_cosmetics"); + + b.Property("GhostParticles") + .HasColumnType("INTEGER") + .HasColumnName("ghost_particles"); + + b.Property("Icon") + .HasColumnType("TEXT") + .HasColumnName("icon"); + + b.Property("LobbyMessage") + .HasColumnType("INTEGER") + .HasColumnName("lobby_message"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("name"); + + b.Property("Priority") + .HasColumnType("INTEGER") + .HasColumnName("priority"); + + b.Property("RoundEndShoutout") + .HasColumnType("INTEGER") + .HasColumnName("round_end_shoutout"); + + b.Property("ShowOnCredits") + .HasColumnType("INTEGER") + .HasColumnName("show_on_credits"); + + b.HasKey("Id") + .HasName("PK_rmc_patron_tiers"); + + b.HasIndex("DiscordRole") + .IsUnique(); + + b.HasIndex("LobbyMessage") + .HasDatabaseName("IX_rmc_patron_tiers_lobby_message"); + + b.HasIndex("RoundEndShoutout") + .HasDatabaseName("IX_rmc_patron_tiers_round_end_shoutout"); + + b.ToTable("rmc_patron_tiers", (string)null); + }); + + modelBuilder.Entity("Content.Server.Database.RoleWhitelist", b => + { + b.Property("PlayerUserId") + .HasColumnType("TEXT") + .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"); + + b.Property("ServerId") + .HasColumnType("INTEGER") + .HasColumnName("server_id"); + + b.Property("StartDate") + .HasColumnType("TEXT") + .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"); + + 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("TEXT") + .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"); + + 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"); + + 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"); + + b.Property("BanId") + .HasColumnType("INTEGER") + .HasColumnName("ban_id"); + + b.Property("UnbanTime") + .HasColumnType("TEXT") + .HasColumnName("unban_time"); + + b.Property("UnbanningAdmin") + .HasColumnType("TEXT") + .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"); + + b.Property("Data") + .IsRequired() + .HasColumnType("BLOB") + .HasColumnName("data"); + + b.Property("Date") + .HasColumnType("TEXT") + .HasColumnName("date"); + + b.Property("Path") + .IsRequired() + .HasColumnType("TEXT") + .HasColumnName("path"); + + b.Property("UserId") + .HasColumnType("TEXT") + .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("TEXT") + .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("BLOB") + .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("BLOB") + .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.DBJobAlternateTitle", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("AltTitles") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_dbjob_alternate_title_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Job", b => + { + b.HasOne("Content.Server.Database.Profile", "Profile") + .WithMany("Jobs") + .HasForeignKey("ProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_job_profile_profile_id"); + + b.Navigation("Profile"); + }); + + modelBuilder.Entity("Content.Server.Database.Player", b => + { + b.OwnsOne("Content.Server.Database.TypedHwid", "LastSeenHWId", b1 => + { + b1.Property("PlayerId") + .HasColumnType("INTEGER") + .HasColumnName("player_id"); + + b1.Property("Hwid") + .IsRequired() + .HasColumnType("BLOB") + .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.Poll", b => + { + b.HasOne("Content.Server.Database.Player", "CreatedBy") + .WithMany() + .HasForeignKey("CreatedById") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("FK_polls_player_created_by_id"); + + b.Navigation("CreatedBy"); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany("Options") + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_options_polls_poll_id"); + + b.Navigation("Poll"); + }); + + modelBuilder.Entity("Content.Server.Database.PollSeen", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany() + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_seen_player_player_user_id"); + + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany() + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_seen_polls_poll_id"); + + b.Navigation("Player"); + + b.Navigation("Poll"); + }); + + modelBuilder.Entity("Content.Server.Database.PollVote", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany() + .HasForeignKey("PlayerUserId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_player_player_user_id"); + + b.HasOne("Content.Server.Database.Poll", "Poll") + .WithMany("Votes") + .HasForeignKey("PollId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_polls_poll_id"); + + b.HasOne("Content.Server.Database.PollOption", "PollOption") + .WithMany("Votes") + .HasForeignKey("PollOptionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_poll_votes_poll_options_poll_option_id"); + + b.Navigation("Player"); + + b.Navigation("Poll"); + + b.Navigation("PollOption"); + }); + + 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_id"); + + 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_loadout_id"); + + 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.RMCDiscordAccountRole", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithMany("Roles") + .HasForeignKey("DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_discord_account_roles_rmc_discord_accounts_discord_id"); + + b.Navigation("Discord"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccount", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithOne("LinkedAccount") + .HasForeignKey("Content.Server.Database.RMCLinkedAccount", "DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_rmc_discord_accounts_discord_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("LinkedAccount") + .HasForeignKey("Content.Server.Database.RMCLinkedAccount", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_player_player_id"); + + b.Navigation("Discord"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkedAccountLogs", b => + { + b.HasOne("Content.Server.Database.RMCDiscordAccount", "Discord") + .WithMany("LinkedAccountLogs") + .HasForeignKey("DiscordId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_logs_rmc_discord_accounts_discord_id"); + + b.HasOne("Content.Server.Database.Player", "Player") + .WithMany("LinkedAccountLogs") + .HasForeignKey("PlayerId") + .HasPrincipalKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linked_accounts_logs_player_player_id1"); + + b.Navigation("Discord"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCLinkingCodes", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("LinkingCodes") + .HasForeignKey("Content.Server.Database.RMCLinkingCodes", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_linking_codes_player_player_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.HasOne("Content.Server.Database.Player", "Player") + .WithOne("Patron") + .HasForeignKey("Content.Server.Database.RMCPatron", "PlayerId") + .HasPrincipalKey("Content.Server.Database.Player", "UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patrons_player_player_id"); + + b.Navigation("Player"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronLobbyMessage", b => + { + b.HasOne("Content.Server.Database.RMCPatron", "Patron") + .WithOne("LobbyMessage") + .HasForeignKey("Content.Server.Database.RMCPatronLobbyMessage", "PatronId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patron_lobby_messages_rmc_patrons_patron_id"); + + b.Navigation("Patron"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatronRoundEndNTShoutout", b => + { + b.HasOne("Content.Server.Database.RMCPatron", "Patron") + .WithOne("RoundEndNTShoutout") + .HasForeignKey("Content.Server.Database.RMCPatronRoundEndNTShoutout", "PatronId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("FK_rmc_patron_round_end_nt_shoutouts_rmc_patrons_patron_id"); + + b.Navigation("Patron"); + }); + + 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"); + + b.Navigation("LinkedAccount"); + + b.Navigation("LinkedAccountLogs"); + + b.Navigation("LinkingCodes"); + + b.Navigation("Patron"); + }); + + modelBuilder.Entity("Content.Server.Database.Poll", b => + { + b.Navigation("Options"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("Content.Server.Database.PollOption", b => + { + b.Navigation("Votes"); + }); + + modelBuilder.Entity("Content.Server.Database.Preference", b => + { + b.Navigation("Profiles"); + }); + + modelBuilder.Entity("Content.Server.Database.Profile", b => + { + b.Navigation("AltTitles"); + + 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.RMCDiscordAccount", b => + { + b.Navigation("LinkedAccount") + .IsRequired(); + + b.Navigation("LinkedAccountLogs"); + + b.Navigation("Roles"); + }); + + modelBuilder.Entity("Content.Server.Database.RMCPatron", b => + { + b.Navigation("LobbyMessage"); + + b.Navigation("RoundEndNTShoutout"); + }); + + 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/Sqlite/20260915201119_EarsAboveHair.cs b/Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.cs new file mode 100644 index 00000000000..c40adf68ab1 --- /dev/null +++ b/Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Content.Server.Database.Migrations.Sqlite +{ + /// + public partial class EarsAboveHair : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ears_above_hair", + table: "profile", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ears_above_hair", + table: "profile"); + } + } +} diff --git a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs index 75ec03c9814..b705a053845 100644 --- a/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs +++ b/Content.Server.Database/Migrations/Sqlite/SqliteServerDbContextModelSnapshot.cs @@ -1212,6 +1212,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) .HasColumnType("TEXT") .HasColumnName("custom_species_name"); + b.Property("EarsAboveHair") + .HasColumnType("INTEGER") + .HasColumnName("ears_above_hair"); + b.Property("ErpPreference") .HasColumnType("INTEGER") .HasColumnName("erp_preference"); diff --git a/Content.Server.Database/Model.cs b/Content.Server.Database/Model.cs index b5010859b5d..25c5c590b27 100644 --- a/Content.Server.Database/Model.cs +++ b/Content.Server.Database/Model.cs @@ -492,6 +492,7 @@ public class Profile public string Gender { get; set; } = null!; public string Species { get; set; } = null!; public string CustomSpeciesName { get; set; } = ""; // Arcane + public bool EarsAboveHair { get; set; } // Arcane public float Height { get; set; } = 1f; // Goobstation: port EE height/width sliders public float Width { get; set; } = 1f; // Goobstation: port EE height/width sliders public string BarkVoice { get; set; } = null!; // Goob Station - Barks diff --git a/Content.Server/Database/ServerDbBase.cs b/Content.Server/Database/ServerDbBase.cs index ef1f29f4678..71a4708f140 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -416,7 +416,8 @@ internal static HumanoidCharacterProfile ConvertProfiles(Profile profile) // Arc hairGradientEnabled, hairGradientColors, hairGradientStyle, - hairGradientOffset + hairGradientOffset, + profile.EarsAboveHair // Arcane-End ), spawnPriority, @@ -497,6 +498,7 @@ internal static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int s // Orion-End profile.Species = humanoid.Species; profile.CustomSpeciesName = humanoid.CustomSpeciesName; // Arcane + profile.EarsAboveHair = appearance.EarsAboveHair; // Arcane profile.Height = humanoid.Height; // Goobstation: port EE height/width sliders profile.Width = humanoid.Width; // Goobstation: port EE height/width sliders profile.Age = humanoid.Age; diff --git a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs index 432a116ccbf..26ca85844fb 100644 --- a/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs +++ b/Content.Shared/Humanoid/HumanoidAppearanceComponent.cs @@ -39,6 +39,13 @@ public sealed partial class HumanoidAppearanceComponent : Component // Arcane-Start [DataField, AutoNetworkedField] public string CustomSpeciesName = ""; + + /// + /// Whether the ears (HeadTop/HeadSide markings) render above the hair instead of being hidden by hair + /// from behind. + /// + [DataField, AutoNetworkedField] + public bool EarsAboveHair; // Arcane-End [DataField] // Goob Station - Barks diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs index b0f8eeebd4a..d0ebb679ad6 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -34,7 +34,6 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, [DataField] public bool HairGradientEnabled { get; set; } - [DataField] public List HairGradientColors { get; set; } = new() { Color.Black, Color.Black }; @@ -60,6 +59,11 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, [DataField] public List Markings { get; set; } = new(); + // Arcane-Start + [DataField] + public bool EarsAboveHair; + // Arcane-End + public HumanoidCharacterAppearance(string hairStyleId, Color hairColor, string facialHairStyleId, @@ -67,10 +71,13 @@ public HumanoidCharacterAppearance(string hairStyleId, Color eyeColor, Color skinColor, List markings, + // Arcane-Start bool hairGradientEnabled = false, IReadOnlyList? hairGradientColors = null, HairGradientStyle hairGradientStyle = HairGradientStyle.Ombre, - float hairGradientOffset = 0.5f) // Arcane-Edit + float hairGradientOffset = 0.5f, + bool earsAboveHair = false) + // Arcane-End { HairStyleId = hairStyleId; HairColor = ClampColor(hairColor); @@ -80,6 +87,7 @@ public HumanoidCharacterAppearance(string hairStyleId, SkinColor = ClampColor(skinColor); Markings = markings; // Arcane-Start + EarsAboveHair = earsAboveHair; HairGradientEnabled = hairGradientEnabled; HairGradientStyle = hairGradientStyle; HairGradientOffset = Math.Clamp(hairGradientOffset, 0f, 1f); @@ -98,46 +106,50 @@ public HumanoidCharacterAppearance(string hairStyleId, } public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) : - this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings), other.HairGradientEnabled, other.HairGradientColors, other.HairGradientStyle, other.HairGradientOffset) // Arcane-Edit + this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings), other.HairGradientEnabled, other.HairGradientColors, other.HairGradientStyle, other.HairGradientOffset, other.EarsAboveHair) // Arcane-Edit { } public HumanoidCharacterAppearance WithHairStyleName(string newName) { - return new(newName, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(newName, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithHairColor(Color newColor) { - return new(HairStyleId, newColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, newColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithFacialHairStyleName(string newName) { - return new(HairStyleId, HairColor, newName, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, HairColor, newName, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithFacialHairColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, newColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, HairColor, FacialHairStyleId, newColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithEyeColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, newColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, newColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithSkinColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, newColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, newColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithMarkings(List newMarkings) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset); // Arcane-Edit + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); // Arcane-Edit } // Arcane-Start + public HumanoidCharacterAppearance WithEarsAboveHair(bool newValue) + { + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, newValue); + } public HumanoidCharacterAppearance WithHairGradient(bool enabled, IReadOnlyList colors, HairGradientStyle? style = null, float? offset = null) { @@ -284,7 +296,8 @@ public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearanc appearance.HairGradientEnabled, appearance.HairGradientColors, appearance.HairGradientStyle, - appearance.HairGradientOffset); + appearance.HairGradientOffset, + appearance.EarsAboveHair); // Arcane-End } @@ -304,6 +317,7 @@ public bool MemberwiseEquals(ICharacterAppearance maybeOther) if (!EyeColor.Equals(other.EyeColor)) return false; if (!SkinColor.Equals(other.SkinColor)) return false; if (!Markings.SequenceEqual(other.Markings)) return false; + if (EarsAboveHair != other.EarsAboveHair) return false; // Arcane return true; } @@ -323,7 +337,8 @@ public bool Equals(HumanoidCharacterAppearance? other) FacialHairColor.Equals(other.FacialHairColor) && EyeColor.Equals(other.EyeColor) && SkinColor.Equals(other.SkinColor) && - Markings.SequenceEqual(other.Markings); + Markings.SequenceEqual(other.Markings) && + EarsAboveHair == other.EarsAboveHair; // Arcane } public override bool Equals(object? obj) @@ -336,7 +351,7 @@ public override int GetHashCode() { var hash = HashCode.Combine( HashCode.Combine(HairStyleId, HairColor, HairGradientEnabled), - FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings); + FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, EarsAboveHair); hash = HashCode.Combine(hash, HairGradientStyle, HairGradientOffset); foreach (var color in HairGradientColors) { diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 80193fdb210..1e96498793b 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -197,6 +197,7 @@ public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearan targetHumanoid.HairGradientColors = new(sourceHumanoid.HairGradientColors); targetHumanoid.HairGradientStyle = sourceHumanoid.HairGradientStyle; targetHumanoid.HairGradientOffset = sourceHumanoid.HairGradientOffset; + targetHumanoid.EarsAboveHair = sourceHumanoid.EarsAboveHair; // Arcane // Arcane-End if (TryComp(target, out var grammar)) @@ -603,6 +604,8 @@ public virtual void LoadProfile(EntityUid uid, HumanoidCharacterProfile? profile _cfgManager.GetCVar(CCVars.MaxNameLength)); // Arcane-End + humanoid.EarsAboveHair = profile.Appearance.EarsAboveHair; // Arcane + // begin Goobstation: port EE height/width sliders var species = _proto.Index(humanoid.Species); diff --git a/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs b/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs index ecfdc964770..1b9383f0c9c 100644 --- a/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs +++ b/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs @@ -93,7 +93,8 @@ protected void UpdateInterface(EntityUid mirrorUid, EntityUid targetUid, WizardM facialHair.Item2, humanoid.EyeColor, humanoid.SkinColor, - humanoid.MarkingSet.GetForwardEnumerator().ToList()); + humanoid.MarkingSet.GetForwardEnumerator().ToList(), + earsAboveHair: humanoid.EarsAboveHair); // Arcane var profile = new HumanoidCharacterProfile().WithGender(humanoid.Gender) .WithSex(humanoid.Sex) diff --git a/Resources/Locale/en-US/preferences/ui/markings-picker.ftl b/Resources/Locale/en-US/preferences/ui/markings-picker.ftl index 31ee8f9c42f..69b91eb8788 100644 --- a/Resources/Locale/en-US/preferences/ui/markings-picker.ftl +++ b/Resources/Locale/en-US/preferences/ui/markings-picker.ftl @@ -17,6 +17,7 @@ markings-rank-up = Up markings-rank-down = Down markings-search = Search marking-points-remaining = Markings left: {$points} +markings-ears-above-hair = Show Ears Above Hair marking-used = {$marking-name} marking-used-forced = {$marking-name} (Forced) marking-slot-add = Add diff --git a/Resources/Locale/ru-RU/preferences/ui/markings-picker.ftl b/Resources/Locale/ru-RU/preferences/ui/markings-picker.ftl index 64dadbc6b08..29e10b17901 100644 --- a/Resources/Locale/ru-RU/preferences/ui/markings-picker.ftl +++ b/Resources/Locale/ru-RU/preferences/ui/markings-picker.ftl @@ -6,6 +6,7 @@ markings-rank-up = Вверх markings-rank-down = Вниз markings-search = Поиск marking-points-remaining = Черт осталось: { $points } +markings-ears-above-hair = Показывать уши поверх волос marking-used = { $marking-name } marking-used-forced = { $marking-name } (Принудительно) marking-slot-add = Добавить diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml index cbfc7a79bc0..4a5d16b0b33 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml @@ -112,15 +112,17 @@ - map: [ "id" ] - map: [ "outerClothing" ] - map: [ "belt" ] #Goobedit - Belts over outerwear - - map: [ "enum.HumanoidVisualLayers.Tail" ] # Mentioned in moth code: This needs renaming lol. + # - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane-Edit - map: [ "back" ] - map: [ "enum.HumanoidVisualLayers.TailOversuit" ] # Floof - map: [ "neck" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane - map: [ "enum.HumanoidVisualLayers.SnoutCover" ] - map: [ "enum.HumanoidVisualLayers.FacialHair" ] + - map: [ "ears" ] # Arcane - map: [ "enum.HumanoidVisualLayers.Hair" ] # Do these need to be here? (arachnid hair arachnid hair) - map: [ "enum.HumanoidVisualLayers.HeadSide" ] - - map: [ "ears" ] + # - map: [ "ears" ] # Arcane-Edit - map: [ "eyes" ] - map: [ "enum.HumanoidVisualLayers.HeadTop" ] - map: [ "maskalt" ] diff --git a/Resources/Prototypes/Entities/Mobs/Species/moth.yml b/Resources/Prototypes/Entities/Mobs/Species/moth.yml index d9b0b51e5cc..38158e1e0bb 100644 --- a/Resources/Prototypes/Entities/Mobs/Species/moth.yml +++ b/Resources/Prototypes/Entities/Mobs/Species/moth.yml @@ -128,11 +128,12 @@ - map: [ "id" ] - map: [ "outerClothing" ] - map: [ "belt" ] #Goobedit - Belts over outerwear - - map: [ "enum.HumanoidVisualLayers.Tail" ] #in the utopian future we should probably have a wings enum inserted here so everyhting doesn't break + # - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane-Edit - map: [ "enum.HumanoidVisualLayers.TailBehindBackpack" ] # imp - map: [ "back" ] - map: [ "enum.HumanoidVisualLayers.TailOversuit" ] # Floof - map: [ "neck" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane - map: [ "enum.HumanoidVisualLayers.Face" ] # Goobstation? - map: [ "enum.HumanoidVisualLayers.SnoutCover" ] - map: [ "enum.HumanoidVisualLayers.FacialHair" ] diff --git a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml index 1a53ce66366..1da2906f653 100644 --- a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml @@ -83,7 +83,7 @@ - map: [ "enum.HumanoidVisualLayers.Face" ] - map: [ "enum.HumanoidVisualLayers.FacialHair" ] - map: [ "enum.HumanoidVisualLayers.HeadSide" ] - - map: [ "enum.HumanoidVisualLayers.Tail" ] + # - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane-Edit - map: [ "pocket1" ] - map: [ "pocket2" ] - map: [ "clownedon" ] # Dynamically generated @@ -97,6 +97,7 @@ - map: [ "mask" ] - map: [ "enum.HumanoidVisualLayers.HeadTop" ] - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane - map: [ "singingLayer" ] sprite: _DV/Effects/harpysinger.rsi state: singing_music_notes @@ -244,7 +245,7 @@ - map: [ "enum.HumanoidVisualLayers.FacialHair" ] - map: [ "enum.HumanoidVisualLayers.HeadSide" ] - map: [ "enum.HumanoidVisualLayers.HeadTop" ] - - map: [ "enum.HumanoidVisualLayers.Tail" ] + # - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane-Edit - map: [ "pocket1" ] - map: [ "pocket2" ] - map: [ "clownedon" ] # Dynamically generated @@ -257,3 +258,4 @@ - map: [ "enum.HumanoidVisualLayers.Hair" ] - map: [ "mask" ] - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane