From 23c4e4b32c9c4b909e7b450645234d4a24a6ba1a Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Mon, 14 Sep 2026 00:21:51 +0300 Subject: [PATCH 01/10] init --- .../DirectionalLayeringSystem.cs | 275 ++++++++++++++++++ 1 file changed, 275 insertions(+) create mode 100644 Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs new file mode 100644 index 0000000000..fdc74984ca --- /dev/null +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -0,0 +1,275 @@ +using System.Collections.Generic; +using Content.Client.Inventory; +using Content.Shared.Humanoid; +using Content.Shared.Humanoid.Markings; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Utility; +using static Robust.Client.GameObjects.SpriteComponent; + +namespace Content.Client._Arcane.DirectionalLayering; + +/// +/// Reorders a humanoid sprite's layers based on the direction the entity is facing. +/// Viewed from the back (facing north) the hair renders above the ears (HeadTop slot) and tails/wings render +/// above the cloak (neck slot); for any other facing the ears and the cloak render on top. +/// +public sealed class DirectionalLayeringSystem : EntitySystem +{ + [Dependency] private readonly SpriteSystem _sprite = default!; + [Dependency] private readonly MarkingManager _markingManager = default!; + [Dependency] private readonly IEyeManager _eyeManager = default!; + + private Angle _lastEyeRotation = Angle.Zero; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnMove); + } + + 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 ApplyOrdering(Entity ent) + { + if (!TryComp(ent.Owner, out TransformComponent? xform)) + return; + + // The renderer picks the RSI direction from `worldRotation + eyeRotation`, so the "back view" has to be + // determined relative to the camera, not the absolute world rotation. + var backView = IsBackView(xform.WorldRotation + _eyeManager.CurrentEye.Rotation); + var candidateKeys = GetCandidateLayerKeys(ent); + + UpdateHairEars(ent, backView, candidateKeys); + UpdateNeckTail(ent, backView, candidateKeys); + } + + /// + /// Every layer map key that can live inside one of the reordered blocks. + /// + private List GetCandidateLayerKeys(Entity ent) + { + var keys = new List + { + HumanoidVisualLayers.Hair, + HumanoidVisualLayers.HeadTop, + HumanoidVisualLayers.Tail, + HumanoidVisualLayers.Wings, + HumanoidVisualLayers.SnoutCover, + "neck", + "mask", + "head", + }; + + foreach (var markingList in ent.Comp1.MarkingSet.Markings.Values) + { + foreach (var marking in markingList) + { + if (!_markingManager.TryGetMarking(marking, out var prototype)) + continue; + + foreach (var sprite in prototype.Sprites) + { + if (sprite is SpriteSpecifier.Rsi rsi) + keys.Add($"{marking.MarkingId}-{rsi.RsiState}"); + } + } + } + + if (TryComp(ent.Owner, out InventorySlotsComponent? slots) && + slots.VisualLayerKeys.TryGetValue("neck", out var neckKeys)) + { + keys.AddRange(neckKeys); + } + + return keys; + } + + 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; + } + } + + private void UpdateHairEars(Entity ent, bool backView, List candidateKeys) + { + if (!TryGetLayerIndex(ent, HumanoidVisualLayers.Hair, out var hairIdx) || + !TryGetLayerIndex(ent, HumanoidVisualLayers.HeadTop, out var headTopIdx) || + !TryGetLayerIndex(ent, "mask", out var maskIdx) || + !TryGetLowerBoundary(ent, out var boundaryIdx)) + { + return; + } + + var hairAboveEars = hairIdx > headTopIdx; + if (hairAboveEars == backView) + return; + + if (backView) + { + // Hair currently below the mask (front layout): move the hair block directly above the ears. + var count = maskIdx - hairIdx; + MoveLayerBlock(ent, candidateKeys, hairIdx, count, boundaryIdx - count); + } + else + { + // Hair currently above the ears (back layout): move the hair block back below the mask. + var count = boundaryIdx - hairIdx; + MoveLayerBlock(ent, candidateKeys, hairIdx, count, maskIdx); + } + } + + private void UpdateNeckTail(Entity ent, bool backView, List candidateKeys) + { + if (!TryGetLayerIndex(ent, "neck", out var neckIdx) || + !TryGetLayerIndex(ent, "head", out var headIdx) || + !TryGetLayerIndex(ent, HumanoidVisualLayers.SnoutCover, out var snoutCoverIdx) || + !TryGetLayerIndex(ent, HumanoidVisualLayers.Tail, out var tailIdx)) + { + return; + } + + var neckAboveTail = neckIdx > tailIdx; + if (neckAboveTail == !backView) + return; + + if (backView) + { + // Cloak currently above the tail/wings (front layout): move the neck block back below the snout cover. + var count = headIdx - neckIdx; + MoveLayerBlock(ent, candidateKeys, neckIdx, count, snoutCoverIdx); + } + else + { + // Cloak currently below the tail/wings (back layout): move the neck block above them. + var count = snoutCoverIdx - neckIdx; + MoveLayerBlock(ent, candidateKeys, neckIdx, count, headIdx - count); + } + } + + /// + /// Index of the first layer that sits right above the ears block: either the tail, the wings or the head. + /// + private bool TryGetLowerBoundary(Entity ent, out int index) + { + index = int.MaxValue; + var found = false; + + if (TryGetLayerIndex(ent, HumanoidVisualLayers.Tail, out var tailIdx)) + { + index = System.Math.Min(index, tailIdx); + found = true; + } + + if (TryGetLayerIndex(ent, HumanoidVisualLayers.Wings, out var wingsIdx)) + { + index = System.Math.Min(index, wingsIdx); + found = true; + } + + if (TryGetLayerIndex(ent, "head", out var headIdx)) + { + index = System.Math.Min(index, headIdx); + found = true; + } + + return found; + } + + /// + /// 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 bool IsBackView(Angle angle) + { + var ang = angle.Reduced().FlipPositive().Theta; + var mod = (System.Math.Floor(ang / MathHelper.PiOver2) % 2) - 0.5; + var modTheta = ang + mod * DirectionBias; + return ((int)System.Math.Round(modTheta / MathHelper.PiOver2) % 4) == 2; + } + + /// + /// Removes a contiguous block of layers and inserts it elsewhere, keeping their relative order and re-registering + /// every layer map key that pointed into the block. The block ends up immediately before the layer that currently + /// sits at . + /// + private void MoveLayerBlock( + Entity ent, + List candidateKeys, + int start, + int count, + int insertBeforeIndex) + { + if (count <= 0) + return; + + var sprite = ent.Comp2; + var captured = new List<(object Key, int Relative)>(); + + foreach (var key in candidateKeys) + { + if (!TryGetLayerIndex(ent, key, out var index) || index < start || index >= start + count) + continue; + + captured.Add((key, index - start)); + } + + var block = new Layer[count]; + for (var i = count - 1; i >= 0; i--) + { + _sprite.RemoveLayer((ent.Owner, sprite), start + i, out var layer, false); + block[i] = layer!; + } + + for (var i = 0; i < count; i++) + _sprite.AddLayer((ent.Owner, sprite), block[i], insertBeforeIndex + i); + + foreach (var (key, relative) in captured) + SetLayerIndex(ent, key, insertBeforeIndex + relative); + } +} From 29b935dddf92a440e3504d6c6757fb56214d0bd9 Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Mon, 14 Sep 2026 00:44:45 +0300 Subject: [PATCH 02/10] fix p.1 --- .../DirectionalLayeringSystem.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index fdc74984ca..d461b711c2 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -13,14 +13,13 @@ namespace Content.Client._Arcane.DirectionalLayering; /// /// Reorders a humanoid sprite's layers based on the direction the entity is facing. -/// Viewed from the back (facing north) the hair renders above the ears (HeadTop slot) and tails/wings render -/// above the cloak (neck slot); for any other facing the ears and the cloak render on top. /// 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 Angle _lastEyeRotation = Angle.Zero; @@ -62,7 +61,7 @@ private void ApplyOrdering(Entity // The renderer picks the RSI direction from `worldRotation + eyeRotation`, so the "back view" has to be // determined relative to the camera, not the absolute world rotation. - var backView = IsBackView(xform.WorldRotation + _eyeManager.CurrentEye.Rotation); + var backView = IsBackView(_transform.GetWorldRotation(ent.Owner) + _eyeManager.CurrentEye.Rotation); var candidateKeys = GetCandidateLayerKeys(ent); UpdateHairEars(ent, backView, candidateKeys); @@ -202,19 +201,19 @@ private bool TryGetLowerBoundary(Entity From 1e823f23082331b7dab9b4ed335a11ae7f9ab2c5 Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Mon, 14 Sep 2026 13:45:24 +0300 Subject: [PATCH 03/10] relarplution --- .../DirectionalLayeringSystem.cs | 658 +++++++++++++++--- .../Entities/Mobs/Species/arachnid.yml | 6 +- .../Prototypes/Entities/Mobs/Species/moth.yml | 3 +- .../_DV/Entities/Mobs/Species/harpy.yml | 3 +- 4 files changed, 552 insertions(+), 118 deletions(-) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index d461b711c2..943e921a0a 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -2,6 +2,7 @@ using Content.Client.Inventory; using Content.Shared.Humanoid; using Content.Shared.Humanoid.Markings; +using Content.Shared.Inventory; using Robust.Client.GameObjects; using Robust.Client.Graphics; using Robust.Shared.GameObjects; @@ -12,7 +13,10 @@ namespace Content.Client._Arcane.DirectionalLayering; /// -/// Reorders a humanoid sprite's layers based on the direction the entity is facing. +/// 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 { @@ -21,13 +25,43 @@ public sealed class DirectionalLayeringSystem : EntitySystem [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly SharedTransformSystem _transform = default!; + private readonly Dictionary _cache = new(); + private Angle _lastEyeRotation = Angle.Zero; + /// + /// 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(OnRemove); + } + + public override void Shutdown() + { + _cache.Clear(); + base.Shutdown(); } public override void FrameUpdate(float frameTime) @@ -54,221 +88,617 @@ private void OnMove(EntityUid uid, HumanoidAppearanceComponent component, ref Mo ApplyOrdering((uid, component, 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 "back view" has to be + // 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 backView = IsBackView(_transform.GetWorldRotation(ent.Owner) + _eyeManager.CurrentEye.Rotation); - var candidateKeys = GetCandidateLayerKeys(ent); + 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, + }; + } - UpdateHairEars(ent, backView, candidateKeys); - UpdateNeckTail(ent, backView, candidateKeys); + 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; + } } /// - /// Every layer map key that can live inside one of the reordered blocks. + /// 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 List GetCandidateLayerKeys(Entity ent) + private OrderingCache GetCache(Entity ent) { - var keys = new List + var hairKeys = GetHairBlockKeys(ent); + var cloakKeys = GetCloakBlockKeys(ent); + var tailKeys = GetTailBlockKeys(ent); + + if (_cache.TryGetValue(ent.Owner, out var cache) && + SameKeys(cache.HairKeys, hairKeys) && + SameKeys(cache.CloakKeys, cloakKeys) && + SameKeys(cache.TailKeys, tailKeys)) { - HumanoidVisualLayers.Hair, - HumanoidVisualLayers.HeadTop, - HumanoidVisualLayers.Tail, - HumanoidVisualLayers.Wings, - HumanoidVisualLayers.SnoutCover, - "neck", - "mask", - "head", + 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 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)) + if (!_markingManager.TryGetMarking(marking, out var prototype) || + prototype.MarkingCategory != MarkingCategories.Hair) + { continue; + } foreach (var sprite in prototype.Sprites) { if (sprite is SpriteSpecifier.Rsi rsi) - keys.Add($"{marking.MarkingId}-{rsi.RsiState}"); + 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)) { - keys.AddRange(neckKeys); + cloakKeys.AddRange(neckKeys); } - return keys; + return cloakKeys; } - private bool TryGetLayerIndex(Entity ent, object key, out int index) + /// + /// 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 sprite = ent.Comp2; - index = 0; - return key switch + var tailKeys = new List { - Enum enumKey => _sprite.LayerMapTryGet((ent.Owner, sprite), enumKey, out index, false), - string stringKey => _sprite.LayerMapTryGet((ent.Owner, sprite), stringKey, out index, false), - _ => false, + HumanoidVisualLayers.Tail, + HumanoidVisualLayers.Wings, }; - } - private void SetLayerIndex(Entity ent, object key, int index) - { - var sprite = ent.Comp2; - switch (key) + foreach (var (category, markings) in ent.Comp1.MarkingSet.Markings) { - case Enum enumKey: - _sprite.LayerMapSet((ent.Owner, sprite), enumKey, index); - break; - case string stringKey: - _sprite.LayerMapSet((ent.Owner, sprite), stringKey, index); - break; + 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 UpdateHairEars(Entity ent, bool backView, List candidateKeys) + private void EnsureHairLayout( + Entity ent, + bool backView, + OrderingCache cache) { - if (!TryGetLayerIndex(ent, HumanoidVisualLayers.Hair, out var hairIdx) || - !TryGetLayerIndex(ent, HumanoidVisualLayers.HeadTop, out var headTopIdx) || - !TryGetLayerIndex(ent, "mask", out var maskIdx) || - !TryGetLowerBoundary(ent, out var boundaryIdx)) + if (cache.HairKeys.Count == 0 || + !TryGetClusterTop(ent, out var clusterTop, out var clusterTopIdx) || + !TryGetBlockExtent(ent, cache.HairKeys, out var blockStart, out var blockEnd)) { return; } - var hairAboveEars = hairIdx > headTopIdx; - if (hairAboveEars == backView) - return; - if (backView) { - // Hair currently below the mask (front layout): move the hair block directly above the ears. - var count = maskIdx - hairIdx; - MoveLayerBlock(ent, candidateKeys, hairIdx, count, boundaryIdx - count); + // 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 { - // Hair currently above the ears (back layout): move the hair block back below the mask. - var count = boundaryIdx - hairIdx; - MoveLayerBlock(ent, candidateKeys, hairIdx, count, maskIdx); + 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); } } - private void UpdateNeckTail(Entity ent, bool backView, List candidateKeys) + /// + /// 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 (!TryGetLayerIndex(ent, "neck", out var neckIdx) || - !TryGetLayerIndex(ent, "head", out var headIdx) || - !TryGetLayerIndex(ent, HumanoidVisualLayers.SnoutCover, out var snoutCoverIdx) || - !TryGetLayerIndex(ent, HumanoidVisualLayers.Tail, out var tailIdx)) + 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; } - var neckAboveTail = neckIdx > tailIdx; - if (neckAboveTail == !backView) + // 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 == "Harpy") + { + // 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; + } + + if (cloakBelowTail == wantCloakBelowTail) return; - if (backView) + 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) && + TryGetLayerIndex(ent, cache.TailKeys[0], out _)) { - // Cloak currently above the tail/wings (front layout): move the neck block back below the snout cover. - var count = headIdx - neckIdx; - MoveLayerBlock(ent, candidateKeys, neckIdx, count, snoutCoverIdx); + // 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; } - else + + if (wantCloakBelowTail) + { + // Back view: tail back on its native spot, directly above its anchor; cloak right below the tail. + if (cache.TailAnchorCaptured && TryGetLayerIndex(ent, cache.TailAnchor!, out var anchorIdx)) + { + var backTailTarget = anchorIdx + 1; + InsertBlock(ent, cache.TailKeys, tailBlock, backTailTarget); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backTailTarget - cloakCount); + 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); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backTailTarget - cloakCount); + 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. + if (!TryGetLayerIndex(ent, "head", out var headIdx)) { - // Cloak currently below the tail/wings (back layout): move the neck block above them. - var count = snoutCoverIdx - neckIdx; - MoveLayerBlock(ent, candidateKeys, neckIdx, count, headIdx - count); + InsertBlock(ent, cache.TailKeys, tailBlock, tailStart); + InsertBlock(ent, cache.CloakKeys, cloakBlock, cloakStart); + return; } + + var cloakTarget = headIdx - cloakCount; + var tailTarget = cloakTarget - tailCount; + InsertBlock(ent, cache.TailKeys, tailBlock, tailTarget); + InsertBlock(ent, cache.CloakKeys, cloakBlock, cloakTarget); } /// - /// Index of the first layer that sits right above the ears block: either the tail, the wings or the head. + /// 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 TryGetLowerBoundary(Entity ent, out int index) + private bool TryGetClusterTop( + Entity ent, + out object key, + out int index) { - index = int.MaxValue; + key = HumanoidVisualLayers.HeadTop; + index = 0; var found = false; - if (TryGetLayerIndex(ent, HumanoidVisualLayers.Tail, out var tailIdx)) + foreach (var candidate in GetHeadTrimClusterKeys(ent)) { - index = Math.Min(index, tailIdx); - found = true; + if (!TryGetLayerIndex(ent, candidate, out var candidateIdx)) + continue; + + if (!found || candidateIdx > index) + { + found = true; + key = candidate; + index = candidateIdx; + } } - if (TryGetLayerIndex(ent, HumanoidVisualLayers.Wings, out var wingsIdx)) + return found; + } + + /// + /// The base and marking layers that form the head-trim cluster above the hair: "mask", "HeadSide", "HeadTop" + /// and their marking layers. + /// + private List GetHeadTrimClusterKeys(Entity ent) + { + var candidates = new List { - index = Math.Min(index, wingsIdx); - found = true; - } + "mask", + HumanoidVisualLayers.HeadSide, + HumanoidVisualLayers.HeadTop, + }; - if (TryGetLayerIndex(ent, "head", out var headIdx)) + foreach (var (category, markings) in ent.Comp1.MarkingSet.Markings) { - index = Math.Min(index, headIdx); - found = true; + 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 found; + return candidates; } /// - /// 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. + /// 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 static bool IsBackView(Angle angle) + private bool TryGetFrontAnchor( + Entity ent, + int above, + out object anchor) { - var ang = angle.Reduced().FlipPositive().Theta; - var mod = (Math.Floor(ang / MathHelper.PiOver2) % 2) - 0.5; - var modTheta = ang + mod * DirectionBias; - return (int) Math.Round(modTheta / MathHelper.PiOver2) % 4 == 2; + 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; } /// - /// Removes a contiguous block of layers and inserts it elsewhere, keeping their relative order and re-registering - /// every layer map key that pointed into the block. The block ends up immediately before the layer that currently - /// sits at . + /// 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 void MoveLayerBlock( + private bool TryGetTailAnchor( Entity ent, - List candidateKeys, - int start, - int count, - int insertBeforeIndex) + int below, + out object key, + out int index) { - if (count <= 0) - return; + 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; + } + + 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; - var captured = new List<(object Key, int Relative)>(); + block = new List<(object, Layer)>(keys.Count); + var indices = new List<(object Key, int Index)>(keys.Count); + start = int.MaxValue; - foreach (var key in candidateKeys) + foreach (var key in keys) { - if (!TryGetLayerIndex(ent, key, out var index) || index < start || index >= start + count) - continue; + if (!TryGetLayerIndex(ent, key, out var index)) + return false; - captured.Add((key, index - start)); + start = Math.Min(start, index); + indices.Add((key, index)); } - var block = new Layer[count]; - for (var i = count - 1; i >= 0; i--) + // 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++) { - _sprite.RemoveLayer((ent.Owner, sprite), start + i, out var layer, false); - block[i] = layer!; + if (!_sprite.RemoveLayer((ent.Owner, sprite), indices[i].Index, out var layer, false)) + return false; + + block.Add((indices[i].Key, layer!)); } - for (var i = 0; i < count; i++) - _sprite.AddLayer((ent.Owner, sprite), block[i], insertBeforeIndex + i); + // 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; - foreach (var (key, relative) in captured) - SetLayerIndex(ent, key, insertBeforeIndex + relative); + 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, + }; } -} +} \ No newline at end of file diff --git a/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml b/Resources/Prototypes/Entities/Mobs/Species/arachnid.yml index cbfc7a79bc..4a5d16b0b3 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 d9b0b51e5c..38158e1e0b 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 1a53ce6636..409b0273b3 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 From 2c3a38a202e5289f66f42c85038e2f60fd9cd5ca Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Mon, 14 Sep 2026 14:35:28 +0300 Subject: [PATCH 04/10] fix p.2 --- .../DirectionalLayering/DirectionalLayeringSystem.cs | 9 ++++++--- Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index 943e921a0a..28bc96220b 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -2,11 +2,13 @@ using Content.Client.Inventory; 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; @@ -29,6 +31,8 @@ public sealed class DirectionalLayeringSystem : EntitySystem 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. @@ -383,7 +387,7 @@ private void EnsureTailAndCloakLayout( var wantCloakBelowTail = view == DirectionalView.Back; - if (ent.Comp1.Species == "Harpy") + 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. @@ -399,8 +403,7 @@ private void EnsureTailAndCloakLayout( if (!TryExtractBlock(ent, cache.TailKeys, out var tailBlock, out _)) return; - if (!TryExtractBlock(ent, cache.CloakKeys, out var cloakBlock, out var cloakStart) && - TryGetLayerIndex(ent, cache.TailKeys[0], out _)) + 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); diff --git a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml index 409b0273b3..1da2906f65 100644 --- a/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml +++ b/Resources/Prototypes/_DV/Entities/Mobs/Species/harpy.yml @@ -245,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 @@ -258,3 +258,4 @@ - map: [ "enum.HumanoidVisualLayers.Hair" ] - map: [ "mask" ] - map: [ "head" ] + - map: [ "enum.HumanoidVisualLayers.Tail" ] # Arcane From 9811d1d233d28ee1f67b7dad7b6e8ce5ee5a49cf Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Mon, 14 Sep 2026 21:16:00 +0300 Subject: [PATCH 05/10] Fix p.3 --- .../Humanoid/HumanoidAppearanceSystem.cs | 2 ++ .../DirectionalLayeringSystem.cs | 29 +++++++++++++++++++ .../HumanoidAppearanceUpdatedEvent.cs | 13 +++++++++ 3 files changed, 44 insertions(+) create mode 100644 Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 6d27b9c3ff..5b62fa42c1 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -75,6 +75,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) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index 28bc96220b..ae40e77992 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -1,5 +1,7 @@ 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; @@ -59,6 +61,9 @@ public override void Initialize() base.Initialize(); SubscribeLocalEvent(OnMove); + SubscribeLocalEvent(OnStartup); + SubscribeLocalEvent(OnAppearanceUpdated); + SubscribeLocalEvent(OnEquipmentVisualsUpdated); SubscribeLocalEvent(OnRemove); } @@ -92,6 +97,30 @@ private void OnMove(EntityUid uid, HumanoidAppearanceComponent component, ref Mo 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" || + !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); diff --git a/Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs b/Content.Client/_Arcane/DirectionalLayering/HumanoidAppearanceUpdatedEvent.cs new file mode 100644 index 0000000000..b0e554e1f5 --- /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 +{ +} From 566a614c190b5ba5ffebd2395b79b914032eca3e Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Tue, 15 Sep 2026 22:24:54 +0300 Subject: [PATCH 06/10] back to school --- .../DirectionalLayeringSystem.cs | 50 ++++++++++++++++--- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index ae40e77992..2bc9477f60 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -423,7 +423,13 @@ private void EnsureTailAndCloakLayout( return; } - if (cloakBelowTail == wantCloakBelowTail) + // 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; @@ -441,12 +447,14 @@ private void EnsureTailAndCloakLayout( if (wantCloakBelowTail) { - // Back view: tail back on its native spot, directly above its anchor; cloak right below the tail. + // 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); - InsertBlock(ent, cache.CloakKeys, cloakBlock, backTailTarget - cloakCount); + var backCloakTarget = Math.Min(backTailTarget - cloakCount, GetCloakCeiling(ent)); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backCloakTarget); return; } @@ -455,7 +463,8 @@ private void EnsureTailAndCloakLayout( { var backTailTarget = clusterIdx + 1; InsertBlock(ent, cache.TailKeys, tailBlock, backTailTarget); - InsertBlock(ent, cache.CloakKeys, cloakBlock, backTailTarget - cloakCount); + var backCloakTarget = Math.Min(backTailTarget - cloakCount, GetCloakCeiling(ent)); + InsertBlock(ent, cache.CloakKeys, cloakBlock, backCloakTarget); return; } @@ -464,7 +473,8 @@ private void EnsureTailAndCloakLayout( return; } - // Front/side view: cloak spread below the head, tail tucked directly under the cloak. + // 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); @@ -472,10 +482,12 @@ private void EnsureTailAndCloakLayout( return; } - var cloakTarget = headIdx - cloakCount; - var tailTarget = cloakTarget - tailCount; - InsertBlock(ent, cache.TailKeys, tailBlock, tailTarget); + 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); } /// @@ -637,6 +649,28 @@ private bool TryGetTailAnchor( 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, From 8b644071a24a36f6949cc0f9dd7c50009cb7d60e Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Tue, 15 Sep 2026 23:40:09 +0300 Subject: [PATCH 07/10] Ears above hair customisation --- .../Humanoid/HumanoidAppearanceSystem.cs | 1 + Content.Client/Humanoid/MarkingPicker.xaml | 3 + Content.Client/Humanoid/MarkingPicker.xaml.cs | 36 +- .../Lobby/UI/HumanoidProfileEditor.xaml.cs | 16 + .../DirectionalLayeringSystem.cs | 6 +- .../20260915201057_EarsAboveHair.Designer.cs | 2915 +++++++++++++++++ .../Postgres/20260915201057_EarsAboveHair.cs | 29 + .../PostgresServerDbContextModelSnapshot.cs | 4 + .../20260915201119_EarsAboveHair.Designer.cs | 2820 ++++++++++++++++ .../Sqlite/20260915201119_EarsAboveHair.cs | 29 + .../SqliteServerDbContextModelSnapshot.cs | 4 + Content.Server.Database/Model.cs | 1 + Content.Server/Database/ServerDbBase.cs | 4 +- .../Humanoid/HumanoidAppearanceComponent.cs | 7 + .../Humanoid/HumanoidCharacterAppearance.cs | 45 +- .../SharedHumanoidAppearanceSystem.cs | 3 + .../MagicMirror/SharedWizardMirrorSystem.cs | 3 +- .../en-US/preferences/ui/markings-picker.ftl | 1 + .../ru-RU/preferences/ui/markings-picker.ftl | 1 + 19 files changed, 5912 insertions(+), 16 deletions(-) create mode 100644 Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.Designer.cs create mode 100644 Content.Server.Database/Migrations/Postgres/20260915201057_EarsAboveHair.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.Designer.cs create mode 100644 Content.Server.Database/Migrations/Sqlite/20260915201119_EarsAboveHair.cs diff --git a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs index 5b62fa42c1..14d21203ab 100644 --- a/Content.Client/Humanoid/HumanoidAppearanceSystem.cs +++ b/Content.Client/Humanoid/HumanoidAppearanceSystem.cs @@ -258,6 +258,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 2244e3acee..486fc64720 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 847f8d2d5a..50df73b10d 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() @@ -388,6 +421,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 +610,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 f065c213b4..8489c162c9 100644 --- a/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Lobby/UI/HumanoidProfileEditor.xaml.cs @@ -561,6 +561,10 @@ public HumanoidProfileEditor( Markings.OnMarkingRemoved += OnMarkingChange; Markings.OnMarkingColorChange += OnMarkingChange; Markings.OnMarkingRankChange += OnMarkingChange; + // Arcane-Start + Markings.OnEarsAboveHairChange += OnEarsAboveHairChange; + Markings.ShowEarsAboveHairOption = true; + // Arcane-End #endregion Markings @@ -1752,6 +1756,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; @@ -2150,6 +2165,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 index 2bc9477f60..ef242629d9 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -522,7 +522,8 @@ private bool TryGetClusterTop( /// /// The base and marking layers that form the head-trim cluster above the hair: "mask", "HeadSide", "HeadTop" - /// and their marking layers. + /// 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) { @@ -533,6 +534,9 @@ private List GetHeadTrimClusterKeys(Entity +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 0000000000..d1472f917e --- /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 dd085ce606..a3f810349a 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 0000000000..07bd0f9b58 --- /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 0000000000..c40adf68ab --- /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 4a7acb4447..e4735c393f 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 50f1620780..29dc435a7a 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 f1c8ac06e3..773d4ac76a 100644 --- a/Content.Server/Database/ServerDbBase.cs +++ b/Content.Server/Database/ServerDbBase.cs @@ -342,7 +342,8 @@ private static HumanoidCharacterProfile ConvertProfiles(Profile profile) Color.FromHex(profile.FacialHairColor), Color.FromHex(profile.EyeColor), Color.FromHex(profile.SkinColor), - markings + markings, + profile.EarsAboveHair // Arcane ), spawnPriority, jobs, @@ -384,6 +385,7 @@ private static Profile ConvertProfiles(HumanoidCharacterProfile humanoid, int sl // 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 17369705a2..02d2efe435 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 54c48052e4..b4fb1475c4 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -35,13 +35,23 @@ public sealed partial class HumanoidCharacterAppearance : ICharacterAppearance, [DataField] public List Markings { get; set; } = new(); + // Arcane-Start + /// + /// Whether the ears (HeadTop/HeadSide markings) should render above the hair instead of being hidden by hair + /// from behind. + /// + [DataField] + public bool EarsAboveHair; + // Arcane-End + public HumanoidCharacterAppearance(string hairStyleId, Color hairColor, string facialHairStyleId, Color facialHairColor, Color eyeColor, Color skinColor, - List markings) + List markings, + bool earsAboveHair = false) // Arcane { HairStyleId = hairStyleId; HairColor = ClampColor(hairColor); @@ -50,48 +60,56 @@ public HumanoidCharacterAppearance(string hairStyleId, EyeColor = ClampColor(eyeColor); SkinColor = ClampColor(skinColor); Markings = markings; + EarsAboveHair = earsAboveHair; } public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) : - this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings)) + this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings), other.EarsAboveHair) // Arcane-Edit { } public HumanoidCharacterAppearance WithHairStyleName(string newName) { - return new(newName, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings); + return new(newName, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithHairColor(Color newColor) { - return new(HairStyleId, newColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings); + return new(HairStyleId, newColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithFacialHairStyleName(string newName) { - return new(HairStyleId, HairColor, newName, FacialHairColor, EyeColor, SkinColor, Markings); + return new(HairStyleId, HairColor, newName, FacialHairColor, EyeColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithFacialHairColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, newColor, EyeColor, SkinColor, Markings); + return new(HairStyleId, HairColor, FacialHairStyleId, newColor, EyeColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithEyeColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, newColor, SkinColor, Markings); + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, newColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithSkinColor(Color newColor) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, newColor, Markings); + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, newColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance WithMarkings(List newMarkings) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings); + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, newMarkings, EarsAboveHair); // Arcane-Edit + } + + // Arcane-Start + public HumanoidCharacterAppearance WithEarsAboveHair(bool newValue) + { + return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, newValue); } + // Arcane-End public static HumanoidCharacterAppearance DefaultWithSpecies(string species) { @@ -217,7 +235,8 @@ public static HumanoidCharacterAppearance EnsureValid(HumanoidCharacterAppearanc facialHairColor, eyeColor, skinColor, - markingSet.GetForwardEnumerator().ToList()); + markingSet.GetForwardEnumerator().ToList(), + appearance.EarsAboveHair); // Arcane } public bool MemberwiseEquals(ICharacterAppearance maybeOther) @@ -230,6 +249,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; } @@ -243,7 +263,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) @@ -253,7 +274,7 @@ public override bool Equals(object? obj) public override int GetHashCode() { - return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings); + return HashCode.Combine(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, EarsAboveHair); // Arcane-Edit } public HumanoidCharacterAppearance Clone() diff --git a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs index 48a2ca0b7e..7553dba9f8 100644 --- a/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs +++ b/Content.Shared/Humanoid/SharedHumanoidAppearanceSystem.cs @@ -192,6 +192,7 @@ public void CloneAppearance(EntityUid source, EntityUid target, HumanoidAppearan targetHumanoid.Gender = sourceHumanoid.Gender; targetHumanoid.CustomSpeciesName = sourceHumanoid.CustomSpeciesName; // Arcane + targetHumanoid.EarsAboveHair = sourceHumanoid.EarsAboveHair; // Arcane if (TryComp(target, out var grammar)) _grammarSystem.SetGender((target, grammar), sourceHumanoid.Gender); @@ -591,6 +592,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 ecfdc96477..5e3c8cc213 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(), + 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 31ee8f9c42..69b91eb878 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 64dadbc6b0..29e10b1790 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 = Добавить From 81aadbb4a341aaec6ae6872fd943c1cd0080e4f5 Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Wed, 16 Sep 2026 20:34:06 +0300 Subject: [PATCH 08/10] fix p.4 --- .../Humanoid/HumanoidCharacterAppearance.cs | 12 ++++-------- .../Wizard/MagicMirror/SharedWizardMirrorSystem.cs | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs index b08be0052d..d0ebb679ad 100644 --- a/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs +++ b/Content.Shared/Humanoid/HumanoidCharacterAppearance.cs @@ -75,7 +75,7 @@ public HumanoidCharacterAppearance(string hairStyleId, bool hairGradientEnabled = false, IReadOnlyList? hairGradientColors = null, HairGradientStyle hairGradientStyle = HairGradientStyle.Ombre, - float hairGradientOffset = 0.5f + float hairGradientOffset = 0.5f, bool earsAboveHair = false) // Arcane-End { @@ -86,12 +86,8 @@ public HumanoidCharacterAppearance(string hairStyleId, EyeColor = ClampColor(eyeColor); SkinColor = ClampColor(skinColor); Markings = markings; - EarsAboveHair = earsAboveHair; - } - - public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) : - this(other.HairStyleId, other.HairColor, other.FacialHairStyleId, other.FacialHairColor, other.EyeColor, other.SkinColor, new(other.Markings), other.EarsAboveHair) // Arcane-Edit // Arcane-Start + EarsAboveHair = earsAboveHair; HairGradientEnabled = hairGradientEnabled; HairGradientStyle = hairGradientStyle; HairGradientOffset = Math.Clamp(hairGradientOffset, 0f, 1f); @@ -110,7 +106,7 @@ public HumanoidCharacterAppearance(HumanoidCharacterAppearance other) : } 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 { } @@ -152,7 +148,7 @@ public HumanoidCharacterAppearance WithMarkings(List newMarkings) // Arcane-Start public HumanoidCharacterAppearance WithEarsAboveHair(bool newValue) { - return new(HairStyleId, HairColor, FacialHairStyleId, FacialHairColor, EyeColor, SkinColor, Markings, newValue, HairGradientEnabled, HairGradientColors, HairGradientStyle, HairGradientOffset, EarsAboveHair); + 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) diff --git a/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs b/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs index 5e3c8cc213..1b9383f0c9 100644 --- a/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs +++ b/Content.Shared/_Shitcode/Wizard/MagicMirror/SharedWizardMirrorSystem.cs @@ -94,7 +94,7 @@ protected void UpdateInterface(EntityUid mirrorUid, EntityUid targetUid, WizardM humanoid.EyeColor, humanoid.SkinColor, humanoid.MarkingSet.GetForwardEnumerator().ToList(), - humanoid.EarsAboveHair); // Arcane + earsAboveHair: humanoid.EarsAboveHair); // Arcane var profile = new HumanoidCharacterProfile().WithGender(humanoid.Gender) .WithSex(humanoid.Sex) From 94d26a397544563faa78fe4a3d1b241ac2b273dc Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Wed, 16 Sep 2026 21:54:52 +0300 Subject: [PATCH 09/10] fix p.5 --- Content.Client/Humanoid/MarkingPicker.xaml.cs | 1 + .../DirectionalLayeringSystem.cs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Content.Client/Humanoid/MarkingPicker.xaml.cs b/Content.Client/Humanoid/MarkingPicker.xaml.cs index 50df73b10d..6733f50213 100644 --- a/Content.Client/Humanoid/MarkingPicker.xaml.cs +++ b/Content.Client/Humanoid/MarkingPicker.xaml.cs @@ -213,6 +213,7 @@ private void SetupCategoryButtons() { _selectedMarkingCategory = MarkingCategories.Chest; } + UpdateEarsAboveHairVisibility(); // Arcane } private string GetMarkingName(MarkingPrototype marking) => Loc.GetString($"marking-{marking.ID}"); diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index ef242629d9..d6f8a0acfc 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -111,7 +111,7 @@ private void OnAppearanceUpdated(EntityUid uid, HumanoidAppearanceComponent comp private void OnEquipmentVisualsUpdated(EquipmentVisualsUpdatedEvent args) { - if (args.Slot != "neck" || + if (args.Slot != "neck" && args.Slot != "back" || !TryComp(args.Equipee, out HumanoidAppearanceComponent? humanoid) || !TryComp(args.Equipee, out SpriteComponent? sprite)) { @@ -724,7 +724,16 @@ private bool TryExtractBlock( 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!)); } @@ -771,4 +780,4 @@ private static DirectionalView GetView(Angle angle) _ => DirectionalView.Side, }; } -} \ No newline at end of file +} From d6ff4d8342cdb8eb85d55f2e6763a3eaa6ca2831 Mon Sep 17 00:00:00 2001 From: ReWAFFlution Date: Fri, 18 Sep 2026 03:54:49 +0300 Subject: [PATCH 10/10] fix p.6 --- .../DirectionalLayeringSystem.cs | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs index d6f8a0acfc..5901bbd40e 100644 --- a/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs +++ b/Content.Client/_Arcane/DirectionalLayering/DirectionalLayeringSystem.cs @@ -174,9 +174,9 @@ private void SetLayerIndex(Entity /// private OrderingCache GetCache(Entity ent) { - var hairKeys = GetHairBlockKeys(ent); - var cloakKeys = GetCloakBlockKeys(ent); - var tailKeys = GetTailBlockKeys(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) && @@ -225,6 +225,28 @@ private static bool SameKeys(List a, List b) 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 }; @@ -710,11 +732,27 @@ private bool TryExtractBlock( 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)); }