mirror of
https://github.com/space-syndicate/space-station-14.git
synced 2026-06-09 13:26:34 +02:00
merge remote wizden/master
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System.Numerics;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Administration.UI.BanList.Bans;
|
||||
using Content.Client.Administration.UI.BanList.RoleBans;
|
||||
using Content.Client.Eui;
|
||||
@@ -73,7 +74,7 @@ public sealed class BanListEui : BaseEui
|
||||
return date.ToString("MM/dd/yyyy h:mm tt");
|
||||
}
|
||||
|
||||
public static void SetData<T>(IBanListLine<T> line, SharedServerBan ban) where T : SharedServerBan
|
||||
public static void SetData<T>(IBanListLine<T> line, SharedBan ban) where T : SharedBan
|
||||
{
|
||||
line.Reason.Text = ban.Reason;
|
||||
line.BanTime.Text = FormatDate(ban.BanTime);
|
||||
@@ -94,20 +95,20 @@ public sealed class BanListEui : BaseEui
|
||||
line.BanningAdmin.Text = ban.BanningAdminName;
|
||||
}
|
||||
|
||||
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedServerBan
|
||||
private void OnLineIdsClicked<T>(IBanListLine<T> line) where T : SharedBan
|
||||
{
|
||||
_popup?.Close();
|
||||
_popup = null;
|
||||
|
||||
var ban = line.Ban;
|
||||
var id = ban.Id == null ? string.Empty : Loc.GetString("ban-list-id", ("id", ban.Id.Value));
|
||||
var ip = ban.Address == null
|
||||
var ip = ban.Addresses.Length == 0
|
||||
? string.Empty
|
||||
: Loc.GetString("ban-list-ip", ("ip", ban.Address.Value.address));
|
||||
var hwid = ban.HWId == null ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", ban.HWId));
|
||||
var guid = ban.UserId == null
|
||||
: Loc.GetString("ban-list-ip", ("ip", string.Join(',', ban.Addresses.Select(a => a.address))));
|
||||
var hwid = ban.HWIds.Length == 0 ? string.Empty : Loc.GetString("ban-list-hwid", ("hwid", string.Join(',', ban.HWIds)));
|
||||
var guid = ban.UserIds.Length == 0
|
||||
? string.Empty
|
||||
: Loc.GetString("ban-list-guid", ("guid", ban.UserId.Value.ToString()));
|
||||
: Loc.GetString("ban-list-guid", ("guid", string.Join(',', ban.UserIds)));
|
||||
|
||||
_popup = new BanListIdsPopup(id, ip, hwid, guid);
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed partial class BanListControl : Control
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetBans(List<SharedServerBan> bans)
|
||||
public void SetBans(List<SharedBan> bans)
|
||||
{
|
||||
for (var i = Bans.ChildCount - 1; i >= 1; i--)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
namespace Content.Client.Administration.UI.BanList.Bans;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedServerBan>
|
||||
public sealed partial class BanListLine : BoxContainer, IBanListLine<SharedBan>
|
||||
{
|
||||
public SharedServerBan Ban { get; }
|
||||
public SharedBan Ban { get; }
|
||||
|
||||
public event Action<BanListLine>? IdsClicked;
|
||||
|
||||
public BanListLine(SharedServerBan ban)
|
||||
public BanListLine(SharedBan ban)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.Administration.UI.BanList;
|
||||
|
||||
public interface IBanListLine<T> where T : SharedServerBan
|
||||
public interface IBanListLine<T> where T : SharedBan
|
||||
{
|
||||
T Ban { get; }
|
||||
Label Reason { get; }
|
||||
|
||||
@@ -16,7 +16,7 @@ public sealed partial class RoleBanListControl : Control
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public void SetRoleBans(List<SharedServerRoleBan> bans)
|
||||
public void SetRoleBans(List<SharedBan> bans)
|
||||
{
|
||||
for (var i = RoleBans.ChildCount - 1; i >= 1; i--)
|
||||
{
|
||||
|
||||
@@ -7,13 +7,13 @@ using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
namespace Content.Client.Administration.UI.BanList.RoleBans;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedServerRoleBan>
|
||||
public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedBan>
|
||||
{
|
||||
public SharedServerRoleBan Ban { get; }
|
||||
public SharedBan Ban { get; }
|
||||
|
||||
public event Action<RoleBanListLine>? IdsClicked;
|
||||
|
||||
public RoleBanListLine(SharedServerRoleBan ban)
|
||||
public RoleBanListLine(SharedBan ban)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
@@ -21,7 +21,7 @@ public sealed partial class RoleBanListLine : BoxContainer, IBanListLine<SharedS
|
||||
IdsHidden.OnPressed += IdsPressed;
|
||||
|
||||
BanListEui.SetData(this, ban);
|
||||
Role.Text = ban.Role;
|
||||
Role.Text = string.Join(", ", ban.Roles ?? []);
|
||||
}
|
||||
|
||||
private void IdsPressed(ButtonEventArgs buttonEventArgs)
|
||||
|
||||
@@ -70,7 +70,7 @@ public sealed partial class AdminNotesLine : BoxContainer
|
||||
|
||||
TimeLabel.Text = Note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss");
|
||||
ServerLabel.Text = Note.ServerName ?? "Unknown";
|
||||
RoundLabel.Text = Note.Round == null ? "Unknown round" : "Round " + Note.Round;
|
||||
RoundLabel.Text = Note.Rounds.Length == 0 ? "Unknown round" : "Round " + string.Join(',', Note.Rounds);
|
||||
AdminLabel.Text = Note.CreatedByName;
|
||||
PlaytimeLabel.Text = $"{Note.PlaytimeAtNote.TotalHours: 0.0}h";
|
||||
|
||||
@@ -143,7 +143,12 @@ public sealed partial class AdminNotesLine : BoxContainer
|
||||
|
||||
private string FormatRoleBanMessage()
|
||||
{
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {string.Join(", ", Note.BannedRoles ?? new[] { "unknown" })} ");
|
||||
var rolesText = string.Join(
|
||||
", ",
|
||||
// Explicit cast here to avoid sandbox violation.
|
||||
(IEnumerable<BanRoleDef>?)Note.BannedRoles ?? [new BanRoleDef("what", "You should not be seeing this")]);
|
||||
|
||||
var banMessage = new StringBuilder($"{Loc.GetString("admin-notes-banned-from")} {rolesText} ");
|
||||
return FormatBanMessageCommon(banMessage);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ public sealed partial class AdminNotesLinePopup : Popup
|
||||
IdLabel.Text = Loc.GetString("admin-notes-id", ("id", note.Id));
|
||||
TypeLabel.Text = Loc.GetString("admin-notes-type", ("type", note.NoteType));
|
||||
SeverityLabel.Text = Loc.GetString("admin-notes-severity", ("severity", note.NoteSeverity ?? NoteSeverity.None));
|
||||
RoundIdLabel.Text = note.Round == null
|
||||
RoundIdLabel.Text = note.Rounds.Length == 0
|
||||
? Loc.GetString("admin-notes-round-id-unknown")
|
||||
: Loc.GetString("admin-notes-round-id", ("id", note.Round));
|
||||
: Loc.GetString("admin-notes-round-id", ("id", string.Join(',', note.Rounds)));
|
||||
CreatedByLabel.Text = Loc.GetString("admin-notes-created-by", ("author", note.CreatedByName));
|
||||
CreatedAtLabel.Text = Loc.GetString("admin-notes-created-at", ("date", note.CreatedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm:ss")));
|
||||
EditedByLabel.Text = Loc.GetString("admin-notes-last-edited-by", ("author", note.EditedByName));
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
using Content.Shared.Body.Systems;
|
||||
|
||||
namespace Content.Client.Body.Systems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class MetabolizerSystem : SharedMetabolizerSystem;
|
||||
@@ -232,6 +232,9 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
|
||||
|
||||
private void OnMarkingsChangedVisibility(Entity<VisualOrganMarkingsComponent> ent, ref BodyRelayedEvent<HumanoidLayerVisibilityChangedEvent> args)
|
||||
{
|
||||
if (!ent.Comp.HideableLayers.Contains(args.Args.Layer))
|
||||
return;
|
||||
|
||||
foreach (var markings in ent.Comp.Markings.Values)
|
||||
{
|
||||
foreach (var marking in markings)
|
||||
@@ -239,7 +242,7 @@ public sealed class VisualBodySystem : SharedVisualBodySystem
|
||||
if (!_marking.TryGetMarking(marking, out var proto))
|
||||
continue;
|
||||
|
||||
if (proto.BodyPart != args.Args.Layer)
|
||||
if (proto.BodyPart != args.Args.Layer && !(ent.Comp.DependentHidingLayers.TryGetValue(args.Args.Layer, out var dependent) && dependent.Contains(proto.BodyPart)))
|
||||
continue;
|
||||
|
||||
foreach (var sprite in proto.Sprites)
|
||||
|
||||
@@ -70,9 +70,9 @@ namespace Content.Client.Cargo.BUI
|
||||
|
||||
_menu.OnClose += Close;
|
||||
|
||||
_menu.OnItemSelected += (args) =>
|
||||
_menu.OnItemSelected += (row) =>
|
||||
{
|
||||
if (args.Button.Parent is not CargoProductRow row)
|
||||
if (row == null)
|
||||
return;
|
||||
|
||||
description.Clear();
|
||||
@@ -175,23 +175,23 @@ namespace Content.Client.Cargo.BUI
|
||||
return true;
|
||||
}
|
||||
|
||||
private void RemoveOrder(ButtonEventArgs args)
|
||||
private void RemoveOrder(CargoOrderData? order)
|
||||
{
|
||||
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
|
||||
if (order == null)
|
||||
return;
|
||||
|
||||
SendMessage(new CargoConsoleRemoveOrderMessage(row.Order.OrderId));
|
||||
SendMessage(new CargoConsoleRemoveOrderMessage(order.OrderId));
|
||||
}
|
||||
|
||||
private void ApproveOrder(ButtonEventArgs args)
|
||||
private void ApproveOrder(CargoOrderData? order)
|
||||
{
|
||||
if (args.Button.Parent?.Parent is not CargoOrderRow row || row.Order == null)
|
||||
if (order == null)
|
||||
return;
|
||||
|
||||
if (OrderCount >= OrderCapacity)
|
||||
return;
|
||||
|
||||
SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
|
||||
SendMessage(new CargoConsoleApproveOrderMessage(order.OrderId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,86 +1,226 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
SetSize="600 600"
|
||||
MinSize="600 600">
|
||||
<BoxContainer Orientation="Vertical" Margin="15 5 15 10">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<RichTextLabel Name="AccountNameLabel"
|
||||
Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'cargo-console-menu-points-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
<RichTextLabel Name="PointsLabel"
|
||||
Text="$0" />
|
||||
</BoxContainer>
|
||||
<Control MinHeight="10"/>
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="540 390"
|
||||
SetSize="995 600">
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
|
||||
<TabContainer Name="TabContainer" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<OptionButton Name="Categories"
|
||||
Prefix="{Loc 'cargo-console-menu-categories-label'}"
|
||||
HorizontalExpand="True" />
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<ScrollContainer HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2">
|
||||
<BoxContainer Name="Products"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<!-- Products get added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<Control MinHeight="5" Name="OrdersSpacer"/>
|
||||
<PanelContainer VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="1"
|
||||
Name="Orders">
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000000" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" Margin="5">
|
||||
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
|
||||
<BoxContainer Name="Requests"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
<!-- Requests are added here by code -->
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
<!-- Funds tab -->
|
||||
<BoxContainer Orientation="Vertical" Margin="15">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
|
||||
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
|
||||
<OptionButton Name="ActionOptions"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
|
||||
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="15"/>
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Sub-Main Container -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True"
|
||||
Margin="8 4 8 6">
|
||||
|
||||
<!-- Left Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="4"
|
||||
Margin="0 0 8 0"
|
||||
HorizontalExpand="True">
|
||||
|
||||
<!-- Info -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="3">
|
||||
|
||||
<!-- Account -->
|
||||
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
|
||||
<RichTextLabel Name="AccountNameLabel"
|
||||
Text="{Loc 'cargo-console-menu-account-name-none-text'}"
|
||||
Margin="4 0"/>
|
||||
|
||||
<!-- Balance -->
|
||||
<Label Text="{Loc 'cargo-console-menu-points-label'}"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2"/>
|
||||
|
||||
<RichTextLabel Name="PointsLabel"
|
||||
Text="$0"
|
||||
Margin="4 0" />
|
||||
|
||||
<!-- Orders Count/Capacity -->
|
||||
<Label Text="{Loc 'cargo-console-menu-order-capacity-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -2 0 -1"/>
|
||||
|
||||
<Label Name="ShuttleCapacityLabel"
|
||||
Text="0/20"
|
||||
Margin="4 0"/>
|
||||
</GridContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 4.5 -8 0"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Search -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 2 0 0">
|
||||
|
||||
<LineEdit Name="SearchBar"
|
||||
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
|
||||
<OptionButton Name="Categories"
|
||||
Prefix="{Loc 'cargo-console-menu-categories-label'}"
|
||||
StyleClasses="OpenLeft"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Product list -->
|
||||
<ScrollContainer
|
||||
HorizontalExpand="False"
|
||||
VerticalExpand="True"
|
||||
HScrollEnabled="False">
|
||||
|
||||
<BoxContainer Name="Products"
|
||||
Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Products get added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -8"/>
|
||||
|
||||
<!-- Right Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SizeFlagsStretchRatio="0.8"
|
||||
HorizontalExpand="True"
|
||||
Name="RightPart">
|
||||
|
||||
<!-- Requests Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
SizeFlagsStretchRatio="2">
|
||||
|
||||
<!-- Title -->
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'cargo-console-menu-requests-label'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4"/>
|
||||
</controls:StripeBack>
|
||||
|
||||
<PanelContainer VerticalExpand="True"
|
||||
Margin="0 -4 0 0">
|
||||
|
||||
<!-- Background -->
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#040404" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Name="Requests"
|
||||
Orientation="Vertical"
|
||||
StyleClasses="transparentItemList"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="8"
|
||||
Margin="8">
|
||||
|
||||
<!-- Requests are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Orders Part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Title -->
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'cargo-console-menu-orders-label'}"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="4"/>
|
||||
</controls:StripeBack>
|
||||
|
||||
<PanelContainer VerticalExpand="True"
|
||||
Margin="0 -4 0 0">
|
||||
|
||||
<!-- Background -->
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#040404" />
|
||||
</PanelContainer.PanelOverride>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="6">
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
StyleClasses="transparentItemList"
|
||||
VerticalExpand="True"
|
||||
SeparationOverride="6">
|
||||
|
||||
<!-- Orders are added here by code -->
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Funds tab -->
|
||||
<BoxContainer Orientation="Vertical" Margin="15">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
|
||||
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
|
||||
<OptionButton Name="ActionOptions"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
|
||||
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="15"/>
|
||||
<BoxContainer HorizontalAlignment="Center">
|
||||
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
|
||||
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</TabContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<!-- TODO: Create customControls element -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="12 0 6 2"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<!-- Footer title -->
|
||||
<Label Text="{Loc 'cargo-console-menu-flavor-left'}"
|
||||
StyleClasses="WindowFooterText" />
|
||||
|
||||
<!-- Version -->
|
||||
<Label Text="{Loc 'cargo-console-menu-flavor-right'}"
|
||||
StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 4 0" />
|
||||
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -6,6 +6,7 @@ using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
@@ -29,9 +30,9 @@ namespace Content.Client.Cargo.UI
|
||||
private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
|
||||
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
|
||||
|
||||
public event Action<ButtonEventArgs>? OnItemSelected;
|
||||
public event Action<ButtonEventArgs>? OnOrderApproved;
|
||||
public event Action<ButtonEventArgs>? OnOrderCanceled;
|
||||
public event Action<CargoProductRow?>? OnItemSelected;
|
||||
public event Action<CargoOrderData?>? OnOrderApproved;
|
||||
public event Action<CargoOrderData?>? OnOrderCanceled;
|
||||
|
||||
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
|
||||
|
||||
@@ -164,7 +165,7 @@ namespace Content.Client.Cargo.UI
|
||||
};
|
||||
button.MainButton.OnPressed += args =>
|
||||
{
|
||||
OnItemSelected?.Invoke(args);
|
||||
OnItemSelected?.Invoke(button);
|
||||
};
|
||||
Products.AddChild(button);
|
||||
}
|
||||
@@ -215,33 +216,61 @@ namespace Content.Client.Cargo.UI
|
||||
|
||||
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
|
||||
var productName = product.Name;
|
||||
var requester = !string.IsNullOrEmpty(order.Requester) ?
|
||||
order.Requester : Loc.GetString("cargo-console-menu-order-row-alerts-requester-unknown");
|
||||
var account = _protoManager.Index(order.Account);
|
||||
|
||||
var row = new CargoOrderRow
|
||||
{
|
||||
Order = order,
|
||||
|
||||
Title =
|
||||
{
|
||||
Text = Loc.GetString(
|
||||
"cargo-console-menu-order-row-title",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("orderPrice", order.Price)),
|
||||
},
|
||||
|
||||
Stride =
|
||||
{
|
||||
PanelOverride = new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = account.Color,
|
||||
ContentMarginBottomOverride = 2,
|
||||
},
|
||||
},
|
||||
|
||||
Icon = { Texture = _spriteSystem.Frame0(product) },
|
||||
|
||||
ProductName =
|
||||
{
|
||||
Text = Loc.GetString(
|
||||
"cargo-console-menu-populate-orders-cargo-order-row-product-name-text",
|
||||
("productName", productName),
|
||||
("orderAmount", order.OrderQuantity),
|
||||
("orderRequester", order.Requester),
|
||||
("orderRequester", requester),
|
||||
("accountColor", account.Color),
|
||||
("account", Loc.GetString(account.Code)))
|
||||
},
|
||||
|
||||
Description =
|
||||
{
|
||||
Text = Loc.GetString("cargo-console-menu-order-reason-description",
|
||||
("reason", order.Reason))
|
||||
Text = !string.IsNullOrEmpty(order.Reason) ?
|
||||
Loc.GetString(
|
||||
"cargo-console-menu-order-row-product-description",
|
||||
("orderReason", order.Reason))
|
||||
:
|
||||
Loc.GetString(
|
||||
"cargo-console-menu-order-row-product-description",
|
||||
("orderReason", Loc.GetString("cargo-console-menu-order-row-alerts-reason-absent")))
|
||||
}
|
||||
};
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
|
||||
|
||||
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(order); };
|
||||
|
||||
// TODO: Disable based on access.
|
||||
row.SetApproveVisible(orderConsole.Mode != CargoOrderConsoleMode.SendToPrimary);
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
|
||||
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(order); };
|
||||
Requests.AddChild(row);
|
||||
}
|
||||
}
|
||||
@@ -294,8 +323,7 @@ namespace Content.Client.Cargo.UI
|
||||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
|
||||
_timing.CurTime < orderConsole.NextAccountActionTime;
|
||||
|
||||
OrdersSpacer.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
Orders.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
RightPart.Visible = orderConsole.Mode != CargoOrderConsoleMode.PrintSlip;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,53 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'cargo-console-order-menu-title'}">
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
Title="{Loc 'cargo-console-order-menu-title'}"
|
||||
MinSize="460 261">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<GridContainer Columns="2">
|
||||
<Label Text="{Loc 'cargo-console-order-menu-product-label'}" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-product-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<Label Name="ProductName"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-description-label'}" />
|
||||
Access="Public" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-description-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<RichTextLabel Name="Description"
|
||||
Access="Public"
|
||||
VerticalExpand="True"
|
||||
SetWidth="350"/>
|
||||
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}" />
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
MaxWidth="460" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-cost-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<Label Name="PointCost"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}" />
|
||||
Access="Public" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-requester-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<LineEdit Name="Requester"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-reason-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<LineEdit Name="Reason"
|
||||
Access="Public" />
|
||||
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}" />
|
||||
|
||||
<Label Text="{Loc 'cargo-console-order-menu-amount-label'}"
|
||||
StyleClasses="LabelKeyText" />
|
||||
|
||||
<SpinBox Name="Amount"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
Value="1" />
|
||||
</GridContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 6 0 2"/>
|
||||
<Button Name="SubmitButton"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-order-menu-submit-button'}"
|
||||
TextAlign="Center" />
|
||||
VerticalAlignment="Bottom" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -1,33 +1,81 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 1">
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
<Control MinWidth="5"/>
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<RichTextLabel Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText" />
|
||||
<Label Name="Description"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
<PanelContainer
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
HorizontalExpand="True"
|
||||
StyleClasses="BackgroundPanel">
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
SeparationOverride="6"
|
||||
Margin="-14 -2">
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Control>
|
||||
<PanelContainer StyleClasses="WindowHeadingBackground" />
|
||||
|
||||
<BoxContainer Margin="6">
|
||||
<Label Name="Title"
|
||||
Access="Public"
|
||||
MaxHeight="28"
|
||||
StyleClasses="LabelKeyText"/>
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
|
||||
<PanelContainer Name="Stride"
|
||||
Access="Public"
|
||||
StyleClasses="LowDivider" />
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Info -->
|
||||
<BoxContainer>
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
Margin="4"
|
||||
Stretch="KeepAspectCentered"
|
||||
RectClipContent="True"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="4 0"/>
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
|
||||
<RichTextLabel Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="LabelSubText" />
|
||||
|
||||
<Label Name="Description"
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="LabelSubText"
|
||||
ClipText="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
|
||||
<!-- Buttons -->
|
||||
<!-- Btn's position hardcoded (args.Button.Parent?.Parent?.Parent type) in CargoConsoleBUI 158 & 166 line -->
|
||||
<BoxContainer Margin="6">
|
||||
<Button Name="Approve"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-order-row-button-approve'}"
|
||||
StyleClasses="OpenRight"
|
||||
HorizontalExpand="True"/>
|
||||
|
||||
<Button Name="Cancel"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-order-row-button-cancel'}"
|
||||
StyleClasses="OpenLeft"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Button Name="Approve"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
|
||||
StyleClasses="OpenRight" />
|
||||
<Button Name="Cancel"
|
||||
Access="Public"
|
||||
Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
|
||||
StyleClasses="OpenLeft" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
<PanelContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True">
|
||||
<Button Name="MainButton"
|
||||
ToolTip=""
|
||||
Access="Public"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True"
|
||||
StyleClasses="OpenBoth"/>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True">
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
<Label Name="ProductName"
|
||||
Access="Public"
|
||||
HorizontalExpand="True" />
|
||||
<PanelContainer StyleClasses="BackgroundDark">
|
||||
<Label Name="PointCost"
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True">
|
||||
<!-- Btn position hardcoded (args.Button.Parent?.Parent type) in CargoConsoleBUI 71 line -->
|
||||
<Button Name="MainButton"
|
||||
ToolTip=""
|
||||
Access="Public"
|
||||
VerticalExpand="False"
|
||||
StyleClasses="OpenBoth" />
|
||||
|
||||
<!-- Icon & Name -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
HorizontalExpand="True"
|
||||
Margin="4 0">
|
||||
|
||||
<TextureRect Name="Icon"
|
||||
Access="Public"
|
||||
MinSize="32 32"
|
||||
RectClipContent="True" />
|
||||
|
||||
<Label Name="ProductName"
|
||||
Access="Public"
|
||||
MinSize="52 32"
|
||||
Align="Right"
|
||||
Margin="0 0 5 0"/>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
HorizontalExpand="True"
|
||||
ClipText="True" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
<Label Name="PointCost"
|
||||
Access="Public"
|
||||
MinSize="56 32"
|
||||
Align="Right"
|
||||
Margin="0 0 5 0"
|
||||
HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -7,7 +7,7 @@ using Robust.Client.UserInterface.XAML;
|
||||
namespace Content.Client.Cargo.UI
|
||||
{
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class CargoProductRow : PanelContainer
|
||||
public sealed partial class CargoProductRow : BoxContainer
|
||||
{
|
||||
public CargoProductPrototype? Product { get; set; }
|
||||
|
||||
|
||||
@@ -291,7 +291,6 @@ namespace Content.Client.Construction
|
||||
_ghosts.Add(comp.GhostId, ghost.Value);
|
||||
|
||||
var sprite = Comp<SpriteComponent>(ghost.Value);
|
||||
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
|
||||
|
||||
if (targetProto.TryGetComponent(out IconComponent? icon, EntityManager.ComponentFactory))
|
||||
{
|
||||
@@ -306,20 +305,11 @@ namespace Content.Client.Construction
|
||||
var targetSprite = EnsureComp<SpriteComponent>(dummy);
|
||||
EntityManager.System<AppearanceSystem>().OnChangeData(dummy, targetSprite);
|
||||
|
||||
for (var i = 0; i < targetSprite.AllLayers.Count(); i++)
|
||||
_sprite.CopySprite((dummy, targetSprite), (ghost.Value, sprite));
|
||||
|
||||
for (var i = 0; i < sprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (!targetSprite[i].Visible || !targetSprite[i].RsiState.IsValid)
|
||||
continue;
|
||||
|
||||
var rsi = targetSprite[i].Rsi ?? targetSprite.BaseRSI;
|
||||
if (rsi is null || !rsi.TryGetState(targetSprite[i].RsiState, out var state) ||
|
||||
state.StateId.Name is null)
|
||||
continue;
|
||||
|
||||
_sprite.AddBlankLayer((ghost.Value, sprite), i);
|
||||
_sprite.LayerSetSprite((ghost.Value, sprite), i, new SpriteSpecifier.Rsi(rsi.Path, state.StateId.Name));
|
||||
sprite.LayerSetShader(i, "unshaded");
|
||||
_sprite.LayerSetVisible((ghost.Value, sprite), i, true);
|
||||
}
|
||||
|
||||
Del(dummy);
|
||||
@@ -327,6 +317,8 @@ namespace Content.Client.Construction
|
||||
else
|
||||
return false;
|
||||
|
||||
_sprite.SetColor((ghost.Value, sprite), new Color(48, 255, 48, 128));
|
||||
|
||||
if (prototype.CanBuildInImpassable)
|
||||
EnsureComp<WallMountComponent>(ghost.Value).Arc = new(Math.Tau);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Client.DebugMon;
|
||||
using Content.Client.Corvax.TTS;
|
||||
using Content.Client.Options;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.FeedbackPopup;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
@@ -26,6 +27,7 @@ using Content.Client.UserInterface;
|
||||
using Content.Client.Viewport;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.Ame.Components;
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Content.Shared.Gravity;
|
||||
using Content.Shared.Localizations;
|
||||
using Robust.Client;
|
||||
@@ -78,6 +80,7 @@ namespace Content.Client.Entry
|
||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
|
||||
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
|
||||
|
||||
public override void PreInit()
|
||||
{
|
||||
@@ -173,6 +176,7 @@ namespace Content.Client.Entry
|
||||
_userInterfaceManager.SetActiveTheme(_configManager.GetCVar(CVars.InterfaceTheme));
|
||||
_documentParsingManager.Initialize();
|
||||
_titleWindowManager.Initialize();
|
||||
_feedbackManager.Initialize();
|
||||
|
||||
_baseClient.RunLevelChanged += (_, args) =>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed class ClientFeedbackManager : SharedFeedbackManager
|
||||
{
|
||||
/// <summary>
|
||||
/// A read-only set representing the currently displayed feedback popups.
|
||||
/// </summary>
|
||||
public IReadOnlySet<ProtoId<FeedbackPopupPrototype>> DisplayedPopups => _displayedPopups;
|
||||
|
||||
private readonly HashSet<ProtoId<FeedbackPopupPrototype>> _displayedPopups = [];
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
NetManager.RegisterNetMessage<FeedbackPopupMessage>(ReceivedPopupMessage);
|
||||
NetManager.RegisterNetMessage<OpenFeedbackPopupMessage>(_ => Open());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the feedback popup window.
|
||||
/// </summary>
|
||||
public void Open()
|
||||
{
|
||||
InvokeDisplayedPopupsChanged(true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Display(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
|
||||
{
|
||||
if (prototypes == null || !NetManager.IsClient)
|
||||
return;
|
||||
|
||||
var count = _displayedPopups.Count;
|
||||
_displayedPopups.UnionWith(prototypes);
|
||||
InvokeDisplayedPopupsChanged(_displayedPopups.Count > count);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Remove(List<ProtoId<FeedbackPopupPrototype>>? prototypes)
|
||||
{
|
||||
if (!NetManager.IsClient)
|
||||
return;
|
||||
|
||||
if (prototypes == null)
|
||||
{
|
||||
_displayedPopups.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
_displayedPopups.ExceptWith(prototypes);
|
||||
}
|
||||
|
||||
InvokeDisplayedPopupsChanged(false);
|
||||
}
|
||||
|
||||
private void ReceivedPopupMessage(FeedbackPopupMessage message)
|
||||
{
|
||||
if (message.Remove)
|
||||
{
|
||||
Remove(message.FeedbackPrototypes);
|
||||
return;
|
||||
}
|
||||
|
||||
Display(message.FeedbackPrototypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Control xmlns="https://spacestation14.io"
|
||||
MinHeight="100">
|
||||
<PanelContainer StyleClasses="BackgroundPanel" ModulateSelfOverride="#2b2b31"/>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<!-- Title -->
|
||||
<PanelContainer StyleIdentifier="FeedbackBorderThinBottom">
|
||||
<RichTextLabel Name="TitleLabel" Margin="12 6 6 6" />
|
||||
</PanelContainer>
|
||||
|
||||
<!-- Description -->
|
||||
<RichTextLabel Name="DescriptionLabel" StyleClasses="LabelLight" Margin="12 4 12 8" VerticalExpand="True"/>
|
||||
|
||||
<!-- Footer -->
|
||||
<PanelContainer StyleIdentifier="FeedbackBorderThinTop">
|
||||
<BoxContainer>
|
||||
<Label FontColorOverride="#b1b1b2" StyleClasses="LabelSmall" Name="TypeLabel" Margin="14 6 6 6" />
|
||||
<Button Name="LinkButton" Text="{Loc feedbackpopup-control-button-text}" MinWidth="80"
|
||||
Margin="8 6 14 6" HorizontalExpand="True" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
|
||||
</BoxContainer>
|
||||
</Control>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FeedbackEntry : Control
|
||||
{
|
||||
private readonly IUriOpener _uri;
|
||||
|
||||
private readonly FeedbackPopupPrototype? _prototype;
|
||||
|
||||
public FeedbackEntry(ProtoId<FeedbackPopupPrototype> popupProto, IPrototypeManager proto, IUriOpener uri)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
_uri = uri;
|
||||
|
||||
_prototype = proto.Index(popupProto);
|
||||
|
||||
// Title
|
||||
TitleLabel.Text = _prototype.Title;
|
||||
DescriptionLabel.Text = _prototype.Description;
|
||||
TypeLabel.Text = _prototype.ResponseType;
|
||||
|
||||
LinkButton.Visible = !string.IsNullOrEmpty(_prototype.ResponseLink);
|
||||
|
||||
// link button
|
||||
if (!string.IsNullOrEmpty(_prototype.ResponseLink))
|
||||
{
|
||||
LinkButton.OnPressed += OnButtonPressed;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnButtonPressed(BaseButton.ButtonEventArgs args)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_prototype?.ResponseLink))
|
||||
_uri.OpenUri(_prototype.ResponseLink);
|
||||
}
|
||||
|
||||
protected override void Resized()
|
||||
{
|
||||
base.Resized();
|
||||
// magic
|
||||
TitleLabel.SetWidth = Width - TitleLabel.Margin.SumHorizontal;
|
||||
TitleLabel.InvalidateArrange();
|
||||
DescriptionLabel.SetWidth = Width - DescriptionLabel.Margin.SumHorizontal;
|
||||
DescriptionLabel.InvalidateArrange();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using Content.Client.Stylesheets;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class FeedbackPopupSheetlet : Sheetlet<PalettedStylesheet>
|
||||
{
|
||||
public override StyleRule[] GetRules(PalettedStylesheet sheet, object config)
|
||||
{
|
||||
var borderTop = new StyleBoxFlat()
|
||||
{
|
||||
BorderColor = sheet.SecondaryPalette.Base,
|
||||
BorderThickness = new Thickness(0, 1, 0, 0),
|
||||
};
|
||||
|
||||
var borderBottom = new StyleBoxFlat()
|
||||
{
|
||||
BorderColor = sheet.SecondaryPalette.Base,
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
};
|
||||
|
||||
return
|
||||
[
|
||||
E<PanelContainer>()
|
||||
.Identifier("FeedbackBorderThinTop")
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderTop),
|
||||
E<PanelContainer>()
|
||||
.Identifier("FeedbackBorderThinBottom")
|
||||
.Prop(PanelContainer.StylePropertyPanel, borderBottom),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Content.Shared.GameTicking;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
/// <summary>
|
||||
/// This handles getting feedback popup messages from the server and making a popup in the client.
|
||||
/// </summary>
|
||||
[UsedImplicitly]
|
||||
public sealed class FeedbackPopupUIController : UIController
|
||||
{
|
||||
[Dependency] private readonly ClientFeedbackManager _feedbackManager = null!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = null!;
|
||||
[Dependency] private readonly IUriOpener _uri = null!;
|
||||
|
||||
private FeedbackPopupWindow _window = null!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
_window = new FeedbackPopupWindow(_proto, _uri);
|
||||
|
||||
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
|
||||
SubscribeNetworkEvent<RoundEndMessageEvent>(OnRoundEnd);
|
||||
|
||||
_feedbackManager.DisplayedPopupsChanged += OnPopupsChanged;
|
||||
}
|
||||
|
||||
public void ToggleWindow()
|
||||
{
|
||||
if (_window.IsOpen)
|
||||
{
|
||||
_window.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_window.OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnRoundEnd(RoundEndMessageEvent ev, EntitySessionEventArgs args)
|
||||
{
|
||||
// Add round end prototypes.
|
||||
var roundEndPrototypes = _feedbackManager.GetOriginFeedbackPrototypes(true);
|
||||
if (roundEndPrototypes.Count == 0)
|
||||
return;
|
||||
|
||||
_feedbackManager.Display(roundEndPrototypes);
|
||||
|
||||
// Even if no new prototypes were added, we still want to open the window.
|
||||
if (!_window.IsOpen)
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnPopupsChanged(bool newPopups)
|
||||
{
|
||||
UpdateWindow(_feedbackManager.DisplayedPopups);
|
||||
|
||||
if (newPopups && !_window.IsOpen)
|
||||
_window.OpenCentered();
|
||||
}
|
||||
|
||||
private void OnPrototypesReloaded(PrototypesReloadedEventArgs ev)
|
||||
{
|
||||
UpdateWindow(_feedbackManager.DisplayedPopups);
|
||||
}
|
||||
|
||||
private void UpdateWindow(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
|
||||
{
|
||||
_window.Update(prototypes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
Title="{Loc feedbackpopup-window-name}" MinSize="510 460" RectClipContent="True">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
<!-- main box area -->
|
||||
<BoxContainer Margin="12 12 12 5" VerticalExpand="True">
|
||||
<PanelContainer HorizontalExpand="True" StyleClasses="PanelDark">
|
||||
<ScrollContainer HorizontalExpand="True" HScrollEnabled="False">
|
||||
<BoxContainer Name="NotificationContainer" HorizontalExpand="True" Orientation="Vertical" Margin="10" SeparationOverride="10" />
|
||||
</ScrollContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical" SetHeight="30" Margin="2 0 0 0">
|
||||
<BoxContainer SetHeight="33" Margin="10 0 10 5">
|
||||
<Label Text="{Loc feedbackpopup-control-ui-footer}" Margin="6 0" StyleClasses="PdaContentFooterText"/>
|
||||
<Label Name="NumNotifications" Margin="6 0" HorizontalExpand="True" HorizontalAlignment="Right"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
@@ -0,0 +1,49 @@
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.FeedbackPopup;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class FeedbackPopupWindow : FancyWindow
|
||||
{
|
||||
private readonly IPrototypeManager _proto;
|
||||
private readonly IUriOpener _uri;
|
||||
|
||||
public FeedbackPopupWindow(IPrototypeManager proto, IUriOpener uri)
|
||||
{
|
||||
_proto = proto;
|
||||
_uri = uri;
|
||||
RobustXamlLoader.Load(this);
|
||||
DisplayNoEntryLabel();
|
||||
}
|
||||
|
||||
public void Update(IReadOnlyCollection<ProtoId<FeedbackPopupPrototype>> prototypes)
|
||||
{
|
||||
NotificationContainer.RemoveAllChildren();
|
||||
|
||||
if (prototypes.Count == 0)
|
||||
DisplayNoEntryLabel();
|
||||
|
||||
foreach (var proto in prototypes)
|
||||
{
|
||||
NotificationContainer.AddChild(new FeedbackEntry(proto, _proto, _uri));
|
||||
}
|
||||
|
||||
NumNotifications.Text = Loc.GetString("feedbackpopup-control-total-surveys", ("num", prototypes.Count));
|
||||
}
|
||||
|
||||
private void DisplayNoEntryLabel()
|
||||
{
|
||||
NotificationContainer.AddChild(new Label()
|
||||
{
|
||||
Text = Loc.GetString("feedbackpopup-control-no-entries"),
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,12 @@ using Content.Client.Chemistry.EntitySystems;
|
||||
using Content.Client.Guidebook.Richtext;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.ControlExtensions;
|
||||
using Content.Shared.Body.Prototypes;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Chemistry.Reaction;
|
||||
using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.Contraband;
|
||||
using Content.Shared.Localizations;
|
||||
using Content.Shared.Metabolism;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Graphics;
|
||||
@@ -132,17 +133,18 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
#region Effects
|
||||
if (_chemistryGuideData.ReagentGuideRegistry.TryGetValue(reagent.ID, out var guideEntryRegistry) &&
|
||||
guideEntryRegistry.GuideEntries != null &&
|
||||
guideEntryRegistry.GuideEntries.Values.Any(pair => pair.EffectDescriptions.Any()))
|
||||
guideEntryRegistry.GuideEntries.Values.Any(pair => pair.EffectDescriptions.Any() || pair.Metabolites?.Any() == true))
|
||||
{
|
||||
EffectsDescriptionContainer.Children.Clear();
|
||||
foreach (var (group, effect) in guideEntryRegistry.GuideEntries)
|
||||
foreach (var (stage, effect) in guideEntryRegistry.GuideEntries)
|
||||
{
|
||||
if (!effect.EffectDescriptions.Any())
|
||||
var hasMetabolites = effect.Metabolites?.Any() == true;
|
||||
if (!effect.EffectDescriptions.Any() && !hasMetabolites)
|
||||
continue;
|
||||
|
||||
var groupLabel = new RichTextLabel();
|
||||
groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-group-rate",
|
||||
("group", _prototype.Index<MetabolismGroupPrototype>(group).LocalizedName), ("rate", effect.MetabolismRate)));
|
||||
groupLabel.SetMarkup(Loc.GetString("guidebook-reagent-effects-metabolism-stage-rate",
|
||||
("stage", _prototype.Index<MetabolismStagePrototype>(stage).LocalizedName), ("rate", effect.MetabolismRate)));
|
||||
var descriptionLabel = new RichTextLabel
|
||||
{
|
||||
Margin = new Thickness(25, 0, 10, 0)
|
||||
@@ -155,9 +157,20 @@ public sealed partial class GuideReagentEmbed : BoxContainer, IDocumentTag, ISea
|
||||
{
|
||||
descMsg.AddMarkupOrThrow(effectString);
|
||||
i++;
|
||||
if (i < descriptionsCount)
|
||||
if (i < descriptionsCount || hasMetabolites)
|
||||
descMsg.PushNewline();
|
||||
}
|
||||
if (hasMetabolites)
|
||||
{
|
||||
var metabolites = new List<string>();
|
||||
foreach (var (metabolite, ratio) in effect.Metabolites!)
|
||||
{
|
||||
metabolites.Add(Loc.GetString("guidebook-reagent-effects-metabolite-item", ("rate", (double)ratio), ("reagent", _prototype.Index(metabolite).LocalizedName)));
|
||||
}
|
||||
metabolites.Sort();
|
||||
|
||||
descMsg.AddMarkupOrThrow(Loc.GetString("guidebook-reagent-effects-metabolites", ("items", ContentLocalizationManager.FormatList(metabolites))));
|
||||
}
|
||||
descriptionLabel.SetMessage(descMsg);
|
||||
|
||||
EffectsDescriptionContainer.AddChild(groupLabel);
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Content.Client.Guidebook.RichText;
|
||||
|
||||
/// <summary>
|
||||
/// RichText tag that can display values extracted from entity prototypes.
|
||||
/// In order to be accessed by this tag, the desired field/property must
|
||||
/// To be accessed by this tag, the desired field/property must
|
||||
/// be tagged with <see cref="Shared.Guidebook.GuidebookDataAttribute"/>.
|
||||
/// </summary>
|
||||
public sealed class ProtodataTag : IMarkupTagHandler
|
||||
|
||||
@@ -26,13 +26,13 @@ public sealed class HideableHumanoidLayersSystem : SharedHideableHumanoidLayersS
|
||||
UpdateSprite(ent);
|
||||
}
|
||||
|
||||
public override void SetLayerVisibility(
|
||||
public override void SetLayerOcclusion(
|
||||
Entity<HideableHumanoidLayersComponent?> ent,
|
||||
HumanoidVisualLayers layer,
|
||||
bool visible,
|
||||
SlotFlags source)
|
||||
{
|
||||
base.SetLayerVisibility(ent, layer, visible, source);
|
||||
base.SetLayerOcclusion(ent, layer, visible, source);
|
||||
|
||||
if (Resolve(ent, ref ent.Comp))
|
||||
UpdateSprite((ent, ent.Comp));
|
||||
|
||||
@@ -325,7 +325,7 @@ public sealed class MarkingsViewModel
|
||||
public void GetMarkingCounts(ProtoId<OrganCategoryPrototype> organ, HumanoidVisualLayers layer, out bool isRequired, out int count, out int selected)
|
||||
{
|
||||
isRequired = false;
|
||||
count = 0;
|
||||
count = -1;
|
||||
selected = 0;
|
||||
|
||||
if (!_organData.TryGetValue(organ, out var organData))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<DefaultWindow Title="{Loc 'instruments-component-channels-menu'}" MinSize="250 350" xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="true" VerticalExpand="true" Align="Center">
|
||||
<ItemList Name="ChannelList" SelectMode="Multiple" Margin="3 3 3 3" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="8"/>
|
||||
<BoxContainer Orientation="Horizontal" HorizontalExpand="true" VerticalExpand="true" Align="Center"
|
||||
@@ -7,7 +7,8 @@
|
||||
<Button Name="AllButton" Text="{Loc 'instruments-component-channels-all-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
<Button Name="ClearButton" Text="{Loc 'instruments-component-channels-clear-button'}" HorizontalExpand="true" VerticalExpand="true" SizeFlagsStretchRatio="1"/>
|
||||
</BoxContainer>
|
||||
<Button Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}" />
|
||||
<controls:SwitchButton Name="DisplayTrackNames"
|
||||
Text="{Loc 'instruments-component-channels-track-names-toggle'}"
|
||||
Margin="0 5 0 0" />
|
||||
</BoxContainer>
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -37,7 +37,6 @@ public sealed partial class ChannelsMenu : DefaultWindow
|
||||
|
||||
private void OnDisplayTrackNamesPressed(BaseButton.ButtonEventArgs obj)
|
||||
{
|
||||
DisplayTrackNames.SetClickPressed(!DisplayTrackNames.Pressed);
|
||||
Populate();
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using Content.Client.Clickable;
|
||||
using Content.Client.Corvax.TTS;
|
||||
using Content.Client.DebugMon;
|
||||
using Content.Client.Eui;
|
||||
using Content.Client.FeedbackPopup;
|
||||
using Content.Client.Fullscreen;
|
||||
using Content.Client.GameTicking.Managers;
|
||||
using Content.Client.GhostKick;
|
||||
@@ -24,6 +25,7 @@ using Content.Client.Lobby;
|
||||
using Content.Client.Players.RateLimiting;
|
||||
using Content.Shared.Administration.Managers;
|
||||
using Content.Shared.Chat;
|
||||
using Content.Shared.FeedbackSystem;
|
||||
using Content.Shared.IoC;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Players.RateLimiting;
|
||||
@@ -63,6 +65,8 @@ namespace Content.Client.IoC
|
||||
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
||||
collection.Register<TitleWindowManager>();
|
||||
collection.Register<ClientsidePlaytimeTrackingManager>();
|
||||
collection.Register<ClientFeedbackManager>();
|
||||
collection.Register<ISharedFeedbackManager, ClientFeedbackManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,7 @@
|
||||
Text="{Loc 'character-setup-gui-character-setup-close-button'}"
|
||||
StyleClasses="ButtonBig"/>
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="highlight">
|
||||
</PanelContainer>
|
||||
<PanelContainer StyleClasses="highlight" MinHeight="2" />
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True" SeparationOverride="0">
|
||||
<ScrollContainer MinSize="325 0" Margin="5 5 0 0">
|
||||
<BoxContainer Name="Characters" Orientation="Vertical" />
|
||||
|
||||
@@ -20,6 +20,9 @@ public sealed class MagicMirrorBoundUserInterface : BoundUserInterface
|
||||
base.Open();
|
||||
|
||||
_window = this.CreateWindow<MagicMirrorWindow>();
|
||||
|
||||
_window.Title = EntMan.GetComponent<MetaDataComponent>(Owner).EntityName;
|
||||
|
||||
_window.MarkingsPicker.SetModel(_markingsModel);
|
||||
|
||||
_markingsModel.MarkingsChanged += (_, _) =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<DefaultWindow xmlns="https://spacestation14.io"
|
||||
xmlns:humanoid="clr-namespace:Content.Client.Humanoid"
|
||||
Title="{Loc 'magic-mirror-window-title'}"
|
||||
MinSize="700 500">
|
||||
<humanoid:MarkingPicker Name="MarkingsPicker" Access="Public" HorizontalExpand="True" VerticalExpand="True" />
|
||||
</DefaultWindow>
|
||||
|
||||
@@ -216,7 +216,7 @@ public sealed partial class CryoPodWindow : FancyWindow
|
||||
|
||||
float? result = null;
|
||||
|
||||
foreach (var (_, metabolism) in reagentProto.Metabolisms)
|
||||
foreach (var (_, metabolism) in reagentProto.Metabolisms.Metabolisms)
|
||||
{
|
||||
foreach (var effect in metabolism.Effects)
|
||||
{
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<Button Access="Public" Name="GuidebookButton" Text="{Loc 'ui-escape-guidebook'}" />
|
||||
<Button Access="Public" Name="WikiButton" Text="{Loc 'ui-escape-wiki'}" />
|
||||
<changelog:ChangelogButton Access="Public" Name="ChangelogButton" />
|
||||
<Button Access="Public" Name="FeedbackButton" Text="{Loc 'ui-escape-feedback'}"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
|
||||
<Button Access="Public" Name="OptionsButton" Text="{Loc 'ui-escape-options'}" />
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 2.5 0 2.5" />
|
||||
|
||||
@@ -25,8 +25,8 @@ public sealed class JobRequirementsManager : ISharedPlaytimeManager
|
||||
[Dependency] private readonly IPrototypeManager _prototypes = default!;
|
||||
|
||||
private readonly Dictionary<string, TimeSpan> _roles = new();
|
||||
private readonly List<string> _jobBans = new();
|
||||
private readonly List<string> _antagBans = new();
|
||||
private readonly List<ProtoId<JobPrototype>> _jobBans = new();
|
||||
private readonly List<ProtoId<AntagPrototype>> _antagBans = new();
|
||||
private readonly List<string> _jobWhitelists = new();
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:style="clr-namespace:Content.Client.Stylesheets"
|
||||
Name="APCMenu"
|
||||
Title="{Loc 'apc-menu-title'}"
|
||||
Resizable="False">
|
||||
@@ -19,9 +18,7 @@
|
||||
<!-- Power On/Off -->
|
||||
<Label Text="{Loc 'apc-menu-breaker-label'}" HorizontalExpand="True"
|
||||
StyleClasses="highlight" MinWidth="120"/>
|
||||
<BoxContainer Orientation="Horizontal" MinWidth="150">
|
||||
<Button Name="BreakerButton" Text="{Loc 'apc-menu-breaker-button'}" HorizontalExpand="True" ToggleMode="True"/>
|
||||
</BoxContainer>
|
||||
<controls:SwitchButton Name="BreakerButton" MinWidth="150"/>
|
||||
<!--Charging Status-->
|
||||
<Label Text="{Loc 'apc-menu-external-label'}" StyleClasses="highlight" MinWidth="120" />
|
||||
<Label Name="ExternalPowerStateLabel" Text="{Loc 'apc-menu-power-state-good'}" />
|
||||
|
||||
@@ -33,10 +33,7 @@ namespace Content.Client.Power.APC.UI
|
||||
{
|
||||
var castState = (ApcBoundInterfaceState) state;
|
||||
|
||||
if (!BreakerButton.Disabled)
|
||||
{
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
}
|
||||
BreakerButton.Pressed = castState.MainBreaker;
|
||||
|
||||
if (PowerLabel != null)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
|
||||
namespace Content.Client.Power.EntitySystems;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class PowerStateSystem : SharedPowerStateSystem;
|
||||
@@ -30,6 +30,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
_window.OnDecalColorChanged += OnDecalColorChanged;
|
||||
_window.OnDecalAngleChanged += OnDecalAngleChanged;
|
||||
_window.OnDecalSnapChanged += OnDecalSnapChanged;
|
||||
_window.OnDecalColorPickerToggled += OnDecalColorPickerToggled;
|
||||
}
|
||||
|
||||
var sprayPainter = EntMan.System<SprayPainterSystem>();
|
||||
@@ -56,6 +57,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
|
||||
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
|
||||
_window.SetDecalSnap(sprayPainter.SnapDecals);
|
||||
_window.SetDecalColorPicker(sprayPainter.ColorPickerEnabled);
|
||||
}
|
||||
|
||||
private void OnDecalSnapChanged(bool snap)
|
||||
@@ -93,4 +95,9 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
|
||||
var key = _window?.IndexToColorKey(args.ItemIndex);
|
||||
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
|
||||
}
|
||||
|
||||
private void OnDecalColorPickerToggled(bool toggle)
|
||||
{
|
||||
SendPredictedMessage(new SprayPainterSetDecalColorPickerMessage(toggle));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,26 @@
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Text="{Loc 'spray-painter-selected-decals'}" />
|
||||
<BoxContainer>
|
||||
<Label Text="{Loc 'spray-painter-selected-decals'}" />
|
||||
<Label Name="SelectedDecalName" StyleClasses="Italic" Margin="4 0" />
|
||||
</BoxContainer>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<GridContainer Columns="7" Name="DecalsGrid">
|
||||
<!-- populated by code -->
|
||||
</GridContainer>
|
||||
</ScrollContainer>
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
|
||||
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True">
|
||||
<BoxContainer HorizontalAlignment="Right" VerticalAlignment="Top" SetHeight="29">
|
||||
<Button Name="ColorPalette" Text="Palette" />
|
||||
<Button Name="ColorPicker" ToggleMode="True">
|
||||
<TextureRect TexturePath="/Textures/Interface/eyedropper.svg.png" Stretch="KeepAspectCentered" SetSize="16 16" />
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
</ColorSelectorSliders>
|
||||
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
|
||||
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
|
||||
</BoxContainer>
|
||||
@@ -18,9 +29,13 @@
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
|
||||
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
|
||||
<Button Name="AddAngleButton" ToolTip="{Loc 'spray-painter-angle-rotation-90-add'}" SetSize="48 32">
|
||||
<TextureRect TexturePath="/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png" Stretch="KeepAspectCentered" SetSize="32 32" />
|
||||
</Button>
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
|
||||
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
|
||||
<Button Name="SubAngleButton" ToolTip="{Loc 'spray-painter-angle-rotation-90-sub'}" SetSize="48 32">
|
||||
<TextureRect TexturePath="/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png" Stretch="KeepAspectCentered" SetSize="32 32" />
|
||||
</Button>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:SprayPainterDecals>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Decals.UI;
|
||||
using Content.Client.Stylesheets;
|
||||
using Content.Shared.Decals;
|
||||
using Robust.Client.AutoGenerated;
|
||||
@@ -8,6 +8,8 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Content.Client.SprayPainter.UI;
|
||||
|
||||
@@ -21,6 +23,9 @@ public sealed partial class SprayPainterDecals : Control
|
||||
public Action<Color?>? OnColorChanged;
|
||||
public Action<int>? OnAngleChanged;
|
||||
public Action<bool>? OnSnapChanged;
|
||||
public Action<bool>? OnColorPickerToggled;
|
||||
|
||||
private PaletteColorPicker? _palette;
|
||||
|
||||
private SpriteSystem? _sprite;
|
||||
private string _selectedDecal = string.Empty;
|
||||
@@ -30,14 +35,17 @@ public sealed partial class SprayPainterDecals : Control
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
|
||||
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
|
||||
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360;
|
||||
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360;
|
||||
SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
|
||||
AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
|
||||
|
||||
UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
|
||||
SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
|
||||
ColorSelector.OnColorChanged += OnColorSelected;
|
||||
|
||||
ColorPalette.OnPressed += ColorPaletteOnPressed;
|
||||
ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed);
|
||||
}
|
||||
|
||||
private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
|
||||
@@ -147,6 +155,7 @@ public sealed partial class SprayPainterDecals : Control
|
||||
public void SetSelectedDecal(string name)
|
||||
{
|
||||
_selectedDecal = name;
|
||||
SelectedDecalName.Text = name;
|
||||
|
||||
if (_sprite is null)
|
||||
return;
|
||||
@@ -171,4 +180,35 @@ public sealed partial class SprayPainterDecals : Control
|
||||
{
|
||||
SnapToTileCheckBox.Pressed = snap;
|
||||
}
|
||||
|
||||
private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _)
|
||||
{
|
||||
// Code copied from other implementations of `PaletteColorPicker`.
|
||||
if (_palette is null)
|
||||
{
|
||||
_palette = new PaletteColorPicker();
|
||||
_palette.OpenCenteredLeft();
|
||||
_palette.PaletteList.OnItemSelected += args =>
|
||||
{
|
||||
var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value;
|
||||
ColorSelector.Color = color;
|
||||
OnColorSelected(color);
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
if (_palette.IsOpen)
|
||||
{
|
||||
_palette.Close();
|
||||
}
|
||||
else
|
||||
{
|
||||
_palette.Open();
|
||||
}
|
||||
}
|
||||
|
||||
public void SetColorPicker(bool enabled)
|
||||
{
|
||||
ColorPicker.Pressed = enabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
public event Action<Color?>? OnDecalColorChanged;
|
||||
public event Action<int>? OnDecalAngleChanged;
|
||||
public event Action<bool>? OnDecalSnapChanged;
|
||||
public event Action<bool>? OnDecalColorPickerToggled;
|
||||
|
||||
// Pipe color data
|
||||
private ItemList _colorList = default!;
|
||||
@@ -195,6 +196,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
|
||||
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
|
||||
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
|
||||
_sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle);
|
||||
|
||||
Tabs.AddChild(_sprayPainterDecals);
|
||||
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
|
||||
@@ -298,7 +300,12 @@ public sealed partial class SprayPainterWindow : DefaultWindow
|
||||
if (_sprayPainterDecals != null)
|
||||
_sprayPainterDecals.SetSnap(snap);
|
||||
}
|
||||
# endregion
|
||||
|
||||
public void SetDecalColorPicker(bool colorPickerEnabled)
|
||||
{
|
||||
_sprayPainterDecals?.SetColorPicker(colorPickerEnabled);
|
||||
}
|
||||
#endregion
|
||||
}
|
||||
|
||||
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
|
||||
|
||||
@@ -6,7 +6,7 @@ namespace Content.Client.Stylesheets;
|
||||
|
||||
public abstract class CommonStylesheet : PalettedStylesheet, IButtonConfig, IWindowConfig, IIconConfig, ITabContainerConfig,
|
||||
ISliderConfig, IRadialMenuConfig, IPlaceholderConfig, ITooltipConfig, IPanelConfig, INanoHeadingConfig,
|
||||
ILineEditConfig, IStripebackConfig, ICheckboxConfig
|
||||
ILineEditConfig, IStripebackConfig, ICheckboxConfig, ISwitchButtonConfig
|
||||
{
|
||||
/// <remarks>
|
||||
/// This constructor will not access any virtual or abstract properties, so you can set them from your config.
|
||||
@@ -73,4 +73,11 @@ public abstract class CommonStylesheet : PalettedStylesheet, IButtonConfig, IWin
|
||||
ColorPalette IButtonConfig.ButtonPalette => PrimaryPalette with { PressedElement = PositivePalette.PressedElement };
|
||||
ColorPalette IButtonConfig.PositiveButtonPalette => PositivePalette;
|
||||
ColorPalette IButtonConfig.NegativeButtonPalette => NegativePalette;
|
||||
|
||||
ResPath ISwitchButtonConfig.SwitchButtonTrackFillPath => new("switchbutton_track_fill.svg.96dpi.png");
|
||||
ResPath ISwitchButtonConfig.SwitchButtonTrackOutlinePath => new("switchbutton_track_outline.svg.96dpi.png");
|
||||
ResPath ISwitchButtonConfig.SwitchButtonThumbFillPath => new("switchbutton_thumb_fill.svg.96dpi.png");
|
||||
ResPath ISwitchButtonConfig.SwitchButtonThumbOutlinePath => new("switchbutton_thumb_outline.svg.96dpi.png");
|
||||
ResPath ISwitchButtonConfig.SwitchButtonSymbolOffPath => new("switchbutton_symbol_off.svg.96dpi.png");
|
||||
ResPath ISwitchButtonConfig.SwitchButtonSymbolOnPath => new("switchbutton_symbol_on.svg.96dpi.png");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Stylesheets.SheetletConfigs;
|
||||
|
||||
public interface ISwitchButtonConfig
|
||||
{
|
||||
public ResPath SwitchButtonTrackFillPath { get; }
|
||||
public ResPath SwitchButtonTrackOutlinePath { get; }
|
||||
public ResPath SwitchButtonThumbFillPath { get; }
|
||||
public ResPath SwitchButtonThumbOutlinePath { get; }
|
||||
public ResPath SwitchButtonSymbolOffPath { get; }
|
||||
public ResPath SwitchButtonSymbolOnPath { get; }
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed class ButtonSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
|
||||
|
||||
var crossTex = sheet.GetTextureOr(iconCfg.CrossIconPath, NanotrasenStylesheet.TextureRoot);
|
||||
var refreshTex = sheet.GetTextureOr(iconCfg.RefreshIconPath, NanotrasenStylesheet.TextureRoot);
|
||||
var helpTex = sheet.GetTextureOr(iconCfg.HelpIconPath, NanotrasenStylesheet.TextureRoot);
|
||||
|
||||
var rules = new List<StyleRule>
|
||||
{
|
||||
@@ -56,6 +57,11 @@ public sealed class ButtonSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet
|
||||
.Class(StyleClass.RefreshButton)
|
||||
.Prop(TextureButton.StylePropertyTexture, refreshTex),
|
||||
|
||||
// Help button
|
||||
E<TextureButton>()
|
||||
.Class(StyleClass.HelpButton)
|
||||
.Prop(TextureButton.StylePropertyTexture, helpTex),
|
||||
|
||||
// Ensure labels in buttons are aligned.
|
||||
E<Label>()
|
||||
// ReSharper disable once AccessToStaticMemberViaDerivedType
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
using Content.Client.Stylesheets.SheetletConfigs;
|
||||
using Content.Client.Stylesheets.Stylesheets;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using static Content.Client.Stylesheets.StylesheetHelpers;
|
||||
|
||||
namespace Content.Client.Stylesheets.Sheetlets;
|
||||
|
||||
[CommonSheetlet]
|
||||
public sealed class SwitchButtonSheetlet<T> : Sheetlet<T> where T : PalettedStylesheet, ISwitchButtonConfig
|
||||
{
|
||||
public override StyleRule[] GetRules(T sheet, object config)
|
||||
{
|
||||
ISwitchButtonConfig switchButtonCfg = sheet;
|
||||
|
||||
var trackFillTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonTrackFillPath, NanotrasenStylesheet.TextureRoot);
|
||||
var trackOutlineTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonTrackOutlinePath, NanotrasenStylesheet.TextureRoot);
|
||||
var thumbFillTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonThumbFillPath, NanotrasenStylesheet.TextureRoot);
|
||||
var thumbOutlineTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonThumbOutlinePath, NanotrasenStylesheet.TextureRoot);
|
||||
var symbolOffTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonSymbolOffPath, NanotrasenStylesheet.TextureRoot);
|
||||
var symbolOnTex = sheet.GetTextureOr(switchButtonCfg.SwitchButtonSymbolOnPath, NanotrasenStylesheet.TextureRoot);
|
||||
|
||||
return
|
||||
[
|
||||
// SwitchButton
|
||||
E<SwitchButton>().Prop(SwitchButton.StylePropertySeparation, 10),
|
||||
|
||||
E<SwitchButton>()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackFill))
|
||||
.Prop(TextureRect.StylePropertyTexture, trackFillTex)
|
||||
.Modulate(sheet.SecondaryPalette.BackgroundDark),
|
||||
|
||||
E<SwitchButton>()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackOutline))
|
||||
.Prop(TextureRect.StylePropertyTexture, trackOutlineTex)
|
||||
.Modulate(sheet.SecondaryPalette.Text),
|
||||
|
||||
E<SwitchButton>()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbFill))
|
||||
.Prop(TextureRect.StylePropertyTexture, thumbFillTex)
|
||||
.Modulate(sheet.PrimaryPalette.Element)
|
||||
.HorizontalAlignment(Control.HAlignment.Left),
|
||||
|
||||
E<SwitchButton>()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbOutline))
|
||||
.Prop(TextureRect.StylePropertyTexture, thumbOutlineTex)
|
||||
.Modulate(sheet.PrimaryPalette.Text)
|
||||
.HorizontalAlignment(Control.HAlignment.Left),
|
||||
|
||||
E<SwitchButton>()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassSymbol))
|
||||
.Prop(TextureRect.StylePropertyTexture, symbolOffTex)
|
||||
.Modulate(sheet.SecondaryPalette.Text),
|
||||
|
||||
// Pressed styles
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackFill))
|
||||
.Modulate(sheet.PositivePalette.Text),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassSymbol))
|
||||
.Prop(TextureRect.StylePropertyTexture, symbolOnTex)
|
||||
.Modulate(Color.White), // Same color as text, not yet in any of the palettes
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbFill))
|
||||
.HorizontalAlignment(Control.HAlignment.Right),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbOutline))
|
||||
.HorizontalAlignment(Control.HAlignment.Right),
|
||||
|
||||
// Disabled styles
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackFill))
|
||||
.Modulate(sheet.SecondaryPalette.DisabledElement),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackOutline))
|
||||
.Modulate(sheet.SecondaryPalette.DisabledElement),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbFill))
|
||||
.Modulate(sheet.PrimaryPalette.DisabledElement),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassThumbOutline))
|
||||
.Modulate(sheet.PrimaryPalette.TextDark),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassSymbol))
|
||||
.Modulate(sheet.SecondaryPalette.TextDark),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<Label>())
|
||||
.Modulate(sheet.PrimaryPalette.TextDark),
|
||||
|
||||
// Both pressed & disabled styles
|
||||
// Note that some of the pressed-only and disabled-only styles do not conflict
|
||||
// and will also be used
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassTrackFill))
|
||||
.Modulate(sheet.PositivePalette.DisabledElement),
|
||||
|
||||
E<SwitchButton>()
|
||||
.PseudoPressed()
|
||||
.PseudoDisabled()
|
||||
.ParentOf(E<TextureRect>().Class(SwitchButton.StyleClassSymbol))
|
||||
.Modulate(sheet.PositivePalette.Text),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ public static class StyleClass
|
||||
|
||||
public const string CrossButtonRed = "CrossButtonRed";
|
||||
public const string RefreshButton = "RefreshButton";
|
||||
public const string HelpButton = "HelpButton";
|
||||
|
||||
public const string ItemStatus = "ItemStatus";
|
||||
public const string ItemStatusNotHeld = "ItemStatusNotHeld";
|
||||
|
||||
@@ -84,8 +84,6 @@ namespace Content.Client.Stylesheets
|
||||
public const string StyleClassLabelSmall = "LabelSmall";
|
||||
public const string StyleClassButtonBig = "ButtonBig";
|
||||
|
||||
public const string StyleClassButtonHelp = "HelpButton";
|
||||
|
||||
public const string StyleClassPopupMessageSmall = "PopupMessageSmall";
|
||||
public const string StyleClassPopupMessageSmallCaution = "PopupMessageSmallCaution";
|
||||
public const string StyleClassPopupMessageMedium = "PopupMessageMedium";
|
||||
@@ -1341,17 +1339,12 @@ namespace Content.Client.Stylesheets
|
||||
new StyleProperty(PanelContainer.StylePropertyPanel, new StyleBoxFlat { BackgroundColor = NanoGold, ContentMarginBottomOverride = 2, ContentMarginLeftOverride = 2}),
|
||||
}),
|
||||
|
||||
Element<TextureButton>()
|
||||
.Class(StyleClassButtonHelp)
|
||||
.Prop(TextureButton.StylePropertyTexture, resCache.GetTexture("/Textures/Interface/VerbIcons/information.svg.192dpi.png")),
|
||||
|
||||
// Labels ---
|
||||
Element<Label>().Class(StyleClassLabelBig)
|
||||
.Prop(Label.StylePropertyFont, notoSans16),
|
||||
|
||||
Element<Label>().Class(StyleClassLabelSmall)
|
||||
.Prop(Label.StylePropertyFont, notoSans10),
|
||||
// ---
|
||||
|
||||
// Different Background shapes ---
|
||||
Element<PanelContainer>().Class(ClassAngleRect)
|
||||
@@ -1614,6 +1607,29 @@ namespace Content.Client.Stylesheets
|
||||
BackgroundColor = FancyTreeSelectedRowColor,
|
||||
}),
|
||||
|
||||
// Inset background (News manager, notifications)
|
||||
Element<PanelContainer>().Class("InsetBackground")
|
||||
.Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
|
||||
{
|
||||
BackgroundColor = Color.FromHex("#202023"),
|
||||
}),
|
||||
|
||||
// Default fancy window border styles
|
||||
Element<PanelContainer>().Class("DefaultBorderBottom")
|
||||
.Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
|
||||
{
|
||||
BorderColor= Color.FromHex("#3B3E56"),
|
||||
BorderThickness= new Thickness(0, 0, 0, 1),
|
||||
}),
|
||||
|
||||
|
||||
Element<PanelContainer>().Class("DefaultBorderTop")
|
||||
.Prop(PanelContainer.StylePropertyPanel, new StyleBoxFlat
|
||||
{
|
||||
BorderColor= Color.FromHex("#3B3E56"),
|
||||
BorderThickness= new Thickness(0, 1, 0, 0),
|
||||
}),
|
||||
|
||||
// Silicon law edit ui
|
||||
Element<Label>().Class(SiliconLawContainer.StyleClassSiliconLawPositionLabel)
|
||||
.Prop(Label.StylePropertyFontColor, NanoGold),
|
||||
|
||||
@@ -0,0 +1,317 @@
|
||||
using System.Numerics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of toggleable button that a switch icon and a secondary text label both showing the current state
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class SwitchButton : ContainerButton
|
||||
{
|
||||
public const string StyleClassTrackFill = "trackFill";
|
||||
public const string StyleClassTrackOutline = "trackOutline";
|
||||
public const string StyleClassThumbFill = "thumbFill";
|
||||
public const string StyleClassThumbOutline = "thumbOutline";
|
||||
public const string StyleClassSymbol = "symbol";
|
||||
|
||||
public const string StylePropertySeparation = "separation";
|
||||
|
||||
private const int DefaultSeparation = 0;
|
||||
|
||||
private int ActualSeparation
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TryGetStyleProperty(StylePropertySeparation, out int separation))
|
||||
{
|
||||
return separation;
|
||||
}
|
||||
|
||||
return SeparationOverride ?? DefaultSeparation;
|
||||
}
|
||||
}
|
||||
|
||||
public int? SeparationOverride { get; set; }
|
||||
public Label Label { get; }
|
||||
public Label OffStateLabel { get; }
|
||||
public Label OnStateLabel { get; }
|
||||
|
||||
// I tried to find a way not to have five textures here, but the other
|
||||
// options were worse.
|
||||
public TextureRect TrackFill { get; }
|
||||
public TextureRect TrackOutline { get; }
|
||||
public TextureRect ThumbFill { get; }
|
||||
public TextureRect ThumbOutline { get; }
|
||||
public TextureRect Symbol { get; }
|
||||
|
||||
public SwitchButton()
|
||||
{
|
||||
ToggleMode = true;
|
||||
|
||||
TrackFill = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassTrackFill },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
TrackOutline = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassTrackOutline },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
ThumbFill = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassThumbFill },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
ThumbOutline = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassThumbOutline },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
Symbol = new TextureRect
|
||||
{
|
||||
StyleClasses = { StyleClassSymbol },
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
};
|
||||
|
||||
Label = new Label();
|
||||
Label.Visible = false;
|
||||
|
||||
OffStateLabel = new Label();
|
||||
OffStateLabel.Text = Loc.GetString("toggle-switch-default-off-state-label");
|
||||
OffStateLabel.ReservesSpace = true;
|
||||
|
||||
OnStateLabel = new Label();
|
||||
OnStateLabel.Text = Loc.GetString("toggle-switch-default-on-state-label");
|
||||
OnStateLabel.ReservesSpace = true;
|
||||
OnStateLabel.Visible = false;
|
||||
|
||||
Label.HorizontalExpand = true;
|
||||
|
||||
AddChild(Label);
|
||||
AddChild(TrackFill);
|
||||
AddChild(TrackOutline);
|
||||
AddChild(ThumbFill);
|
||||
AddChild(ThumbOutline);
|
||||
AddChild(Symbol);
|
||||
AddChild(OffStateLabel);
|
||||
AddChild(OnStateLabel);
|
||||
}
|
||||
|
||||
protected override void DrawModeChanged()
|
||||
{
|
||||
// Workaround for child controls not being updated automatically.
|
||||
// Remove once https://github.com/space-wizards/RobustToolbox/pull/6264
|
||||
// or similar is merged.
|
||||
var relevantChangeMade = false;
|
||||
|
||||
if (Disabled)
|
||||
{
|
||||
if (!HasStylePseudoClass(StylePseudoClassDisabled))
|
||||
{
|
||||
AddStylePseudoClass(StylePseudoClassDisabled);
|
||||
relevantChangeMade = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HasStylePseudoClass(StylePseudoClassDisabled))
|
||||
{
|
||||
RemoveStylePseudoClass(StylePseudoClassDisabled);
|
||||
relevantChangeMade = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (Pressed)
|
||||
{
|
||||
if (!HasStylePseudoClass(StylePseudoClassPressed))
|
||||
{
|
||||
AddStylePseudoClass(StylePseudoClassPressed);
|
||||
relevantChangeMade = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (HasStylePseudoClass(StylePseudoClassPressed))
|
||||
{
|
||||
RemoveStylePseudoClass(StylePseudoClassPressed);
|
||||
relevantChangeMade = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (relevantChangeMade)
|
||||
{
|
||||
Label.RemoveStyleClass("dummy");
|
||||
TrackFill.RemoveStyleClass("dummy");
|
||||
TrackOutline.RemoveStyleClass("dummy");
|
||||
ThumbFill.RemoveStyleClass("dummy");
|
||||
ThumbOutline.RemoveStyleClass("dummy");
|
||||
Symbol.RemoveStyleClass("dummy");
|
||||
OffStateLabel.RemoveStyleClass("dummy");
|
||||
OnStateLabel.RemoveStyleClass("dummy");
|
||||
}
|
||||
|
||||
// no base.DrawModeChanged() call - ContainerButton's pseudoclass handling
|
||||
// doesn't support a button being both pressed and disabled
|
||||
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, the button will allow shrinking and clip text of the main
|
||||
/// label to prevent the text from going outside the bounds of the button.
|
||||
/// If false, the minimum size will always fit the contained text.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public bool ClipText { get => Label.ClipText; set => Label.ClipText = value; }
|
||||
|
||||
/// <summary>
|
||||
/// The text displayed by the button's main label.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? Text
|
||||
{
|
||||
get => Label.Text;
|
||||
set
|
||||
{
|
||||
Label.Text = value;
|
||||
Label.Visible = !string.IsNullOrEmpty(value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text displayed by the button's secondary label in the off state.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? OffStateText
|
||||
{
|
||||
get => OffStateLabel.Text;
|
||||
set => OffStateLabel.Text = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The text displayed by the button's secondary label in the on state.
|
||||
/// </summary>
|
||||
[ViewVariables]
|
||||
public string? OnStateText
|
||||
{
|
||||
get => OnStateLabel.Text;
|
||||
set => OnStateLabel.Text = value;
|
||||
}
|
||||
|
||||
private void UpdateAppearance()
|
||||
{
|
||||
if (OffStateLabel is not null)
|
||||
{
|
||||
OffStateLabel.Visible = !Pressed;
|
||||
}
|
||||
|
||||
if (OnStateLabel is not null)
|
||||
{
|
||||
OnStateLabel.Visible = Pressed;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void StylePropertiesChanged()
|
||||
{
|
||||
base.StylePropertiesChanged();
|
||||
UpdateAppearance();
|
||||
}
|
||||
|
||||
protected override Vector2 MeasureOverride(Vector2 availableSize)
|
||||
{
|
||||
var desiredSize = Vector2.Zero;
|
||||
var separation = ActualSeparation;
|
||||
|
||||
// Start with the icon, since it always appears
|
||||
if (TrackOutline is not null)
|
||||
{
|
||||
TrackOutline.Measure(availableSize);
|
||||
desiredSize = TrackOutline.DesiredSize;
|
||||
}
|
||||
|
||||
// Add space for the label if it has text
|
||||
if (! string.IsNullOrEmpty(Label?.Text))
|
||||
{
|
||||
Label.Measure(availableSize);
|
||||
desiredSize.X += separation + Label.DesiredSize.X;
|
||||
desiredSize.Y = float.Max(desiredSize.Y, Label.DesiredSize.Y);
|
||||
}
|
||||
|
||||
// Add space for the state labels if at least one of them has text
|
||||
var stateLabelSpace = Vector2.Zero;
|
||||
if (! string.IsNullOrEmpty(OffStateLabel?.Text))
|
||||
{
|
||||
OffStateLabel.Measure(availableSize);
|
||||
stateLabelSpace = OffStateLabel.DesiredSize;
|
||||
}
|
||||
|
||||
if (! string.IsNullOrEmpty(OnStateLabel?.Text))
|
||||
{
|
||||
OnStateLabel.Measure(availableSize);
|
||||
stateLabelSpace.Y = float.Max(stateLabelSpace.Y, OnStateLabel.DesiredSize.Y);
|
||||
stateLabelSpace.X = float.Max(stateLabelSpace.X, OnStateLabel.DesiredSize.X);
|
||||
}
|
||||
|
||||
if (stateLabelSpace != Vector2.Zero)
|
||||
{
|
||||
desiredSize.X += separation + stateLabelSpace.X;
|
||||
desiredSize.Y = float.Max(desiredSize.Y, stateLabelSpace.Y);
|
||||
}
|
||||
|
||||
return desiredSize;
|
||||
}
|
||||
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var separation = ActualSeparation;
|
||||
|
||||
var actualMainLabelWidth = finalSize.X - separation - TrackOutline.DesiredSize.X;
|
||||
float iconPosition = 0;
|
||||
float stateLabelPosition = 0;
|
||||
|
||||
if (string.IsNullOrEmpty(Label?.Text))
|
||||
{
|
||||
stateLabelPosition = TrackOutline.DesiredSize.X + separation;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrEmpty(OffStateLabel?.Text) || !string.IsNullOrEmpty(OnStateLabel?.Text))
|
||||
{
|
||||
var stateLabelsWidth = float.Max(OffStateLabel!.DesiredSize.X, OnStateLabel.DesiredSize.X);
|
||||
actualMainLabelWidth -= (separation + stateLabelsWidth);
|
||||
}
|
||||
actualMainLabelWidth = float.Max(actualMainLabelWidth, 0);
|
||||
iconPosition = actualMainLabelWidth + separation;
|
||||
stateLabelPosition = iconPosition + TrackOutline.DesiredSize.X + separation;
|
||||
}
|
||||
|
||||
var mainLabelTargetBox = new UIBox2(0, 0, actualMainLabelWidth, finalSize.Y);
|
||||
Label?.Arrange(mainLabelTargetBox);
|
||||
|
||||
var iconTargetBox = new UIBox2(iconPosition, 0, iconPosition + TrackOutline.DesiredSize.X, finalSize.Y);
|
||||
TrackFill.Arrange(iconTargetBox);
|
||||
TrackOutline.Arrange(iconTargetBox);
|
||||
Symbol.Arrange(iconTargetBox);
|
||||
|
||||
ThumbOutline.Measure(TrackOutline.DesiredSize); // didn't measure in MeasureOverride, don't need its size there
|
||||
var thumbLeft = iconTargetBox.Left;
|
||||
if (Pressed)
|
||||
thumbLeft = iconTargetBox.Right - ThumbOutline.DesiredSize.X;
|
||||
var thumbTargetBox = new UIBox2(thumbLeft, 0, thumbLeft + ThumbOutline.DesiredSize.X, finalSize.Y);
|
||||
ThumbFill.Arrange(thumbTargetBox);
|
||||
ThumbOutline.Arrange(thumbTargetBox);
|
||||
|
||||
var stateLabelsTargetBox = new UIBox2(stateLabelPosition, 0, finalSize.X, finalSize.Y);
|
||||
OffStateLabel?.Arrange(stateLabelsTargetBox);
|
||||
OnStateLabel?.Arrange(stateLabelsTargetBox);
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.FeedbackPopup;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Client.UserInterface.Systems.Guidebook;
|
||||
using Content.Client.UserInterface.Systems.Info;
|
||||
@@ -25,6 +26,7 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
[Dependency] private readonly InfoUIController _info = default!;
|
||||
[Dependency] private readonly OptionsUIController _options = default!;
|
||||
[Dependency] private readonly GuidebookUIController _guidebook = default!;
|
||||
[Dependency] private readonly FeedbackPopupUIController _feedback = null!;
|
||||
|
||||
private Options.UI.EscapeMenu? _escapeWindow;
|
||||
|
||||
@@ -63,6 +65,12 @@ public sealed class EscapeUIController : UIController, IOnStateEntered<GameplayS
|
||||
_escapeWindow.OnClose += DeactivateButton;
|
||||
_escapeWindow.OnOpen += ActivateButton;
|
||||
|
||||
_escapeWindow.FeedbackButton.OnPressed += _ =>
|
||||
{
|
||||
CloseEscapeWindow();
|
||||
_feedback.ToggleWindow();
|
||||
};
|
||||
|
||||
_escapeWindow.ChangelogButton.OnPressed += _ =>
|
||||
{
|
||||
CloseEscapeWindow();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Content.Client.UserInterface.Systems.Inventory.Controls;
|
||||
using Content.Shared.Hands.Components;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
|
||||
namespace Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
@@ -7,10 +9,10 @@ namespace Content.Client.UserInterface.Systems.Hands.Controls;
|
||||
public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
|
||||
{
|
||||
private readonly GridContainer _grid;
|
||||
public int ColumnLimit { get => _grid.Columns; set => _grid.Columns = value; }
|
||||
public int MaxButtonCount { get; set; } = 0;
|
||||
private readonly List<HandButton> _orderedButtons = new();
|
||||
public HandsComponent? PlayerHandsComponent;
|
||||
|
||||
public int MaxButtonsPerRow { get; set; }= 6;
|
||||
public int ColumnLimit { get; set; } = 6;
|
||||
|
||||
/// <summary>
|
||||
/// Indexer. This is used to reference a HandsContainer from the
|
||||
@@ -24,57 +26,32 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
|
||||
_grid.ExpandBackwards = true;
|
||||
}
|
||||
|
||||
public override HandButton? AddButton(HandButton newButton)
|
||||
protected override void AddButton(HandButton newButton)
|
||||
{
|
||||
if (MaxButtonCount > 0)
|
||||
{
|
||||
if (ButtonCount >= MaxButtonCount)
|
||||
return null;
|
||||
_orderedButtons.Add(newButton);
|
||||
|
||||
_grid.AddChild(newButton);
|
||||
}
|
||||
else
|
||||
_grid.RemoveAllChildren();
|
||||
var enumerable = PlayerHandsComponent?.SortedHands is { } sortedHands
|
||||
? _orderedButtons.OrderBy(it => sortedHands.IndexOf(it.SlotName))
|
||||
: _orderedButtons.OrderBy(it => it.HandLocation);
|
||||
foreach (var button in enumerable)
|
||||
{
|
||||
_grid.AddChild(newButton);
|
||||
_grid.AddChild(button);
|
||||
}
|
||||
|
||||
_grid.Columns = Math.Min(_grid.ChildCount, MaxButtonsPerRow);
|
||||
return base.AddButton(newButton);
|
||||
_grid.Columns = Math.Min(_grid.ChildCount, ColumnLimit);
|
||||
}
|
||||
|
||||
public override void RemoveButton(string handName)
|
||||
protected override void RemoveButton(HandButton button)
|
||||
{
|
||||
var button = GetButton(handName);
|
||||
if (button == null)
|
||||
return;
|
||||
base.RemoveButton(button);
|
||||
_orderedButtons.Remove(button);
|
||||
_grid.RemoveChild(button);
|
||||
}
|
||||
|
||||
public bool TryGetLastButton(out HandButton? control)
|
||||
public override void ClearButtons()
|
||||
{
|
||||
if (Buttons.Count == 0)
|
||||
{
|
||||
control = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
control = Buttons.Values.Last();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryRemoveLastHand(out HandButton? control)
|
||||
{
|
||||
var success = TryGetLastButton(out control);
|
||||
if (control != null)
|
||||
RemoveButton(control);
|
||||
return success;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ClearButtons();
|
||||
_grid.RemoveAllChildren();
|
||||
base.ClearButtons();
|
||||
_orderedButtons.Clear();
|
||||
}
|
||||
|
||||
public IEnumerable<HandButton> GetButtons()
|
||||
@@ -85,8 +62,4 @@ public sealed class HandsContainer : ItemSlotUIContainer<HandButton>
|
||||
yield return hand;
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsFull => (MaxButtonCount != 0 && ButtonCount >= MaxButtonCount);
|
||||
|
||||
public int ButtonCount => _grid.ChildCount;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using Content.Shared.Hands.Components;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Inventory.VirtualItem;
|
||||
using Content.Shared.Timing;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
@@ -26,9 +25,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
[UISystemDependency] private readonly HandsSystem _handsSystem = default!;
|
||||
[UISystemDependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
|
||||
private readonly List<HandsContainer> _handsContainers = new();
|
||||
private readonly Dictionary<string, int> _handContainerIndices = new();
|
||||
private readonly Dictionary<string, HandButton> _handLookup = new();
|
||||
private HandsComponent? _playerHandsComponent;
|
||||
private HandButton? _activeHand;
|
||||
|
||||
@@ -40,8 +36,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
private HandButton? _statusHandLeft;
|
||||
private HandButton? _statusHandRight;
|
||||
|
||||
private int _backupSuffix; //this is used when autogenerating container names if they don't have names
|
||||
|
||||
private HotbarGui? HandsGui => UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
|
||||
|
||||
public void OnSystemLoaded(HandsSystem system)
|
||||
@@ -119,25 +113,16 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
|
||||
private void UnloadPlayerHands()
|
||||
{
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = false;
|
||||
|
||||
_handContainerIndices.Clear();
|
||||
_handLookup.Clear();
|
||||
HandsGui?.Visible = false;
|
||||
HandsGui?.HandContainer.ClearButtons();
|
||||
_playerHandsComponent = null;
|
||||
|
||||
foreach (var container in _handsContainers)
|
||||
{
|
||||
container.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadPlayerHands(Entity<HandsComponent> handsComp)
|
||||
{
|
||||
DebugTools.Assert(_playerHandsComponent == null);
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = true;
|
||||
|
||||
HandsGui?.Visible = true;
|
||||
HandsGui?.HandContainer.PlayerHandsComponent = handsComp;
|
||||
_playerHandsComponent = handsComp;
|
||||
foreach (var (name, hand) in handsComp.Comp.Hands)
|
||||
{
|
||||
@@ -182,29 +167,18 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
|
||||
private void HandBlocked(string handName)
|
||||
{
|
||||
if (!_handLookup.TryGetValue(handName, out var hand))
|
||||
{
|
||||
if (HandsGui?.HandContainer.TryGetButton(handName, out var hand) != true)
|
||||
return;
|
||||
}
|
||||
|
||||
hand.Blocked = true;
|
||||
hand!.Blocked = true;
|
||||
}
|
||||
|
||||
private void HandUnblocked(string handName)
|
||||
{
|
||||
if (!_handLookup.TryGetValue(handName, out var hand))
|
||||
{
|
||||
if (HandsGui?.HandContainer.TryGetButton(handName, out var hand) != true)
|
||||
return;
|
||||
}
|
||||
|
||||
hand.Blocked = false;
|
||||
}
|
||||
|
||||
private int GetHandContainerIndex(string containerName)
|
||||
{
|
||||
if (!_handContainerIndices.TryGetValue(containerName, out var result))
|
||||
return -1;
|
||||
return result;
|
||||
hand!.Blocked = false;
|
||||
}
|
||||
|
||||
private void OnItemAdded(string name, EntityUid entity)
|
||||
@@ -243,7 +217,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), name, out var handData))
|
||||
{
|
||||
UpdateHandStatus(hand, null, handData);
|
||||
if (handData?.EmptyRepresentative is { } representative)
|
||||
if (handData.Value.EmptyRepresentative is { } representative)
|
||||
{
|
||||
SetRepresentative(hand, representative);
|
||||
return;
|
||||
@@ -253,30 +227,6 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
hand.SetEntity(null);
|
||||
}
|
||||
|
||||
private HandsContainer GetFirstAvailableContainer()
|
||||
{
|
||||
if (_handsContainers.Count == 0)
|
||||
throw new Exception("Could not find an attached hand hud container");
|
||||
foreach (var container in _handsContainers)
|
||||
{
|
||||
if (container.IsFull)
|
||||
continue;
|
||||
return container;
|
||||
}
|
||||
|
||||
throw new Exception("All attached hand hud containers were full!");
|
||||
}
|
||||
|
||||
public bool TryGetHandContainer(string containerName, out HandsContainer? container)
|
||||
{
|
||||
container = null;
|
||||
var containerIndex = GetHandContainerIndex(containerName);
|
||||
if (containerIndex == -1)
|
||||
return false;
|
||||
container = _handsContainers[containerIndex];
|
||||
return true;
|
||||
}
|
||||
|
||||
//propagate hand activation to the hand system.
|
||||
private void StorageActivate(GUIBoundKeyEventArgs args, SlotControl handControl)
|
||||
{
|
||||
@@ -293,24 +243,23 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_handLookup.TryGetValue(handName, out var handControl) || handControl == _activeHand)
|
||||
if (HandsGui?.HandContainer.TryGetButton(handName, out var handControl) != true || handControl == _activeHand)
|
||||
return;
|
||||
|
||||
if (_activeHand != null)
|
||||
_activeHand.Highlight = false;
|
||||
|
||||
handControl.Highlight = true;
|
||||
handControl!.Highlight = true;
|
||||
_activeHand = handControl;
|
||||
|
||||
if (HandsGui != null &&
|
||||
_playerHandsComponent != null &&
|
||||
if (_playerHandsComponent != null &&
|
||||
_player.LocalSession?.AttachedEntity is { } playerEntity &&
|
||||
_handsSystem.TryGetHand((playerEntity, _playerHandsComponent), handName, out var hand))
|
||||
{
|
||||
var heldEnt = _handsSystem.GetHeldItem((playerEntity, _playerHandsComponent), handName);
|
||||
|
||||
var foldedLocation = hand.Value.Location.GetUILocation();
|
||||
if (foldedLocation == HandUILocation.Left)
|
||||
var foldedLocation = hand.Value.Location;
|
||||
if (foldedLocation == HandLocation.Left)
|
||||
{
|
||||
_statusHandLeft = handControl;
|
||||
HandsGui.UpdatePanelEntityLeft(heldEnt, hand.Value);
|
||||
@@ -328,8 +277,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
|
||||
private HandButton? GetHand(string handName)
|
||||
{
|
||||
_handLookup.TryGetValue(handName, out var handControl);
|
||||
return handControl;
|
||||
return HandsGui?.HandContainer.GetButton(handName);
|
||||
}
|
||||
|
||||
private HandButton AddHand(string handName, Hand hand)
|
||||
@@ -338,17 +286,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
button.StoragePressed += StorageActivate;
|
||||
button.Pressed += HandPressed;
|
||||
|
||||
if (!_handLookup.TryAdd(handName, button))
|
||||
return _handLookup[handName];
|
||||
|
||||
if (HandsGui != null)
|
||||
{
|
||||
HandsGui.HandContainer.AddButton(button);
|
||||
}
|
||||
else
|
||||
{
|
||||
GetFirstAvailableContainer().AddButton(button);
|
||||
}
|
||||
HandsGui?.HandContainer.TryAddButton(button);
|
||||
|
||||
if (hand.EmptyRepresentative is { } representative)
|
||||
{
|
||||
@@ -359,7 +297,7 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
// If we don't have a status for this hand type yet, set it.
|
||||
// This means we have status filled by default in most scenarios,
|
||||
// otherwise the user'd need to switch hands to "activate" the hands the first time.
|
||||
if (hand.Location.GetUILocation() == HandUILocation.Left)
|
||||
if (hand.Location == HandLocation.Left)
|
||||
_statusHandLeft ??= button;
|
||||
else
|
||||
_statusHandRight ??= button;
|
||||
@@ -378,62 +316,17 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
_handsSystem.ReloadHandButtons();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Swap hands from one container to the other.
|
||||
/// </summary>
|
||||
/// <param name="other"></param>
|
||||
/// <param name="source"></param>
|
||||
public void SwapHands(HandsContainer other, HandsContainer? source = null)
|
||||
{
|
||||
if (HandsGui == null && source == null)
|
||||
{
|
||||
throw new ArgumentException("Cannot swap hands if no source hand container exists!");
|
||||
}
|
||||
|
||||
source ??= HandsGui!.HandContainer;
|
||||
|
||||
var transfer = new List<Control>();
|
||||
foreach (var child in source.Children)
|
||||
{
|
||||
if (child is not HandButton)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
transfer.Add(child);
|
||||
}
|
||||
|
||||
foreach (var control in transfer)
|
||||
{
|
||||
source.RemoveChild(control);
|
||||
other.AddChild(control);
|
||||
}
|
||||
}
|
||||
|
||||
private void RemoveHand(string handName)
|
||||
{
|
||||
RemoveHand(handName, out _);
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
private bool RemoveHand(string handName, out HandButton? handButton)
|
||||
{
|
||||
if (!_handLookup.TryGetValue(handName, out handButton))
|
||||
return false;
|
||||
if (handButton.Parent is HandsContainer handContainer)
|
||||
{
|
||||
handContainer.RemoveButton(handButton);
|
||||
}
|
||||
if (HandsGui?.HandContainer.TryRemoveButton(handName, out var handButton) != true)
|
||||
return;
|
||||
|
||||
if (_statusHandLeft == handButton)
|
||||
_statusHandLeft = null;
|
||||
if (_statusHandRight == handButton)
|
||||
_statusHandRight = null;
|
||||
|
||||
_handLookup.Remove(handName);
|
||||
handButton.Orphan();
|
||||
UpdateVisibleStatusPanels();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void UpdateVisibleStatusPanels()
|
||||
@@ -441,9 +334,12 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
var leftVisible = false;
|
||||
var rightVisible = false;
|
||||
|
||||
foreach (var hand in _handLookup.Values)
|
||||
if (HandsGui is null)
|
||||
return;
|
||||
|
||||
foreach (var hand in HandsGui.HandContainer.GetButtons())
|
||||
{
|
||||
if (hand.HandLocation.GetUILocation() == HandUILocation.Left)
|
||||
if (hand.HandLocation == HandLocation.Left)
|
||||
{
|
||||
leftVisible = true;
|
||||
}
|
||||
@@ -453,73 +349,34 @@ public sealed class HandsUIController : UIController, IOnStateEntered<GameplaySt
|
||||
}
|
||||
}
|
||||
|
||||
HandsGui?.UpdateStatusVisibility(leftVisible, rightVisible);
|
||||
}
|
||||
|
||||
public string RegisterHandContainer(HandsContainer handContainer)
|
||||
{
|
||||
var name = "HandContainer_" + _backupSuffix;
|
||||
|
||||
if (handContainer.Indexer == null)
|
||||
{
|
||||
handContainer.Indexer = name;
|
||||
_backupSuffix++;
|
||||
}
|
||||
else
|
||||
{
|
||||
name = handContainer.Indexer;
|
||||
}
|
||||
|
||||
_handContainerIndices.Add(name, _handsContainers.Count);
|
||||
_handsContainers.Add(handContainer);
|
||||
return name;
|
||||
}
|
||||
|
||||
public bool RemoveHandContainer(string handContainerName)
|
||||
{
|
||||
var index = GetHandContainerIndex(handContainerName);
|
||||
if (index == -1)
|
||||
return false;
|
||||
_handContainerIndices.Remove(handContainerName);
|
||||
_handsContainers.RemoveAt(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool RemoveHandContainer(string handContainerName, out HandsContainer? container)
|
||||
{
|
||||
var success = _handContainerIndices.TryGetValue(handContainerName, out var index);
|
||||
container = _handsContainers[index];
|
||||
_handContainerIndices.Remove(handContainerName);
|
||||
_handsContainers.RemoveAt(index);
|
||||
return success;
|
||||
HandsGui.UpdateStatusVisibility(leftVisible, rightVisible);
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
if (HandsGui != null)
|
||||
HandsGui.Visible = _playerHandsComponent != null;
|
||||
HandsGui?.Visible = _playerHandsComponent != null;
|
||||
}
|
||||
|
||||
public override void FrameUpdate(FrameEventArgs args)
|
||||
{
|
||||
base.FrameUpdate(args);
|
||||
|
||||
if (HandsGui is not { } handsGui)
|
||||
return;
|
||||
|
||||
// TODO this should be event based but 2 systems modify the same component differently for some reason
|
||||
foreach (var container in _handsContainers)
|
||||
foreach (var hand in handsGui.HandContainer.GetButtons())
|
||||
{
|
||||
foreach (var hand in container.GetButtons())
|
||||
|
||||
if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay))
|
||||
{
|
||||
|
||||
if (!_entities.TryGetComponent(hand.Entity, out UseDelayComponent? useDelay))
|
||||
{
|
||||
hand.CooldownDisplay.Visible = false;
|
||||
continue;
|
||||
}
|
||||
var delay = _useDelay.GetLastEndingDelay((hand.Entity.Value, useDelay));
|
||||
|
||||
hand.CooldownDisplay.Visible = true;
|
||||
hand.CooldownDisplay.FromTime(delay.StartTime, delay.EndTime);
|
||||
hand.CooldownDisplay.Visible = false;
|
||||
continue;
|
||||
}
|
||||
var delay = _useDelay.GetLastEndingDelay((hand.Entity.Value, useDelay));
|
||||
|
||||
hand.CooldownDisplay.Visible = true;
|
||||
hand.CooldownDisplay.FromTime(delay.StartTime, delay.EndTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ public sealed class HotbarUIController : UIController
|
||||
_inventory = UIManager.GetUIController<InventoryUIController>();
|
||||
_hands = UIManager.GetUIController<HandsUIController>();
|
||||
_storage = UIManager.GetUIController<StorageUIController>();
|
||||
_hands.RegisterHandContainer(handsContainer);
|
||||
}
|
||||
|
||||
public void ReloadHotbar()
|
||||
|
||||
@@ -11,8 +11,8 @@ public sealed partial class HotbarGui : UIWidget
|
||||
public HotbarGui()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
StatusPanelRight.SetSide(HandUILocation.Right);
|
||||
StatusPanelLeft.SetSide(HandUILocation.Left);
|
||||
StatusPanelRight.SetSide(HandLocation.Right);
|
||||
StatusPanelLeft.SetSide(HandLocation.Left);
|
||||
var hotbarController = UserInterfaceManager.GetUIController<HotbarUIController>();
|
||||
|
||||
hotbarController.Setup(HandContainer);
|
||||
@@ -29,10 +29,10 @@ public sealed partial class HotbarGui : UIWidget
|
||||
StatusPanelRight.Update(entity, hand);
|
||||
}
|
||||
|
||||
public void SetHighlightHand(HandUILocation? hand)
|
||||
public void SetHighlightHand(HandLocation? hand)
|
||||
{
|
||||
StatusPanelLeft.UpdateHighlight(hand is HandUILocation.Left);
|
||||
StatusPanelRight.UpdateHighlight(hand is HandUILocation.Right);
|
||||
StatusPanelLeft.UpdateHighlight(hand is HandLocation.Left);
|
||||
StatusPanelRight.UpdateHighlight(hand is HandLocation.Right);
|
||||
}
|
||||
|
||||
public void UpdateStatusVisibility(bool left, bool right)
|
||||
|
||||
@@ -14,54 +14,36 @@ public interface IItemslotUIContainer
|
||||
[Virtual]
|
||||
public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContainer where T : SlotControl
|
||||
{
|
||||
protected readonly Dictionary<string, T> Buttons = new();
|
||||
private readonly Dictionary<string, T> _buttons = new();
|
||||
|
||||
private int? _maxColumns;
|
||||
public int? MaxColumns { get; set; }
|
||||
|
||||
public int? MaxColumns
|
||||
public virtual void ClearButtons()
|
||||
{
|
||||
get => _maxColumns;
|
||||
set => _maxColumns = value;
|
||||
}
|
||||
|
||||
public virtual bool TryAddButton(T newButton, out T button)
|
||||
{
|
||||
var tempButton = AddButton(newButton);
|
||||
if (tempButton == null)
|
||||
foreach (var button in _buttons.Values)
|
||||
{
|
||||
button = newButton;
|
||||
return false;
|
||||
button.Orphan();
|
||||
}
|
||||
|
||||
button = newButton;
|
||||
return true;
|
||||
}
|
||||
|
||||
public void ClearButtons()
|
||||
{
|
||||
foreach (var button in Buttons.Values)
|
||||
{
|
||||
button.Dispose();
|
||||
}
|
||||
|
||||
Buttons.Clear();
|
||||
_buttons.Clear();
|
||||
}
|
||||
|
||||
public bool TryRegisterButton(SlotControl control, string newSlotName)
|
||||
{
|
||||
if (newSlotName == "")
|
||||
return false;
|
||||
if (!(control is T slotButton))
|
||||
if (control is not T slotButton)
|
||||
return false;
|
||||
if (Buttons.TryGetValue(newSlotName, out var foundButton))
|
||||
|
||||
if (_buttons.TryGetValue(newSlotName, out var foundButton))
|
||||
{
|
||||
if (control == foundButton)
|
||||
return true; //if the slotName is already set do nothing
|
||||
throw new Exception("Could not update button to slot:" + newSlotName + " slot already assigned!");
|
||||
}
|
||||
|
||||
Buttons.Remove(slotButton.SlotName);
|
||||
AddButton(slotButton);
|
||||
_buttons.Remove(slotButton.SlotName);
|
||||
TryAddButton(slotButton);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -69,69 +51,54 @@ public abstract class ItemSlotUIContainer<T> : GridContainer, IItemslotUIContain
|
||||
{
|
||||
if (control is not T newButton)
|
||||
return false;
|
||||
return AddButton(newButton) != null;
|
||||
return TryAddButton(newButton) != null;
|
||||
}
|
||||
|
||||
public virtual T? AddButton(T newButton)
|
||||
{
|
||||
if (!Children.Contains(newButton) && newButton.Parent == null && newButton.SlotName != "")
|
||||
AddChild(newButton);
|
||||
Columns = _maxColumns ?? ChildCount;
|
||||
return AddButtonToDict(newButton);
|
||||
}
|
||||
|
||||
protected virtual T? AddButtonToDict(T newButton)
|
||||
public T? TryAddButton(T newButton)
|
||||
{
|
||||
if (newButton.SlotName == "")
|
||||
{
|
||||
Logger.Warning("Could not add button " + newButton.Name + "No slotname");
|
||||
Log.Warning($"{newButton.Name} because it has no slot name");
|
||||
return null;
|
||||
}
|
||||
|
||||
return !Buttons.TryAdd(newButton.SlotName, newButton) ? null : newButton;
|
||||
if (Children.Contains(newButton) || newButton.Parent != null)
|
||||
return null;
|
||||
|
||||
if (!_buttons.TryAdd(newButton.SlotName, newButton))
|
||||
return null;
|
||||
|
||||
AddButton(newButton);
|
||||
return newButton;
|
||||
}
|
||||
|
||||
public virtual void RemoveButton(string slotName)
|
||||
protected virtual void AddButton(T newButton)
|
||||
{
|
||||
if (!Buttons.TryGetValue(slotName, out var button))
|
||||
return;
|
||||
AddChild(newButton);
|
||||
Columns = MaxColumns ?? ChildCount;
|
||||
}
|
||||
|
||||
public bool TryRemoveButton(string slotName, [NotNullWhen(true)] out T? button)
|
||||
{
|
||||
if (!_buttons.TryGetValue(slotName, out button))
|
||||
return false;
|
||||
|
||||
_buttons.Remove(button.SlotName);
|
||||
RemoveButton(button);
|
||||
return true;
|
||||
}
|
||||
|
||||
public virtual void RemoveButtons(params string[] slotNames)
|
||||
protected virtual void RemoveButton(T button)
|
||||
{
|
||||
foreach (var slotName in slotNames)
|
||||
{
|
||||
RemoveButton(slotName);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void RemoveButtons(params T?[] buttons)
|
||||
{
|
||||
foreach (var button in buttons)
|
||||
{
|
||||
if (button != null)
|
||||
RemoveButton(button);
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void RemoveButtonFromDict(T button)
|
||||
{
|
||||
Buttons.Remove(button.SlotName);
|
||||
}
|
||||
|
||||
public virtual void RemoveButton(T button)
|
||||
{
|
||||
RemoveButtonFromDict(button);
|
||||
Children.Remove(button);
|
||||
button.Dispose();
|
||||
}
|
||||
|
||||
public virtual T? GetButton(string slotName)
|
||||
public T? GetButton(string slotName)
|
||||
{
|
||||
return !Buttons.TryGetValue(slotName, out var button) ? null : button;
|
||||
return _buttons.GetValueOrDefault(slotName);
|
||||
}
|
||||
|
||||
public virtual bool TryGetButton(string slotName, [NotNullWhen(true)] out T? button)
|
||||
public bool TryGetButton(string slotName, [NotNullWhen(true)] out T? button)
|
||||
{
|
||||
return (button = GetButton(slotName)) != null;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed partial class ItemStatusPanel : Control
|
||||
[ViewVariables] private Hand? _hand;
|
||||
|
||||
// Tracked so we can re-run SetSide() if the theme changes.
|
||||
private HandUILocation _side;
|
||||
private HandLocation _side;
|
||||
|
||||
public ItemStatusPanel()
|
||||
{
|
||||
@@ -28,7 +28,7 @@ public sealed partial class ItemStatusPanel : Control
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
public void SetSide(HandUILocation location)
|
||||
public void SetSide(HandLocation location)
|
||||
{
|
||||
// AN IMPORTANT REMINDER ABOUT THIS CODE:
|
||||
// In the UI, the RIGHT hand is on the LEFT on the screen.
|
||||
@@ -43,14 +43,14 @@ public sealed partial class ItemStatusPanel : Control
|
||||
|
||||
switch (location)
|
||||
{
|
||||
case HandUILocation.Right:
|
||||
case HandLocation.Right or HandLocation.Middle:
|
||||
texture = Theme.ResolveTexture("item_status_right");
|
||||
textureHighlight = Theme.ResolveTexture("item_status_right_highlight");
|
||||
cutOut = StyleBox.Margin.Left;
|
||||
flat = StyleBox.Margin.Right;
|
||||
contentMargin = MarginFromThemeColor("_itemstatus_content_margin_right");
|
||||
break;
|
||||
case HandUILocation.Left:
|
||||
case HandLocation.Left:
|
||||
texture = Theme.ResolveTexture("item_status_left");
|
||||
textureHighlight = Theme.ResolveTexture("item_status_left_highlight");
|
||||
cutOut = StyleBox.Margin.Right;
|
||||
|
||||
@@ -147,7 +147,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
|
||||
if (!container.TryGetButton(data.SlotName, out var button))
|
||||
{
|
||||
button = CreateSlotButton(data);
|
||||
container.AddButton(button);
|
||||
container.TryAddButton(button);
|
||||
}
|
||||
|
||||
var showStorage = _entities.HasComponent<StorageComponent>(data.HeldEntity);
|
||||
@@ -373,7 +373,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
|
||||
return;
|
||||
|
||||
var button = CreateSlotButton(data);
|
||||
slotGroup.AddButton(button);
|
||||
slotGroup.TryAddButton(button);
|
||||
}
|
||||
|
||||
private void RemoveSlot(SlotData data)
|
||||
@@ -381,7 +381,7 @@ public sealed class InventoryUIController : UIController, IOnStateEntered<Gamepl
|
||||
if (!_slotGroups.TryGetValue(data.SlotGroup, out var slotGroup))
|
||||
return;
|
||||
|
||||
slotGroup.RemoveButton(data.SlotName);
|
||||
slotGroup.TryRemoveButton(data.SlotName, out _);
|
||||
}
|
||||
|
||||
public void ReloadSlots()
|
||||
|
||||
@@ -32,16 +32,6 @@ public sealed class GrapplingGunSystem : SharedGrapplingGunSystem
|
||||
if (!TryComp<GrapplingGunComponent>(handUid, out var grappling))
|
||||
return;
|
||||
|
||||
if (!TryComp<JointComponent>(handUid, out var jointComp) ||
|
||||
!jointComp.GetJoints.TryGetValue(GrapplingJoint, out var joint) ||
|
||||
joint is not DistanceJoint distance)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (distance.MaxLength <= distance.MinLength)
|
||||
return;
|
||||
|
||||
var reelKey = _input.CmdStates.GetState(EngineKeyFunctions.UseSecondary) == BoundKeyState.Down;
|
||||
|
||||
if (!TryComp<CombatModeComponent>(local, out var combatMode) ||
|
||||
|
||||
@@ -32,9 +32,9 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
// No bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty);
|
||||
});
|
||||
|
||||
// Try to pardon a ban that does not exist
|
||||
@@ -43,9 +43,9 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
// Still no bans on record
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Is.Empty);
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(1), Is.Null);
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Is.Empty);
|
||||
});
|
||||
|
||||
var banReason = "test";
|
||||
@@ -57,9 +57,9 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
// Should have one ban on record now
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
|
||||
await pair.RunTicksSync(5);
|
||||
@@ -70,17 +70,17 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 2"));
|
||||
|
||||
// The existing ban is unaffected
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Not.Null);
|
||||
|
||||
var ban = await sDatabase.GetServerBanAsync(1);
|
||||
var ban = await sDatabase.GetBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
Assert.That(ban, Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
|
||||
// Check that it matches
|
||||
Assert.That(ban.Id, Is.EqualTo(1));
|
||||
Assert.That(ban.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(ban.UserIds, Is.EquivalentTo([clientId]));
|
||||
Assert.That(ban.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(ban.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(ban.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
@@ -95,20 +95,20 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
await server.WaitPost(() => sConsole.ExecuteCommand("pardon 1"));
|
||||
|
||||
// No bans should be returned
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
|
||||
|
||||
// Direct id lookup returns a pardoned ban
|
||||
var pardonedBan = await sDatabase.GetServerBanAsync(1);
|
||||
var pardonedBan = await sDatabase.GetBanAsync(1);
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// Check that it matches
|
||||
Assert.That(pardonedBan, Is.Not.Null);
|
||||
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
|
||||
Assert.That(pardonedBan.Id, Is.EqualTo(1));
|
||||
Assert.That(pardonedBan.UserId, Is.EqualTo(clientId));
|
||||
Assert.That(pardonedBan.UserIds, Is.EquivalentTo([clientId]));
|
||||
Assert.That(pardonedBan.BanTime.UtcDateTime - DateTime.UtcNow, Is.LessThanOrEqualTo(MarginOfError));
|
||||
Assert.That(pardonedBan.ExpirationTime, Is.Not.Null);
|
||||
Assert.That(pardonedBan.ExpirationTime.Value.UtcDateTime - DateTime.UtcNow.AddHours(24), Is.LessThanOrEqualTo(MarginOfError));
|
||||
@@ -133,13 +133,13 @@ namespace Content.IntegrationTests.Tests.Commands
|
||||
Assert.Multiple(async () =>
|
||||
{
|
||||
// No bans should be returned
|
||||
Assert.That(await sDatabase.GetServerBanAsync(null, clientId, null, null), Is.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(null, clientId, null, null), Is.Null);
|
||||
|
||||
// Direct id lookup returns a pardoned ban
|
||||
Assert.That(await sDatabase.GetServerBanAsync(1), Is.Not.Null);
|
||||
Assert.That(await sDatabase.GetBanAsync(1), Is.Not.Null);
|
||||
|
||||
// The list is still returned since that ignores pardons
|
||||
Assert.That(await sDatabase.GetServerBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
Assert.That(await sDatabase.GetBansAsync(null, clientId, null, null), Has.Count.EqualTo(1));
|
||||
});
|
||||
|
||||
// Reconnect client. Slightly faster than dirtying the pair.
|
||||
|
||||
@@ -38,9 +38,9 @@ public sealed class RCDTest : InteractionTest
|
||||
pEast = Transform.WithEntityId(pEast, MapData.Grid);
|
||||
pWest = Transform.WithEntityId(pWest, MapData.Grid);
|
||||
|
||||
await SetTile(Plating, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
|
||||
await SetTile(Plating, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
|
||||
await SetTile(Plating, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
|
||||
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pNorth), MapData.Grid);
|
||||
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pSouth), MapData.Grid);
|
||||
await SetTile(PlatingRCD, SEntMan.GetNetCoordinates(pEast), MapData.Grid);
|
||||
await SetTile(Lattice, SEntMan.GetNetCoordinates(pWest), MapData.Grid);
|
||||
|
||||
Assert.That(ProtoMan.TryIndex(RCDSettingWall, out var settingWall), $"RCDPrototype not found: {RCDSettingWall}.");
|
||||
@@ -194,7 +194,7 @@ public sealed class RCDTest : InteractionTest
|
||||
// Deconstruct the steel tile.
|
||||
await Interact(null, pEast);
|
||||
await RunSeconds(settingDeconstructTile.Delay + 1); // wait for the deconstruction to finish
|
||||
await AssertTile(Lattice, FromServer(pEast));
|
||||
await AssertTile(PlatingRCD, FromServer(pEast));
|
||||
|
||||
// Check that the cost of the deconstruction was subtracted from the current charges.
|
||||
newCharges = sCharges.GetCurrentCharges(ToServer(rcd));
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
using System.Collections.Generic;
|
||||
using Content.IntegrationTests.Tests.Interaction;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Markings;
|
||||
using Content.Shared.Inventory;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Humanoid;
|
||||
|
||||
[TestOf(typeof(SharedHideableHumanoidLayersSystem))]
|
||||
public sealed class HideableHumanoidLayersTest : InteractionTest
|
||||
{
|
||||
protected override string PlayerPrototype => "MobVulpkanin";
|
||||
|
||||
[Test]
|
||||
public async Task BasicHiding()
|
||||
{
|
||||
await SpawnTarget("ClothingMaskGas");
|
||||
await Pickup(); // equip mask
|
||||
await UseInHand();
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var hideableHumanoidLayers = SEntMan.GetComponent<HideableHumanoidLayersComponent>(SPlayer);
|
||||
Assert.That(hideableHumanoidLayers.HiddenLayers, Does.ContainKey(HumanoidVisualLayers.Snout).WithValue(SlotFlags.MASK));
|
||||
});
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
SEntMan.DeleteEntity(STarget); // de-equip mask
|
||||
|
||||
var hideableHumanoidLayers = SEntMan.GetComponent<HideableHumanoidLayersComponent>(SPlayer);
|
||||
Assert.That(hideableHumanoidLayers.HiddenLayers, Does.Not.ContainKey(HumanoidVisualLayers.Snout));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task DependentHiding()
|
||||
{
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
var visualBody = SEntMan.System<SharedVisualBodySystem>();
|
||||
visualBody.ApplyMarkings(SPlayer, new()
|
||||
{
|
||||
["Head"] = new()
|
||||
{
|
||||
[HumanoidVisualLayers.SnoutCover] = new List<Marking>() { new("VulpSnoutNose", 1) },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await SpawnTarget("ClothingMaskGas");
|
||||
await Pickup(); // equip mask
|
||||
await UseInHand();
|
||||
|
||||
await RunTicks(20);
|
||||
|
||||
await Client.WaitAssertion(() =>
|
||||
{
|
||||
var spriteSystem = CEntMan.System<SpriteSystem>();
|
||||
var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout");
|
||||
var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose");
|
||||
var spriteComp = CEntMan.GetComponent<SpriteComponent>(CPlayer);
|
||||
|
||||
Assert.That(spriteComp[snoutIndex].Visible, Is.False);
|
||||
Assert.That(spriteComp[snoutCoverIndex].Visible, Is.False);
|
||||
});
|
||||
|
||||
await Server.WaitAssertion(() =>
|
||||
{
|
||||
SEntMan.DeleteEntity(STarget); // de-equip mask
|
||||
});
|
||||
|
||||
await RunTicks(20);
|
||||
|
||||
await Client.WaitAssertion(() =>
|
||||
{
|
||||
var spriteSystem = CEntMan.System<SpriteSystem>();
|
||||
var snoutIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnout-snout");
|
||||
var snoutCoverIndex = spriteSystem.LayerMapGet(CPlayer, "VulpSnoutNose-snout-nose");
|
||||
var spriteComp = CEntMan.GetComponent<SpriteComponent>(CPlayer);
|
||||
|
||||
Assert.That(spriteComp[snoutIndex].Visible, Is.True);
|
||||
Assert.That(spriteComp[snoutCoverIndex].Visible, Is.True);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.Body;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Humanoid;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Humanoid;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class HideablePrototypeValidation
|
||||
{
|
||||
[Test]
|
||||
public async Task NoOrgansWithoutClothing()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var requirements = new Dictionary<Enum, HashSet<EntProtoId>>();
|
||||
foreach (var (proto, component) in pair.GetPrototypesWithComponent<VisualOrganMarkingsComponent>())
|
||||
{
|
||||
foreach (var layer in component.HideableLayers)
|
||||
{
|
||||
requirements[layer] = requirements.GetValueOrDefault(layer) ?? [];
|
||||
requirements[layer].Add(proto.ID);
|
||||
}
|
||||
}
|
||||
|
||||
var provided = new HashSet<HumanoidVisualLayers>();
|
||||
foreach (var (_, component) in pair.GetPrototypesWithComponent<HideLayerClothingComponent>())
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
if (component.Slots is { } slots)
|
||||
{
|
||||
provided.UnionWith(slots);
|
||||
}
|
||||
provided.UnionWith(component.Layers.Keys);
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
}
|
||||
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
foreach (var (key, requirement) in requirements)
|
||||
{
|
||||
Assert.That(provided, Does.Contain(key), $"No clothing will hide {key} that can be hidden on {string.Join(", ", requirement.Select(it => it.Id))}");
|
||||
}
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task NoClothingWithoutOrgans()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
|
||||
var requirements = new Dictionary<Enum, HashSet<EntProtoId>>();
|
||||
foreach (var (proto, component) in pair.GetPrototypesWithComponent<HideLayerClothingComponent>())
|
||||
{
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
foreach (var layer in component.Layers.Keys.Concat(component.Slots ?? []))
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
{
|
||||
requirements[layer] = requirements.GetValueOrDefault(layer) ?? [];
|
||||
requirements[layer].Add(proto.ID);
|
||||
}
|
||||
}
|
||||
|
||||
var provided = new HashSet<Enum>();
|
||||
foreach (var (_, component) in pair.GetPrototypesWithComponent<VisualOrganMarkingsComponent>())
|
||||
{
|
||||
provided.UnionWith(component.HideableLayers);
|
||||
}
|
||||
|
||||
using var scope = Assert.EnterMultipleScope();
|
||||
foreach (var (key, requirement) in requirements)
|
||||
{
|
||||
Assert.That(provided, Does.Contain(key), $"No organ will hide {key} that can be hidden by {string.Join(", ", requirement.Select(it => it.Id))}");
|
||||
}
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Content.Shared.Humanoid;
|
||||
using Content.Shared.Humanoid.Prototypes;
|
||||
using Content.Shared.Preferences;
|
||||
using Content.Shared.Speech.Components;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Humanoid;
|
||||
|
||||
[TestFixture]
|
||||
[TestOf(typeof(HumanoidProfileSystem))]
|
||||
public sealed class HumanoidProfileTests
|
||||
{
|
||||
private static readonly ProtoId<SpeciesPrototype> Vox = "Vox";
|
||||
|
||||
[Test]
|
||||
public async Task EnsureValidLoading()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
await server.WaitIdleAsync();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
var entityManager = server.ResolveDependency<IEntityManager>();
|
||||
var humanoidProfile = entityManager.System<HumanoidProfileSystem>();
|
||||
var human = entityManager.Spawn("MobHuman");
|
||||
humanoidProfile.ApplyProfileTo(human, new HumanoidCharacterProfile()
|
||||
.WithSex(Sex.Female)
|
||||
.WithAge(67)
|
||||
.WithGender(Gender.Neuter)
|
||||
.WithSpecies(Vox));
|
||||
var humanoidComponent = entityManager.GetComponent<HumanoidProfileComponent>(human);
|
||||
var voiceComponent = entityManager.GetComponent<VocalComponent>(human);
|
||||
|
||||
Assert.That(humanoidComponent.Age, Is.EqualTo(67));
|
||||
Assert.That(humanoidComponent.Sex, Is.EqualTo(Sex.Female));
|
||||
Assert.That(humanoidComponent.Gender, Is.EqualTo(Gender.Neuter));
|
||||
Assert.That(humanoidComponent.Species, Is.EqualTo(Vox));
|
||||
|
||||
Assert.That(voiceComponent.Sounds, Is.Not.Null, message: "the MobHuman spawned by this test needs to have sex-specific sound set");
|
||||
Assert.That(voiceComponent.Sounds![Sex.Female], Is.EqualTo(voiceComponent.EmoteSounds));
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,9 @@ public abstract partial class InteractionTest
|
||||
protected const string Floor = "FloorSteel";
|
||||
protected const string FloorItem = "FloorTileItemSteel";
|
||||
protected const string Plating = "Plating";
|
||||
protected const string PlatingRCD = "PlatingRCD";
|
||||
protected const string Lattice = "Lattice";
|
||||
protected const string PlatingBrass = "PlatingBrass";
|
||||
|
||||
// Structures
|
||||
protected const string Airlock = "Airlock";
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Power.Components;
|
||||
using Content.Shared.Power.Components;
|
||||
using Content.Shared.Power.EntitySystems;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Power;
|
||||
|
||||
[TestFixture, TestOf(typeof(SharedPowerStateSystem))]
|
||||
public sealed class PowerStatePrototypeTest
|
||||
{
|
||||
/// <summary>
|
||||
/// Asserts that the <see cref="SharedApcPowerReceiverComponent"/>'s load is the same
|
||||
/// as the idle or working power draw from <see cref="PowerStateComponent"/>,
|
||||
/// depending on the current power state.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task AssertApcPowerMatchesPowerState()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var server = pair.Server;
|
||||
|
||||
var protoMan = server.ResolveDependency<IPrototypeManager>();
|
||||
var entMan = server.ResolveDependency<IEntityManager>();
|
||||
|
||||
await server.WaitAssertion(() =>
|
||||
{
|
||||
Assert.Multiple(delegate
|
||||
{
|
||||
foreach (var prototype in protoMan.EnumeratePrototypes<EntityPrototype>()
|
||||
.Where(p => !p.Abstract)
|
||||
.Where(p => !pair.IsTestPrototype(p)))
|
||||
{
|
||||
if (!prototype.TryGetComponent<PowerStateComponent>(out var powerStateComp, entMan.ComponentFactory))
|
||||
continue;
|
||||
|
||||
// LESSON LEARNED:
|
||||
// ENSURE THAT THE COMPONENT YOU ARE TRYING TO GET IS THE SERVER-SIDE VARIANT
|
||||
if (!prototype.TryGetComponent<ApcPowerReceiverComponent>(out var powerReceiverComp, entMan.ComponentFactory))
|
||||
{
|
||||
Assert.Fail(
|
||||
$"Entity prototype '{prototype.ID}' has a PowerStateComponent but is missing the required ApcPowerReceiverComponent.");
|
||||
}
|
||||
|
||||
var expectedLoad = powerStateComp.IsWorking
|
||||
? powerStateComp.WorkingPowerDraw
|
||||
: powerStateComp.IdlePowerDraw;
|
||||
|
||||
Assert.That(powerReceiverComp.Load,
|
||||
Is.EqualTo(expectedLoad),
|
||||
$"Entity prototype '{prototype.ID}' has mismatched power draw between PowerStateComponent and SharedApcPowerReceiverComponent.");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ public sealed class PowerStateTest
|
||||
Assert.That(receiver.Load, Is.EqualTo(powerState.IdlePowerDraw).Within(0.01f));
|
||||
});
|
||||
|
||||
var system = entManager.System<PowerStateSystem>();
|
||||
var system = entManager.System<SharedPowerStateSystem>();
|
||||
system.SetWorkingState((ent, powerState), true);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
@@ -93,7 +93,7 @@ public sealed class PowerStateTest
|
||||
|
||||
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
|
||||
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
|
||||
var system = entManager.System<PowerStateSystem>();
|
||||
var system = entManager.System<SharedPowerStateSystem>();
|
||||
Entity<PowerStateComponent> newEnt = (ent, powerState);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
@@ -146,7 +146,7 @@ public sealed class PowerStateTest
|
||||
|
||||
var receiver = entManager.GetComponent<Server.Power.Components.ApcPowerReceiverComponent>(ent);
|
||||
var powerState = entManager.GetComponent<PowerStateComponent>(ent);
|
||||
var system = entManager.System<PowerStateSystem>();
|
||||
var system = entManager.System<SharedPowerStateSystem>();
|
||||
Entity<PowerStateComponent> valueTuple = (ent, powerState);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
|
||||
@@ -100,4 +100,25 @@ public sealed class TileConstructionTests : InteractionTest
|
||||
|
||||
await AssertEntityLookup((FloorItem, 1));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test brassPlating -> floor -> brassPlating using tilestacking
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task BrassPlatingPlace()
|
||||
{
|
||||
await SetTile(PlatingBrass);
|
||||
|
||||
// Brass Plating -> Tile
|
||||
await InteractUsing(FloorItem);
|
||||
Assert.That(HandSys.GetActiveItem((SEntMan.GetEntity(Player), Hands)), Is.Null);
|
||||
await AssertTile(Floor);
|
||||
AssertGridCount(1);
|
||||
|
||||
// Tile -> Brass Plating
|
||||
await InteractUsing(Pry);
|
||||
await AssertTile(PlatingBrass);
|
||||
AssertGridCount(1);
|
||||
await AssertEntityLookup((FloorItem, 1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Maps;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests.Tiles;
|
||||
|
||||
public sealed class TileStackRecursionTest
|
||||
{
|
||||
[Test]
|
||||
public async Task TestBaseTurfRecursion()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var protoMan = pair.Server.ResolveDependency<IPrototypeManager>();
|
||||
var cfg = pair.Server.ResolveDependency<IConfigurationManager>();
|
||||
var maxTileHistoryLength = cfg.GetCVar(CCVars.TileStackLimit);
|
||||
Assert.That(protoMan.TryGetInstances<ContentTileDefinition>(out var tiles));
|
||||
Assert.That(tiles, Is.Not.EqualTo(null));
|
||||
//store the distance from the root node to the given tile node
|
||||
var nodes = new List<(ProtoId<ContentTileDefinition>, int)>();
|
||||
//each element of list is a connection from BaseTurf tile to tile that goes on it
|
||||
var edges = new List<(ProtoId<ContentTileDefinition>, ProtoId<ContentTileDefinition>)>();
|
||||
foreach (var ctdef in tiles!.Values)
|
||||
{
|
||||
//at first, each node is unexplored and has infinite distance to root.
|
||||
//we use space node as root - everything is supposed to start at space, and it's hardcoded into the game anyway.
|
||||
if (ctdef.ID == ContentTileDefinition.SpaceID)
|
||||
{
|
||||
nodes.Insert(0, (ctdef.ID, 0)); //space is the first element
|
||||
continue;
|
||||
}
|
||||
Assert.That(ctdef.BaseTurf != ctdef.ID);
|
||||
nodes.Add((ctdef.ID, int.MaxValue));
|
||||
if (ctdef.BaseTurf != null)
|
||||
edges.Add((ctdef.BaseTurf.Value, ctdef.ID));
|
||||
Assert.That(ctdef.BaseWhitelist, Does.Not.Contain(ctdef.ID));
|
||||
edges.AddRange(ctdef.BaseWhitelist.Select(possibleTurf =>
|
||||
(possibleTurf, new ProtoId<ContentTileDefinition>(ctdef.ID))));
|
||||
}
|
||||
Bfs(nodes, edges, maxTileHistoryLength);
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
|
||||
private void Bfs(List<(ProtoId<ContentTileDefinition>, int)> nodes, List<(ProtoId<ContentTileDefinition>, ProtoId<ContentTileDefinition>)> edges, int depthLimit)
|
||||
{
|
||||
var root = nodes[0];
|
||||
var queue = new Queue<(ProtoId<ContentTileDefinition>, int)>();
|
||||
queue.Enqueue(root);
|
||||
while (queue.Count != 0)
|
||||
{
|
||||
var u = queue.Dequeue();
|
||||
//get a list of tiles that can be put on this tile
|
||||
var adj = edges.Where(n => n.Item1 == u.Item1).Select(n => n.Item2);
|
||||
var adjNodes = nodes.Where(n => adj.Contains(n.Item1)).ToList();
|
||||
foreach (var node in adjNodes)
|
||||
{
|
||||
var adjNode = node;
|
||||
adjNode.Item2 = u.Item2 + 1;
|
||||
Assert.That(adjNode.Item2, Is.LessThanOrEqualTo(depthLimit)); //we can doomstack tiles on top of each other. Bad!
|
||||
queue.Enqueue(adjNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+2125
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using Content.Shared.Database;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using NpgsqlTypes;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class BanRefactor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban",
|
||||
columns: table => new
|
||||
{
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
type = table.Column<byte>(type: "smallint", nullable: false),
|
||||
playtime_at_note = table.Column<TimeSpan>(type: "interval", nullable: false),
|
||||
ban_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
expiration_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
reason = table.Column<string>(type: "text", nullable: false),
|
||||
severity = table.Column<int>(type: "integer", nullable: false),
|
||||
banning_admin = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
last_edited_by_id = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
last_edited_at = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||
exempt_flags = table.Column<int>(type: "integer", nullable: false),
|
||||
auto_delete = table.Column<bool>(type: "boolean", nullable: false),
|
||||
hidden = table.Column<bool>(type: "boolean", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban", x => x.ban_id);
|
||||
table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_banning_admin",
|
||||
column: x => x.banning_admin,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_last_edited_by_id",
|
||||
column: x => x.last_edited_by_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_address",
|
||||
columns: table => new
|
||||
{
|
||||
ban_address_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
address = table.Column<NpgsqlInet>(type: "inet", nullable: false),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_address", x => x.ban_address_id);
|
||||
table.CheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_address_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_hwid",
|
||||
columns: table => new
|
||||
{
|
||||
ban_hwid_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
hwid = table.Column<byte[]>(type: "bytea", nullable: false),
|
||||
hwid_type = table.Column<int>(type: "integer", nullable: false),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_hwid_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_player",
|
||||
columns: table => new
|
||||
{
|
||||
ban_player_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
user_id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_player", x => x.ban_player_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_role",
|
||||
columns: table => new
|
||||
{
|
||||
ban_role_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
role_type = table.Column<string>(type: "text", nullable: false),
|
||||
role_id = table.Column<string>(type: "text", nullable: false),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_role", x => x.ban_role_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_role_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_round",
|
||||
columns: table => new
|
||||
{
|
||||
ban_round_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false),
|
||||
round_id = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_round", x => x.ban_round_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_round_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_round_round_round_id",
|
||||
column: x => x.round_id,
|
||||
principalTable: "round",
|
||||
principalColumn: "round_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "unban",
|
||||
columns: table => new
|
||||
{
|
||||
unban_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ban_id = table.Column<int>(type: "integer", nullable: false),
|
||||
unbanning_admin = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
unban_time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_unban", x => x.unban_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_unban_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_banning_admin",
|
||||
table: "ban",
|
||||
column: "banning_admin");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_last_edited_by_id",
|
||||
table: "ban",
|
||||
column: "last_edited_by_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_address_ban_id",
|
||||
table: "ban_address",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_hwid_ban_id",
|
||||
table: "ban_hwid",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_player_ban_id",
|
||||
table: "ban_player",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_player_user_id_ban_id",
|
||||
table: "ban_player",
|
||||
columns: new[] { "user_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_role_ban_id",
|
||||
table: "ban_role",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_role_role_type_role_id_ban_id",
|
||||
table: "ban_role",
|
||||
columns: new[] { "role_type", "role_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_round_ban_id",
|
||||
table: "ban_round",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_round_round_id_ban_id",
|
||||
table: "ban_round",
|
||||
columns: new[] { "round_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_unban_ban_id",
|
||||
table: "unban",
|
||||
column: "ban_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_server_ban_hit_ban_ban_id",
|
||||
table: "server_ban_hit",
|
||||
column: "ban_id",
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
CREATE INDEX "IX_ban_address_address"
|
||||
ON ban_address
|
||||
USING gist
|
||||
(address inet_ops)
|
||||
INCLUDE (ban_id);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id"
|
||||
ON ban_hwid
|
||||
(hwid_type, hwid, ban_id);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ban_address_address_ban_id"
|
||||
ON ban_address
|
||||
(address, ban_id);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql($"""
|
||||
-- REMOVE:
|
||||
-- TRUNCATE ban RESTART IDENTITY CASCADE;
|
||||
|
||||
--
|
||||
-- Insert game bans
|
||||
--
|
||||
INSERT INTO
|
||||
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
|
||||
SELECT
|
||||
server_ban_id, {(int)BanType.Server}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden
|
||||
FROM
|
||||
server_ban;
|
||||
|
||||
-- Update ID sequence to be after newly inserted IDs.
|
||||
SELECT setval('ban_ban_id_seq', (SELECT MAX(ban_id) FROM ban));
|
||||
|
||||
-- Insert ban player records.
|
||||
INSERT INTO
|
||||
ban_player (user_id, ban_id)
|
||||
SELECT
|
||||
player_user_id, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
player_user_id IS NOT NULL;
|
||||
|
||||
-- Insert ban address records.
|
||||
INSERT INTO
|
||||
ban_address (address, ban_id)
|
||||
SELECT
|
||||
address, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
address IS NOT NULL;
|
||||
|
||||
-- Insert ban HWID records.
|
||||
INSERT INTO
|
||||
ban_hwid (hwid, hwid_type, ban_id)
|
||||
SELECT
|
||||
hwid, hwid_type, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
hwid IS NOT NULL;
|
||||
|
||||
-- Insert ban unban records.
|
||||
INSERT INTO
|
||||
unban (ban_id, unbanning_admin, unban_time)
|
||||
SELECT
|
||||
ban_id, unbanning_admin, unban_time
|
||||
FROM server_unban;
|
||||
|
||||
|
||||
-- Insert ban round records.
|
||||
INSERT INTO
|
||||
ban_round (round_id, ban_id)
|
||||
SELECT
|
||||
round_id, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
round_id IS NOT NULL;
|
||||
|
||||
--
|
||||
-- Insert role bans
|
||||
-- This shit is a pain in the ass
|
||||
-- > Declarative language
|
||||
-- > Has to write procedural code in it
|
||||
--
|
||||
|
||||
-- Create mapping table from role ban -> server ban.
|
||||
-- We have to manually calculate the new ban IDs by using the sequence.
|
||||
-- We also want to merge role ban records because the game code previously did that in some UI,
|
||||
-- and that code is now gone, expecting the DB to do it.
|
||||
|
||||
-- Create a table to store IDs to merge.
|
||||
CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE) ON COMMIT DROP;
|
||||
-- TRUNCATE _role_ban_import_merge_map;
|
||||
|
||||
-- Create a table to store merged IDs -> new ban IDs
|
||||
CREATE TEMPORARY TABLE /*IF NOT EXISTS*/ _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE) ON COMMIT DROP;
|
||||
-- TRUNCATE _role_ban_import_id_map;
|
||||
|
||||
-- Calculate merged role bans.
|
||||
INSERT INTO
|
||||
_role_ban_import_merge_map
|
||||
SELECT
|
||||
(
|
||||
SELECT
|
||||
sub.server_role_ban_id
|
||||
FROM
|
||||
server_role_ban AS sub
|
||||
LEFT JOIN server_role_unban AS sub_unban
|
||||
ON sub_unban.ban_id = sub.server_role_ban_id
|
||||
WHERE
|
||||
main.reason IS NOT DISTINCT FROM sub.reason
|
||||
AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id
|
||||
AND main.address IS NOT DISTINCT FROM sub.address
|
||||
AND main.hwid IS NOT DISTINCT FROM sub.hwid
|
||||
AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type
|
||||
AND date_trunc('second', main.ban_time, 'utc') = date_trunc('second', sub.ban_time, 'utc')
|
||||
AND (
|
||||
(main.expiration_time IS NULL) = (sub.expiration_time IS NULL)
|
||||
OR date_trunc('minute', main.expiration_time, 'utc') = date_trunc('minute', sub.expiration_time, 'utc')
|
||||
)
|
||||
AND main.round_id IS NOT DISTINCT FROM sub.round_id
|
||||
AND main.severity IS NOT DISTINCT FROM sub.severity
|
||||
AND main.hidden IS NOT DISTINCT FROM sub.hidden
|
||||
AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin
|
||||
AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL)
|
||||
ORDER BY
|
||||
sub.server_role_ban_id ASC
|
||||
LIMIT 1
|
||||
), main.server_role_ban_id
|
||||
FROM
|
||||
server_role_ban AS main
|
||||
LEFT JOIN server_role_unban AS main_unban
|
||||
ON main_unban.ban_id = main.server_role_ban_id;
|
||||
|
||||
-- Assign new ban IDs for merged IDs.
|
||||
INSERT INTO
|
||||
_role_ban_import_id_map
|
||||
SELECT
|
||||
DISTINCT ON (merge_id)
|
||||
nextval('ban_ban_id_seq'),
|
||||
merge_id
|
||||
FROM
|
||||
_role_ban_import_merge_map;
|
||||
|
||||
-- I sure fucking wish CTEs could span multiple queries...
|
||||
|
||||
-- Insert new ban records
|
||||
INSERT INTO
|
||||
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
|
||||
SELECT
|
||||
im.ban_id, {(int)BanType.Role}, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id;
|
||||
|
||||
-- Insert role ban player records.
|
||||
INSERT INTO
|
||||
ban_player (user_id, ban_id)
|
||||
SELECT
|
||||
player_user_id, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND player_user_id IS NOT NULL;
|
||||
|
||||
-- Insert role ban address records.
|
||||
INSERT INTO
|
||||
ban_address (address, ban_id)
|
||||
SELECT
|
||||
address, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND address IS NOT NULL;
|
||||
|
||||
-- Insert role ban HWID records.
|
||||
INSERT INTO
|
||||
ban_hwid (hwid, hwid_type, ban_id)
|
||||
SELECT
|
||||
hwid, hwid_type, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND hwid IS NOT NULL;
|
||||
|
||||
-- Insert role ban role records.
|
||||
INSERT INTO
|
||||
ban_role (role_type, role_id, ban_id)
|
||||
SELECT
|
||||
split_part(role_id, ':', 1), split_part(role_id, ':', 2), im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = mm.server_role_ban_id
|
||||
-- Yes, we have some messy ban records which, after merging, end up with duplicate roles.
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert role unban records.
|
||||
INSERT INTO
|
||||
unban (ban_id, unbanning_admin, unban_time)
|
||||
SELECT
|
||||
im.ban_id, unbanning_admin, unban_time
|
||||
FROM server_role_unban sru
|
||||
INNER JOIN _role_ban_import_id_map im
|
||||
ON im.merge_id = sru.ban_id;
|
||||
|
||||
-- Insert role rounds
|
||||
INSERT INTO
|
||||
ban_round (round_id, ban_id)
|
||||
SELECT
|
||||
round_id, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND round_id IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_server_ban_hit_server_ban_ban_id",
|
||||
table: "server_ban_hit");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_role_unban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_unban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_role_ban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_ban");
|
||||
|
||||
migrationBuilder.Sql($"""
|
||||
CREATE OR REPLACE FUNCTION send_server_ban_notification()
|
||||
RETURNS trigger AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify(
|
||||
'ban_notification',
|
||||
json_build_object('ban_id', NEW.ban_id)::text
|
||||
);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER notify_on_server_ban_insert
|
||||
AFTER INSERT ON ban
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.type = {(int)BanType.Server})
|
||||
EXECUTE FUNCTION send_server_ban_notification();
|
||||
""");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
throw new NotSupportedException("This migration cannot be rolled back");
|
||||
}
|
||||
}
|
||||
}
|
||||
+393
-389
@@ -519,6 +519,221 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("assigned_user_id", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Ban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<bool>("AutoDelete")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("auto_delete");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<int>("ExemptFlags")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("exempt_flags");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("interval")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<byte>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_address_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<NpgsqlInet>("Address")
|
||||
.HasColumnType("inet")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_hwid_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_player_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<Guid>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_role_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_round_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
@@ -1076,95 +1291,6 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("server", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("server_ban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<NpgsqlInet?>("Address")
|
||||
.HasColumnType("inet")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<bool>("AutoDelete")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("auto_delete");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<int>("ExemptFlags")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("exempt_flags");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<Guid?>("PlayerUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("interval")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_ban");
|
||||
|
||||
b.HasIndex("Address");
|
||||
|
||||
b.HasIndex("BanningAdmin");
|
||||
|
||||
b.HasIndex("LastEditedById");
|
||||
|
||||
b.HasIndex("PlayerUserId")
|
||||
.HasDatabaseName("IX_server_ban_player_user_id");
|
||||
|
||||
b.HasIndex("RoundId")
|
||||
.HasDatabaseName("IX_server_ban_round_id");
|
||||
|
||||
b.ToTable("server_ban", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
|
||||
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
@@ -1214,152 +1340,6 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("server_ban_hit", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("server_role_ban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<NpgsqlInet?>("Address")
|
||||
.HasColumnType("inet")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<Guid?>("PlayerUserId")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("interval")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_role_ban");
|
||||
|
||||
b.HasIndex("Address");
|
||||
|
||||
b.HasIndex("BanningAdmin");
|
||||
|
||||
b.HasIndex("LastEditedById");
|
||||
|
||||
b.HasIndex("PlayerUserId")
|
||||
.HasDatabaseName("IX_server_role_ban_player_user_id");
|
||||
|
||||
b.HasIndex("RoundId")
|
||||
.HasDatabaseName("IX_server_role_ban_round_id");
|
||||
|
||||
b.ToTable("server_role_ban", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address");
|
||||
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("role_unban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("UnbanningAdmin")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("unbanning_admin");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_role_unban");
|
||||
|
||||
b.HasIndex("BanId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("server_role_unban", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("unban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("UnbanningAdmin")
|
||||
.HasColumnType("uuid")
|
||||
.HasColumnName("unbanning_admin");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_unban");
|
||||
|
||||
b.HasIndex("BanId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("server_unban", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Trait", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1387,6 +1367,36 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.ToTable("trait", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Unban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("unban_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("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<int>("Id")
|
||||
@@ -1671,6 +1681,123 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
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<int>("BanHwidId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ban_hwid_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("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")
|
||||
@@ -1827,70 +1954,14 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("Server");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "CreatedBy")
|
||||
.WithMany("AdminServerBansCreated")
|
||||
.HasForeignKey("BanningAdmin")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_ban_player_banning_admin");
|
||||
|
||||
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
|
||||
.WithMany("AdminServerBansLastEdited")
|
||||
.HasForeignKey("LastEditedById")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_ban_player_last_edited_by_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.Round", "Round")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoundId")
|
||||
.HasConstraintName("FK_server_ban_round_round_id");
|
||||
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
|
||||
{
|
||||
b1.Property<int>("ServerBanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("server_ban_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("hwid_type");
|
||||
|
||||
b1.HasKey("ServerBanId");
|
||||
|
||||
b1.ToTable("server_ban");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ServerBanId")
|
||||
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
|
||||
});
|
||||
|
||||
b.Navigation("CreatedBy");
|
||||
|
||||
b.Navigation("HWId");
|
||||
|
||||
b.Navigation("LastEditedBy");
|
||||
|
||||
b.Navigation("Round");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerBan", "Ban")
|
||||
b.HasOne("Content.Server.Database.Ban", "Ban")
|
||||
.WithMany("BanHits")
|
||||
.HasForeignKey("BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
|
||||
.HasConstraintName("FK_server_ban_hit_ban_ban_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
|
||||
.WithMany("BanHits")
|
||||
@@ -1904,86 +1975,6 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
b.Navigation("Connection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "CreatedBy")
|
||||
.WithMany("AdminServerRoleBansCreated")
|
||||
.HasForeignKey("BanningAdmin")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_role_ban_player_banning_admin");
|
||||
|
||||
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
|
||||
.WithMany("AdminServerRoleBansLastEdited")
|
||||
.HasForeignKey("LastEditedById")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.Round", "Round")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoundId")
|
||||
.HasConstraintName("FK_server_role_ban_round_round_id");
|
||||
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
|
||||
{
|
||||
b1.Property<int>("ServerRoleBanId")
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("server_role_ban_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("hwid_type");
|
||||
|
||||
b1.HasKey("ServerRoleBanId");
|
||||
|
||||
b1.ToTable("server_role_ban");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ServerRoleBanId")
|
||||
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
|
||||
});
|
||||
|
||||
b.Navigation("CreatedBy");
|
||||
|
||||
b.Navigation("HWId");
|
||||
|
||||
b.Navigation("LastEditedBy");
|
||||
|
||||
b.Navigation("Round");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
|
||||
.WithOne("Unban")
|
||||
.HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
|
||||
|
||||
b.Navigation("Ban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerBan", "Ban")
|
||||
.WithOne("Unban")
|
||||
.HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_unban_server_ban_ban_id");
|
||||
|
||||
b.Navigation("Ban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Trait", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
@@ -1996,6 +1987,18 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
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)
|
||||
@@ -2030,6 +2033,23 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
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");
|
||||
@@ -2059,10 +2079,6 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
|
||||
b.Navigation("AdminServerBansLastEdited");
|
||||
|
||||
b.Navigation("AdminServerRoleBansCreated");
|
||||
|
||||
b.Navigation("AdminServerRoleBansLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsCreated");
|
||||
|
||||
b.Navigation("AdminWatchlistsDeleted");
|
||||
@@ -2111,18 +2127,6 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
|
||||
b.Navigation("Rounds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.Navigation("BanHits");
|
||||
|
||||
b.Navigation("Unban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.Navigation("Unban");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
+2044
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,498 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class BanRefactor : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban",
|
||||
columns: table => new
|
||||
{
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
type = table.Column<byte>(type: "INTEGER", nullable: false),
|
||||
playtime_at_note = table.Column<TimeSpan>(type: "TEXT", nullable: false),
|
||||
ban_time = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
expiration_time = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
reason = table.Column<string>(type: "TEXT", nullable: false),
|
||||
severity = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
banning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
last_edited_by_id = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
last_edited_at = table.Column<DateTime>(type: "TEXT", nullable: true),
|
||||
exempt_flags = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
auto_delete = table.Column<bool>(type: "INTEGER", nullable: false),
|
||||
hidden = table.Column<bool>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban", x => x.ban_id);
|
||||
table.CheckConstraint("NoExemptOnRoleBan", "type = 0 OR exempt_flags = 0");
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_banning_admin",
|
||||
column: x => x.banning_admin,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_last_edited_by_id",
|
||||
column: x => x.last_edited_by_id,
|
||||
principalTable: "player",
|
||||
principalColumn: "user_id",
|
||||
onDelete: ReferentialAction.SetNull);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_address",
|
||||
columns: table => new
|
||||
{
|
||||
ban_address_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_address", x => x.ban_address_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_address_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_hwid",
|
||||
columns: table => new
|
||||
{
|
||||
ban_hwid_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
hwid = table.Column<byte[]>(type: "BLOB", nullable: false),
|
||||
hwid_type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_hwid", x => x.ban_hwid_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_hwid_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_player",
|
||||
columns: table => new
|
||||
{
|
||||
ban_player_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
user_id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_player", x => x.ban_player_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_player_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_role",
|
||||
columns: table => new
|
||||
{
|
||||
ban_role_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
role_type = table.Column<string>(type: "TEXT", nullable: false),
|
||||
role_id = table.Column<string>(type: "TEXT", nullable: false),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_role", x => x.ban_role_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_role_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ban_round",
|
||||
columns: table => new
|
||||
{
|
||||
ban_round_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
round_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ban_round", x => x.ban_round_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_round_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
table.ForeignKey(
|
||||
name: "FK_ban_round_round_round_id",
|
||||
column: x => x.round_id,
|
||||
principalTable: "round",
|
||||
principalColumn: "round_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "unban",
|
||||
columns: table => new
|
||||
{
|
||||
unban_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
ban_id = table.Column<int>(type: "INTEGER", nullable: false),
|
||||
unbanning_admin = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
unban_time = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_unban", x => x.unban_id);
|
||||
table.ForeignKey(
|
||||
name: "FK_unban_ban_ban_id",
|
||||
column: x => x.ban_id,
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_banning_admin",
|
||||
table: "ban",
|
||||
column: "banning_admin");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_last_edited_by_id",
|
||||
table: "ban",
|
||||
column: "last_edited_by_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_address_ban_id",
|
||||
table: "ban_address",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_hwid_ban_id",
|
||||
table: "ban_hwid",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_player_ban_id",
|
||||
table: "ban_player",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_player_user_id_ban_id",
|
||||
table: "ban_player",
|
||||
columns: new[] { "user_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_role_ban_id",
|
||||
table: "ban_role",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_role_role_type_role_id_ban_id",
|
||||
table: "ban_role",
|
||||
columns: new[] { "role_type", "role_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_round_ban_id",
|
||||
table: "ban_round",
|
||||
column: "ban_id");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ban_round_round_id_ban_id",
|
||||
table: "ban_round",
|
||||
columns: new[] { "round_id", "ban_id" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_unban_ban_id",
|
||||
table: "unban",
|
||||
column: "ban_id",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
CREATE UNIQUE INDEX "IX_ban_hwid_hwid_ban_id"
|
||||
ON ban_hwid
|
||||
(hwid_type, hwid, ban_id);
|
||||
|
||||
CREATE UNIQUE INDEX "IX_ban_address_address_ban_id"
|
||||
ON ban_address
|
||||
(address, ban_id);
|
||||
""");
|
||||
|
||||
migrationBuilder.Sql("""
|
||||
--
|
||||
-- Insert game bans
|
||||
--
|
||||
INSERT INTO
|
||||
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
|
||||
SELECT
|
||||
server_ban_id, 0, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden
|
||||
FROM
|
||||
server_ban;
|
||||
|
||||
-- Insert ban player records.
|
||||
INSERT INTO
|
||||
ban_player (user_id, ban_id)
|
||||
SELECT
|
||||
player_user_id, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
player_user_id IS NOT NULL;
|
||||
|
||||
-- Insert ban address records.
|
||||
INSERT INTO
|
||||
ban_address (address, ban_id)
|
||||
SELECT
|
||||
address, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
address IS NOT NULL;
|
||||
|
||||
-- Insert ban HWID records.
|
||||
INSERT INTO
|
||||
ban_hwid (hwid, hwid_type, ban_id)
|
||||
SELECT
|
||||
hwid, hwid_type, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
hwid IS NOT NULL;
|
||||
|
||||
-- Insert ban unban records.
|
||||
INSERT INTO
|
||||
unban (ban_id, unbanning_admin, unban_time)
|
||||
SELECT
|
||||
ban_id, unbanning_admin, unban_time
|
||||
FROM server_unban;
|
||||
|
||||
-- Insert ban round records.
|
||||
INSERT INTO
|
||||
ban_round (round_id, ban_id)
|
||||
SELECT
|
||||
round_id, server_ban_id
|
||||
FROM
|
||||
server_ban
|
||||
WHERE
|
||||
round_id IS NOT NULL;
|
||||
|
||||
--
|
||||
-- Insert role bans
|
||||
-- This shit is a pain in the ass
|
||||
-- > Declarative language
|
||||
-- > Has to write procedural code in it
|
||||
--
|
||||
|
||||
-- Create mapping table from role ban -> server ban.
|
||||
-- We have to manually calculate the new ban IDs by using the sequence.
|
||||
-- We also want to merge role ban records because the game code previously did that in some UI,
|
||||
-- and that code is now gone, expecting the DB to do it.
|
||||
|
||||
-- Create a table to store IDs to merge.
|
||||
CREATE TEMPORARY TABLE _role_ban_import_merge_map (merge_id INTEGER, server_role_ban_id INTEGER UNIQUE);
|
||||
|
||||
-- Create a table to store merged IDs -> new ban IDs
|
||||
CREATE TEMPORARY TABLE _role_ban_import_id_map (ban_id INTEGER UNIQUE, merge_id INTEGER UNIQUE);
|
||||
|
||||
-- Calculate merged role bans.
|
||||
INSERT INTO
|
||||
_role_ban_import_merge_map
|
||||
SELECT
|
||||
(
|
||||
SELECT
|
||||
sub.server_role_ban_id
|
||||
FROM
|
||||
server_role_ban AS sub
|
||||
LEFT JOIN server_role_unban AS sub_unban
|
||||
ON sub_unban.ban_id = sub.server_role_ban_id
|
||||
WHERE
|
||||
main.reason IS NOT DISTINCT FROM sub.reason
|
||||
AND main.player_user_id IS NOT DISTINCT FROM sub.player_user_id
|
||||
AND main.address IS NOT DISTINCT FROM sub.address
|
||||
AND main.hwid IS NOT DISTINCT FROM sub.hwid
|
||||
AND main.hwid_type IS NOT DISTINCT FROM sub.hwid_type
|
||||
AND main.ban_time = sub.ban_time
|
||||
AND (
|
||||
(main.expiration_time IS NULL) = (sub.expiration_time IS NULL)
|
||||
OR main.expiration_time = sub.expiration_time
|
||||
)
|
||||
AND main.round_id IS NOT DISTINCT FROM sub.round_id
|
||||
AND main.severity IS NOT DISTINCT FROM sub.severity
|
||||
AND main.hidden IS NOT DISTINCT FROM sub.hidden
|
||||
AND main.banning_admin IS NOT DISTINCT FROM sub.banning_admin
|
||||
AND (sub_unban.ban_id IS NULL) = (main_unban.ban_id IS NULL)
|
||||
ORDER BY
|
||||
sub.server_role_ban_id ASC
|
||||
LIMIT 1
|
||||
), main.server_role_ban_id
|
||||
FROM
|
||||
server_role_ban AS main
|
||||
LEFT JOIN server_role_unban AS main_unban
|
||||
ON main_unban.ban_id = main.server_role_ban_id;
|
||||
|
||||
-- Assign new ban IDs for merged IDs.
|
||||
INSERT OR IGNORE INTO
|
||||
_role_ban_import_id_map
|
||||
SELECT
|
||||
merge_id + (SELECT seq FROM sqlite_sequence WHERE name = 'ban'),
|
||||
merge_id
|
||||
FROM
|
||||
_role_ban_import_merge_map;
|
||||
|
||||
-- I sure fucking wish CTEs could span multiple queries...
|
||||
|
||||
-- Insert new ban records
|
||||
INSERT INTO
|
||||
ban (ban_id, type, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, exempt_flags, auto_delete, hidden)
|
||||
SELECT
|
||||
im.ban_id, 1, playtime_at_note, ban_time, expiration_time, reason, severity, banning_admin, last_edited_by_id, last_edited_at, 0, FALSE, hidden
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id;
|
||||
|
||||
-- Insert role ban player records.
|
||||
INSERT INTO
|
||||
ban_player (user_id, ban_id)
|
||||
SELECT
|
||||
player_user_id, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND player_user_id IS NOT NULL;
|
||||
|
||||
-- Insert role ban address records.
|
||||
INSERT INTO
|
||||
ban_address (address, ban_id)
|
||||
SELECT
|
||||
address, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND address IS NOT NULL;
|
||||
|
||||
-- Insert role ban HWID records.
|
||||
INSERT INTO
|
||||
ban_hwid (hwid, hwid_type, ban_id)
|
||||
SELECT
|
||||
hwid, hwid_type, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND hwid IS NOT NULL;
|
||||
|
||||
-- Insert role ban role records.
|
||||
INSERT INTO
|
||||
ban_role (role_type, role_id, ban_id)
|
||||
SELECT
|
||||
substr(role_id, 1, instr(role_id, ':')-1),
|
||||
substr(role_id, instr(role_id, ':')+1),
|
||||
im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = mm.server_role_ban_id
|
||||
-- Yes, we have some messy ban records which, after merging, end up with duplicate roles.
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Insert role unban records.
|
||||
INSERT INTO
|
||||
unban (ban_id, unbanning_admin, unban_time)
|
||||
SELECT
|
||||
im.ban_id, unbanning_admin, unban_time
|
||||
FROM server_role_unban sru
|
||||
INNER JOIN _role_ban_import_id_map im
|
||||
ON im.merge_id = sru.ban_id;
|
||||
|
||||
-- Insert role rounds
|
||||
INSERT INTO
|
||||
ban_round (round_id, ban_id)
|
||||
SELECT
|
||||
round_id, im.ban_id
|
||||
FROM
|
||||
_role_ban_import_id_map im
|
||||
INNER JOIN _role_ban_import_merge_map mm
|
||||
ON im.merge_id = mm.merge_id
|
||||
INNER JOIN server_role_ban srb
|
||||
ON srb.server_role_ban_id = im.merge_id
|
||||
WHERE mm.merge_id = mm.server_role_ban_id
|
||||
AND round_id IS NOT NULL;
|
||||
""");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_server_ban_hit_ban_ban_id",
|
||||
table: "server_ban_hit",
|
||||
column: "ban_id",
|
||||
principalTable: "ban",
|
||||
principalColumn: "ban_id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_server_ban_hit_server_ban_ban_id",
|
||||
table: "server_ban_hit");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_role_unban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_unban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_role_ban");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "server_ban");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
throw new NotSupportedException("This migration cannot be rolled back");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -489,6 +489,207 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("assigned_user_id", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Ban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<bool>("AutoDelete")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("auto_delete");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<int>("ExemptFlags")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exempt_flags");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.Property<byte>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_address_id");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_hwid_id");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_player_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<Guid>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_role_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<string>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_round_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<int>("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<int>("Id")
|
||||
@@ -1017,91 +1218,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("server", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("server_ban_id");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<bool>("AutoDelete")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("auto_delete");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<int>("ExemptFlags")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("exempt_flags");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<Guid?>("PlayerUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_ban");
|
||||
|
||||
b.HasIndex("Address");
|
||||
|
||||
b.HasIndex("BanningAdmin");
|
||||
|
||||
b.HasIndex("LastEditedById");
|
||||
|
||||
b.HasIndex("PlayerUserId")
|
||||
.HasDatabaseName("IX_server_ban_player_user_id");
|
||||
|
||||
b.HasIndex("RoundId")
|
||||
.HasDatabaseName("IX_server_ban_round_id");
|
||||
|
||||
b.ToTable("server_ban", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBanExemption", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
@@ -1149,144 +1265,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("server_ban_hit", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("server_role_ban_id");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<DateTime>("BanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("ban_time");
|
||||
|
||||
b.Property<Guid?>("BanningAdmin")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("banning_admin");
|
||||
|
||||
b.Property<DateTime?>("ExpirationTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("expiration_time");
|
||||
|
||||
b.Property<bool>("Hidden")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("hidden");
|
||||
|
||||
b.Property<DateTime?>("LastEditedAt")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_at");
|
||||
|
||||
b.Property<Guid?>("LastEditedById")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("last_edited_by_id");
|
||||
|
||||
b.Property<Guid?>("PlayerUserId")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("player_user_id");
|
||||
|
||||
b.Property<TimeSpan>("PlaytimeAtNote")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("playtime_at_note");
|
||||
|
||||
b.Property<string>("Reason")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("reason");
|
||||
|
||||
b.Property<string>("RoleId")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("role_id");
|
||||
|
||||
b.Property<int?>("RoundId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("round_id");
|
||||
|
||||
b.Property<int>("Severity")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("severity");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_role_ban");
|
||||
|
||||
b.HasIndex("Address");
|
||||
|
||||
b.HasIndex("BanningAdmin");
|
||||
|
||||
b.HasIndex("LastEditedById");
|
||||
|
||||
b.HasIndex("PlayerUserId")
|
||||
.HasDatabaseName("IX_server_role_ban_player_user_id");
|
||||
|
||||
b.HasIndex("RoundId")
|
||||
.HasDatabaseName("IX_server_role_ban_round_id");
|
||||
|
||||
b.ToTable("server_role_ban", null, t =>
|
||||
{
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("role_unban_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("UnbanningAdmin")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unbanning_admin");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_role_unban");
|
||||
|
||||
b.HasIndex("BanId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("server_role_unban", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unban_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("UnbanningAdmin")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unbanning_admin");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_server_unban");
|
||||
|
||||
b.HasIndex("BanId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("server_unban", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Trait", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
@@ -1312,6 +1290,34 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("trait", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Unban", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("unban_id");
|
||||
|
||||
b.Property<int>("BanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_id");
|
||||
|
||||
b.Property<DateTime>("UnbanTime")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("unban_time");
|
||||
|
||||
b.Property<Guid?>("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<int>("Id")
|
||||
@@ -1594,6 +1600,123 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
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<int>("BanHwidId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ban_hwid_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("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")
|
||||
@@ -1750,70 +1873,14 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("Server");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "CreatedBy")
|
||||
.WithMany("AdminServerBansCreated")
|
||||
.HasForeignKey("BanningAdmin")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_ban_player_banning_admin");
|
||||
|
||||
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
|
||||
.WithMany("AdminServerBansLastEdited")
|
||||
.HasForeignKey("LastEditedById")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_ban_player_last_edited_by_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.Round", "Round")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoundId")
|
||||
.HasConstraintName("FK_server_ban_round_round_id");
|
||||
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
|
||||
{
|
||||
b1.Property<int>("ServerBanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("server_ban_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("hwid_type");
|
||||
|
||||
b1.HasKey("ServerBanId");
|
||||
|
||||
b1.ToTable("server_ban");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ServerBanId")
|
||||
.HasConstraintName("FK_server_ban_server_ban_server_ban_id");
|
||||
});
|
||||
|
||||
b.Navigation("CreatedBy");
|
||||
|
||||
b.Navigation("HWId");
|
||||
|
||||
b.Navigation("LastEditedBy");
|
||||
|
||||
b.Navigation("Round");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBanHit", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerBan", "Ban")
|
||||
b.HasOne("Content.Server.Database.Ban", "Ban")
|
||||
.WithMany("BanHits")
|
||||
.HasForeignKey("BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_ban_hit_server_ban_ban_id");
|
||||
.HasConstraintName("FK_server_ban_hit_ban_ban_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.ConnectionLog", "Connection")
|
||||
.WithMany("BanHits")
|
||||
@@ -1827,86 +1894,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.Navigation("Connection");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Player", "CreatedBy")
|
||||
.WithMany("AdminServerRoleBansCreated")
|
||||
.HasForeignKey("BanningAdmin")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_role_ban_player_banning_admin");
|
||||
|
||||
b.HasOne("Content.Server.Database.Player", "LastEditedBy")
|
||||
.WithMany("AdminServerRoleBansLastEdited")
|
||||
.HasForeignKey("LastEditedById")
|
||||
.HasPrincipalKey("UserId")
|
||||
.OnDelete(DeleteBehavior.SetNull)
|
||||
.HasConstraintName("FK_server_role_ban_player_last_edited_by_id");
|
||||
|
||||
b.HasOne("Content.Server.Database.Round", "Round")
|
||||
.WithMany()
|
||||
.HasForeignKey("RoundId")
|
||||
.HasConstraintName("FK_server_role_ban_round_round_id");
|
||||
|
||||
b.OwnsOne("Content.Server.Database.TypedHwid", "HWId", b1 =>
|
||||
{
|
||||
b1.Property<int>("ServerRoleBanId")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("server_role_ban_id");
|
||||
|
||||
b1.Property<byte[]>("Hwid")
|
||||
.IsRequired()
|
||||
.HasColumnType("BLOB")
|
||||
.HasColumnName("hwid");
|
||||
|
||||
b1.Property<int>("Type")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasDefaultValue(0)
|
||||
.HasColumnName("hwid_type");
|
||||
|
||||
b1.HasKey("ServerRoleBanId");
|
||||
|
||||
b1.ToTable("server_role_ban");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("ServerRoleBanId")
|
||||
.HasConstraintName("FK_server_role_ban_server_role_ban_server_role_ban_id");
|
||||
});
|
||||
|
||||
b.Navigation("CreatedBy");
|
||||
|
||||
b.Navigation("HWId");
|
||||
|
||||
b.Navigation("LastEditedBy");
|
||||
|
||||
b.Navigation("Round");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleUnban", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerRoleBan", "Ban")
|
||||
.WithOne("Unban")
|
||||
.HasForeignKey("Content.Server.Database.ServerRoleUnban", "BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_role_unban_server_role_ban_ban_id");
|
||||
|
||||
b.Navigation("Ban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerUnban", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.ServerBan", "Ban")
|
||||
.WithOne("Unban")
|
||||
.HasForeignKey("Content.Server.Database.ServerUnban", "BanId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired()
|
||||
.HasConstraintName("FK_server_unban_server_ban_ban_id");
|
||||
|
||||
b.Navigation("Ban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Trait", b =>
|
||||
{
|
||||
b.HasOne("Content.Server.Database.Profile", "Profile")
|
||||
@@ -1919,6 +1906,18 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
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)
|
||||
@@ -1953,6 +1952,23 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
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");
|
||||
@@ -1982,10 +1998,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
|
||||
b.Navigation("AdminServerBansLastEdited");
|
||||
|
||||
b.Navigation("AdminServerRoleBansCreated");
|
||||
|
||||
b.Navigation("AdminServerRoleBansLastEdited");
|
||||
|
||||
b.Navigation("AdminWatchlistsCreated");
|
||||
|
||||
b.Navigation("AdminWatchlistsDeleted");
|
||||
@@ -2034,18 +2046,6 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
|
||||
b.Navigation("Rounds");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerBan", b =>
|
||||
{
|
||||
b.Navigation("BanHits");
|
||||
|
||||
b.Navigation("Unban");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.ServerRoleBan", b =>
|
||||
{
|
||||
b.Navigation("Unban");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,328 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using Content.Shared.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NpgsqlTypes;
|
||||
|
||||
// ReSharper disable EntityFramework.ModelValidation.UnlimitedStringLength
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
//
|
||||
// Contains model definitions primarily related to bans.
|
||||
//
|
||||
|
||||
internal static class ModelBan
|
||||
{
|
||||
public static void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Ban>()
|
||||
.HasOne(b => b.CreatedBy)
|
||||
.WithMany(pl => pl.AdminServerBansCreated)
|
||||
.HasForeignKey(b => b.BanningAdmin)
|
||||
.HasPrincipalKey(pl => pl.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<Ban>()
|
||||
.HasOne(b => b.LastEditedBy)
|
||||
.WithMany(pl => pl.AdminServerBansLastEdited)
|
||||
.HasForeignKey(b => b.LastEditedById)
|
||||
.HasPrincipalKey(pl => pl.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<BanPlayer>()
|
||||
.HasIndex(bp => new { bp.UserId, bp.BanId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<BanHwid>()
|
||||
.OwnsOne(bp => bp.HWId)
|
||||
.Property(hwid => hwid.Hwid)
|
||||
.HasColumnName("hwid");
|
||||
|
||||
modelBuilder.Entity<BanRole>()
|
||||
.HasIndex(bp => new { bp.RoleType, bp.RoleId, bp.BanId })
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<BanRound>()
|
||||
.HasIndex(bp => new { bp.RoundId, bp.BanId })
|
||||
.IsUnique();
|
||||
|
||||
// Following indices have to be made manually by migration, due to limitations in EF Core:
|
||||
// https://github.com/dotnet/efcore/issues/11336
|
||||
// https://github.com/npgsql/efcore.pg/issues/2567
|
||||
// modelBuilder.Entity<BanAddress>()
|
||||
// .HasIndex(bp => new { bp.Address, bp.BanId })
|
||||
// .IsUnique();
|
||||
// modelBuilder.Entity<BanHwid>()
|
||||
// .HasIndex(hwid => new { hwid.HWId.Type, hwid.HWId.Hwid, hwid.Hwid })
|
||||
// .IsUnique();
|
||||
// (postgres only)
|
||||
// modelBuilder.Entity<BanAddress>()
|
||||
// .HasIndex(ba => ba.Address)
|
||||
// .IncludeProperties(ba => ba.BanId)
|
||||
// .IsUnique()
|
||||
// .HasMethod("gist")
|
||||
// .HasOperators("inet_ops");
|
||||
|
||||
modelBuilder.Entity<Ban>()
|
||||
.ToTable(t => t.HasCheckConstraint("NoExemptOnRoleBan", $"type = {(int)BanType.Server} OR exempt_flags = 0"));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a ban of some kind.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Bans come in two types: <see cref="BanType.Server"/> and <see cref="BanType.Role"/>,
|
||||
/// distinguished with <see cref="Type"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Bans have one or more "matching data", these being <see cref="BanAddress"/>, <see cref="BanPlayer"/>,
|
||||
/// and <see cref="BanHwid"/> entities. If a player's connection info matches any of these,
|
||||
/// the ban's effects will apply to that player.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Bans can be set to expire after a certain point in time, or be permanent. They can also be removed manually
|
||||
/// ("unbanned") by an admin, which is stored as an <see cref="Unban"/> entity existing for this ban.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class Ban
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this is a role or server ban.
|
||||
/// </summary>
|
||||
public required BanType Type { get; set; }
|
||||
|
||||
public TimeSpan PlaytimeAtNote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time when the ban was applied by an administrator.
|
||||
/// </summary>
|
||||
public DateTime BanTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
|
||||
/// </summary>
|
||||
public DateTime? ExpirationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The administrator-stated reason for applying the ban.
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The severity of the incident
|
||||
/// </summary>
|
||||
public NoteSeverity Severity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID of the admin that initially applied the ban.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(CreatedBy))]
|
||||
public Guid? BanningAdmin { get; set; }
|
||||
|
||||
public Player? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID of the admin that last edited the note
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(LastEditedBy))]
|
||||
public Guid? LastEditedById { get; set; }
|
||||
|
||||
public Player? LastEditedBy { get; set; }
|
||||
public DateTime? LastEditedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
|
||||
/// </summary>
|
||||
public ServerBanExemptFlags ExemptFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this ban should be automatically deleted from the database when it expires.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This isn't done automatically by the game,
|
||||
/// you will need to set up something like a cron job to clear this from your database,
|
||||
/// using a command like this:
|
||||
/// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()"
|
||||
/// </remarks>
|
||||
public bool AutoDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to display this ban in the admin remarks (notes) panel
|
||||
/// </summary>
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If present, an administrator has manually repealed this ban.
|
||||
/// </summary>
|
||||
public Unban? Unban { get; set; }
|
||||
|
||||
public List<BanRound>? Rounds { get; set; }
|
||||
public List<BanPlayer>? Players { get; set; }
|
||||
public List<BanAddress>? Addresses { get; set; }
|
||||
public List<BanHwid>? Hwids { get; set; }
|
||||
public List<BanRole>? Roles { get; set; }
|
||||
public List<ServerBanHit>? BanHits { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base type for entities that specify ban matching data.
|
||||
/// </summary>
|
||||
public interface IBanSelector
|
||||
{
|
||||
int BanId { get; }
|
||||
Ban? Ban { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a ban was related to a round (e.g. placed on that round).
|
||||
/// </summary>
|
||||
public sealed class BanRound
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the ban to which this round was relevant.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
public Ban? Ban { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the round to which this ban was relevant to.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Round))]
|
||||
public int RoundId { get; set; }
|
||||
|
||||
public Round? Round { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a player that a <see cref="T:Database.Ban"/> matches.
|
||||
/// </summary>
|
||||
public sealed class BanPlayer : IBanSelector
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user ID of the banned player.
|
||||
/// </summary>
|
||||
public Guid UserId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the ban to which this applies.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
public Ban? Ban { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies an IP address range that a <see cref="T:Database.Ban"/> matches.
|
||||
/// </summary>
|
||||
public sealed class BanAddress : IBanSelector
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The address range being matched.
|
||||
/// </summary>
|
||||
public required NpgsqlInet Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the ban to which this applies.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
public Ban? Ban { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies a HWID that a <see cref="T:Database.Ban"/> matches.
|
||||
/// </summary>
|
||||
public sealed class BanHwid : IBanSelector
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The HWID being matched.
|
||||
/// </summary>
|
||||
public required TypedHwid HWId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the ban to which this applies.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
public Ban? Ban { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single role banned among a greater role ban record.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="Ban"/>s of type <see cref="BanType.Role"/> should have one or more <see cref="BanRole"/>s
|
||||
/// to store which roles are actually banned.
|
||||
/// It is invalid for <see cref="BanType.Server"/> bans to have <see cref="BanRole"/> entities.
|
||||
/// </remarks>
|
||||
public sealed class BanRole
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// What type of role is being banned. For example <c>Job</c> or <c>Antag</c>.
|
||||
/// </summary>
|
||||
public required string RoleType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the role being banned. This is probably something like a prototype.
|
||||
/// </summary>
|
||||
public required string RoleId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of the ban to which this applies.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
public Ban? Ban { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An explicit repeal of a <see cref="Ban"/> by an administrator.
|
||||
/// Having an entry for a ban neutralizes it.
|
||||
/// </summary>
|
||||
public sealed class Unban
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of ban that is being repealed.
|
||||
/// </summary>
|
||||
[ForeignKey(nameof(Ban))]
|
||||
public int BanId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ban that is being repealed.
|
||||
/// </summary>
|
||||
public Ban? Ban { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The admin that repealed the ban.
|
||||
/// </summary>
|
||||
public Guid? UnbanningAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the ban was repealed.
|
||||
/// </summary>
|
||||
public DateTime UnbanTime { get; set; }
|
||||
}
|
||||
@@ -9,7 +9,6 @@ using System.Net;
|
||||
using System.Text.Json;
|
||||
using Content.Shared.Database;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NpgsqlTypes;
|
||||
|
||||
namespace Content.Server.Database
|
||||
{
|
||||
@@ -31,13 +30,17 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminLogPlayer> AdminLogPlayer { get; set; } = null!;
|
||||
public DbSet<Whitelist> Whitelist { get; set; } = null!;
|
||||
public DbSet<Blacklist> Blacklist { get; set; } = null!;
|
||||
public DbSet<ServerBan> Ban { get; set; } = default!;
|
||||
public DbSet<ServerUnban> Unban { get; set; } = default!;
|
||||
public DbSet<Ban> Ban { get; set; } = default!;
|
||||
public DbSet<BanRound> BanRound { get; set; } = default!;
|
||||
public DbSet<BanPlayer> BanPlayer { get; set; } = default!;
|
||||
public DbSet<BanAddress> BanAddress { get; set; } = default!;
|
||||
public DbSet<BanHwid> BanHwid { get; set; } = default!;
|
||||
public DbSet<BanRole> BanRole { get; set; } = default!;
|
||||
public DbSet<Unban> Unban { get; set; } = default!;
|
||||
public DbSet<ServerBanExemption> BanExemption { get; set; } = default!;
|
||||
public DbSet<ConnectionLog> ConnectionLog { get; set; } = default!;
|
||||
public DbSet<ServerBanHit> ServerBanHit { get; set; } = default!;
|
||||
public DbSet<ServerRoleBan> RoleBan { get; set; } = default!;
|
||||
public DbSet<ServerRoleUnban> RoleUnban { get; set; } = default!;
|
||||
|
||||
public DbSet<PlayTime> PlayTime { get; set; } = default!;
|
||||
public DbSet<UploadedResourceLog> UploadedResourceLog { get; set; } = default!;
|
||||
public DbSet<AdminNote> AdminNotes { get; set; } = null!;
|
||||
@@ -145,43 +148,11 @@ namespace Content.Server.Database
|
||||
modelBuilder.Entity<AdminLogPlayer>()
|
||||
.HasKey(logPlayer => new {logPlayer.RoundId, logPlayer.LogId, logPlayer.PlayerUserId});
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.HasIndex(p => p.PlayerUserId);
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.HasIndex(p => p.Address);
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.HasIndex(p => p.PlayerUserId);
|
||||
|
||||
modelBuilder.Entity<ServerUnban>()
|
||||
.HasIndex(p => p.BanId)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<ServerBan>().ToTable(t =>
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
|
||||
|
||||
// Ban exemption can't have flags 0 since that wouldn't exempt anything.
|
||||
// The row should be removed if setting to 0.
|
||||
modelBuilder.Entity<ServerBanExemption>().ToTable(t =>
|
||||
t.HasCheckConstraint("FlagsNotZero", "flags != 0"));
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.HasIndex(p => p.PlayerUserId);
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.HasIndex(p => p.Address);
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.HasIndex(p => p.PlayerUserId);
|
||||
|
||||
modelBuilder.Entity<ServerRoleUnban>()
|
||||
.HasIndex(p => p.BanId)
|
||||
.IsUnique();
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>().ToTable(t =>
|
||||
t.HasCheckConstraint("HaveEitherAddressOrUserIdOrHWId", "address IS NOT NULL OR player_user_id IS NOT NULL OR hwid IS NOT NULL"));
|
||||
|
||||
modelBuilder.Entity<Player>()
|
||||
.HasIndex(p => p.UserId)
|
||||
.IsUnique();
|
||||
@@ -296,34 +267,6 @@ namespace Content.Server.Database
|
||||
t.HasCheckConstraint("NotDismissedAndSeen",
|
||||
"NOT dismissed OR seen"));
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.HasOne(ban => ban.CreatedBy)
|
||||
.WithMany(author => author.AdminServerBansCreated)
|
||||
.HasForeignKey(ban => ban.BanningAdmin)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.HasOne(ban => ban.LastEditedBy)
|
||||
.WithMany(author => author.AdminServerBansLastEdited)
|
||||
.HasForeignKey(ban => ban.LastEditedById)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.HasOne(ban => ban.CreatedBy)
|
||||
.WithMany(author => author.AdminServerRoleBansCreated)
|
||||
.HasForeignKey(ban => ban.BanningAdmin)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.HasOne(ban => ban.LastEditedBy)
|
||||
.WithMany(author => author.AdminServerRoleBansLastEdited)
|
||||
.HasForeignKey(ban => ban.LastEditedById)
|
||||
.HasPrincipalKey(author => author.UserId)
|
||||
.OnDelete(DeleteBehavior.SetNull);
|
||||
|
||||
modelBuilder.Entity<RoleWhitelist>()
|
||||
.HasOne(w => w.Player)
|
||||
.WithMany(p => p.JobWhitelists)
|
||||
@@ -342,26 +285,6 @@ namespace Content.Server.Database
|
||||
.Property(p => p.Type)
|
||||
.HasDefaultValue(HwidType.Legacy);
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Hwid)
|
||||
.HasColumnName("hwid");
|
||||
|
||||
modelBuilder.Entity<ServerBan>()
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Type)
|
||||
.HasDefaultValue(HwidType.Legacy);
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Hwid)
|
||||
.HasColumnName("hwid");
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>()
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Type)
|
||||
.HasDefaultValue(HwidType.Legacy);
|
||||
|
||||
modelBuilder.Entity<ConnectionLog>()
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Hwid)
|
||||
@@ -371,6 +294,8 @@ namespace Content.Server.Database
|
||||
.OwnsOne(p => p.HWId)
|
||||
.Property(p => p.Type)
|
||||
.HasDefaultValue(HwidType.Legacy);
|
||||
|
||||
ModelBan.OnModelCreating(modelBuilder);
|
||||
}
|
||||
|
||||
public virtual IQueryable<AdminLog> SearchLogs(IQueryable<AdminLog> query, string searchText)
|
||||
@@ -592,10 +517,8 @@ namespace Content.Server.Database
|
||||
public List<AdminMessage> AdminMessagesCreated { get; set; } = null!;
|
||||
public List<AdminMessage> AdminMessagesLastEdited { get; set; } = null!;
|
||||
public List<AdminMessage> AdminMessagesDeleted { get; set; } = null!;
|
||||
public List<ServerBan> AdminServerBansCreated { get; set; } = null!;
|
||||
public List<ServerBan> AdminServerBansLastEdited { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansCreated { get; set; } = null!;
|
||||
public List<ServerRoleBan> AdminServerRoleBansLastEdited { get; set; } = null!;
|
||||
public List<Ban> AdminServerBansCreated { get; set; } = null!;
|
||||
public List<Ban> AdminServerBansLastEdited { get; set; } = null!;
|
||||
public List<RoleWhitelist> JobWhitelists { get; set; } = null!;
|
||||
}
|
||||
|
||||
@@ -725,30 +648,6 @@ namespace Content.Server.Database
|
||||
[ForeignKey("RoundId,LogId")] public AdminLog Log { get; set; } = default!;
|
||||
}
|
||||
|
||||
// Used by SS14.Admin
|
||||
public interface IBanCommon<TUnban> where TUnban : IUnbanCommon
|
||||
{
|
||||
int Id { get; set; }
|
||||
Guid? PlayerUserId { get; set; }
|
||||
NpgsqlInet? Address { get; set; }
|
||||
TypedHwid? HWId { get; set; }
|
||||
DateTime BanTime { get; set; }
|
||||
DateTime? ExpirationTime { get; set; }
|
||||
string Reason { get; set; }
|
||||
NoteSeverity Severity { get; set; }
|
||||
Guid? BanningAdmin { get; set; }
|
||||
TUnban? Unban { get; set; }
|
||||
}
|
||||
|
||||
// Used by SS14.Admin
|
||||
public interface IUnbanCommon
|
||||
{
|
||||
int Id { get; set; }
|
||||
int BanId { get; set; }
|
||||
Guid? UnbanningAdmin { get; set; }
|
||||
DateTime UnbanTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags for use with <see cref="ServerBanExemption"/>.
|
||||
/// </summary>
|
||||
@@ -786,138 +685,6 @@ namespace Content.Server.Database
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A ban from playing on the server.
|
||||
/// If an incoming connection matches any of UserID, IP, or HWID, they will be blocked from joining the server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// At least one of UserID, IP, or HWID must be given (otherwise the ban would match nothing).
|
||||
/// </remarks>
|
||||
[Table("server_ban"), Index(nameof(PlayerUserId))]
|
||||
public class ServerBan : IBanCommon<ServerUnban>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
[ForeignKey("Round")]
|
||||
public int? RoundId { get; set; }
|
||||
public Round? Round { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The user ID of the banned player.
|
||||
/// </summary>
|
||||
public Guid? PlayerUserId { get; set; }
|
||||
[Required] public TimeSpan PlaytimeAtNote { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// CIDR IP address range of the ban. The whole range can match the ban.
|
||||
/// </summary>
|
||||
public NpgsqlInet? Address { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Hardware ID of the banned player.
|
||||
/// </summary>
|
||||
public TypedHwid? HWId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time when the ban was applied by an administrator.
|
||||
/// </summary>
|
||||
public DateTime BanTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the ban will expire. If null, the ban is permanent and will not expire naturally.
|
||||
/// </summary>
|
||||
public DateTime? ExpirationTime { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The administrator-stated reason for applying the ban.
|
||||
/// </summary>
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The severity of the incident
|
||||
/// </summary>
|
||||
public NoteSeverity Severity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID of the admin that applied the ban.
|
||||
/// </summary>
|
||||
[ForeignKey("CreatedBy")]
|
||||
public Guid? BanningAdmin { get; set; }
|
||||
|
||||
public Player? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// User ID of the admin that last edited the note
|
||||
/// </summary>
|
||||
[ForeignKey("LastEditedBy")]
|
||||
public Guid? LastEditedById { get; set; }
|
||||
|
||||
public Player? LastEditedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// When the ban was last edited
|
||||
/// </summary>
|
||||
public DateTime? LastEditedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Optional flags that allow adding exemptions to the ban via <see cref="ServerBanExemption"/>.
|
||||
/// </summary>
|
||||
public ServerBanExemptFlags ExemptFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If present, an administrator has manually repealed this ban.
|
||||
/// </summary>
|
||||
public ServerUnban? Unban { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether this ban should be automatically deleted from the database when it expires.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This isn't done automatically by the game,
|
||||
/// you will need to set up something like a cron job to clear this from your database,
|
||||
/// using a command like this:
|
||||
/// psql -d ss14 -c "DELETE FROM server_ban WHERE auto_delete AND expiration_time < NOW()"
|
||||
/// </remarks>
|
||||
public bool AutoDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to display this ban in the admin remarks (notes) panel
|
||||
/// </summary>
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
public List<ServerBanHit> BanHits { get; set; } = null!;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An explicit repeal of a <see cref="ServerBan"/> by an administrator.
|
||||
/// Having an entry for a ban neutralizes it.
|
||||
/// </summary>
|
||||
[Table("server_unban")]
|
||||
public class ServerUnban : IUnbanCommon
|
||||
{
|
||||
[Column("unban_id")] public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ID of ban that is being repealed.
|
||||
/// </summary>
|
||||
public int BanId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The ban that is being repealed.
|
||||
/// </summary>
|
||||
public ServerBan Ban { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The admin that repealed the ban.
|
||||
/// </summary>
|
||||
public Guid? UnbanningAdmin { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The time the ban repealed.
|
||||
/// </summary>
|
||||
public DateTime UnbanTime { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An exemption for a specific user to a certain type of <see cref="ServerBan"/>.
|
||||
/// </summary>
|
||||
@@ -938,7 +705,7 @@ namespace Content.Server.Database
|
||||
|
||||
/// <summary>
|
||||
/// The ban flags to exempt this player from.
|
||||
/// If any bit overlaps <see cref="ServerBan.ExemptFlags"/>, the ban is ignored.
|
||||
/// If any bit overlaps <see cref="Ban.ExemptFlags"/>, the ban is ignored.
|
||||
/// </summary>
|
||||
public ServerBanExemptFlags Flags { get; set; }
|
||||
}
|
||||
@@ -1001,54 +768,10 @@ namespace Content.Server.Database
|
||||
public int BanId { get; set; }
|
||||
public int ConnectionId { get; set; }
|
||||
|
||||
public ServerBan Ban { get; set; } = null!;
|
||||
public Ban Ban { get; set; } = null!;
|
||||
public ConnectionLog Connection { get; set; } = null!;
|
||||
}
|
||||
|
||||
[Table("server_role_ban"), Index(nameof(PlayerUserId))]
|
||||
public sealed class ServerRoleBan : IBanCommon<ServerRoleUnban>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public int? RoundId { get; set; }
|
||||
public Round? Round { get; set; }
|
||||
public Guid? PlayerUserId { get; set; }
|
||||
[Required] public TimeSpan PlaytimeAtNote { get; set; }
|
||||
public NpgsqlInet? Address { get; set; }
|
||||
public TypedHwid? HWId { get; set; }
|
||||
|
||||
public DateTime BanTime { get; set; }
|
||||
|
||||
public DateTime? ExpirationTime { get; set; }
|
||||
|
||||
public string Reason { get; set; } = null!;
|
||||
|
||||
public NoteSeverity Severity { get; set; }
|
||||
[ForeignKey("CreatedBy")] public Guid? BanningAdmin { get; set; }
|
||||
public Player? CreatedBy { get; set; }
|
||||
|
||||
[ForeignKey("LastEditedBy")] public Guid? LastEditedById { get; set; }
|
||||
public Player? LastEditedBy { get; set; }
|
||||
public DateTime? LastEditedAt { get; set; }
|
||||
|
||||
public ServerRoleUnban? Unban { get; set; }
|
||||
public bool Hidden { get; set; }
|
||||
|
||||
public string RoleId { get; set; } = null!;
|
||||
}
|
||||
|
||||
[Table("server_role_unban")]
|
||||
public sealed class ServerRoleUnban : IUnbanCommon
|
||||
{
|
||||
[Column("role_unban_id")] public int Id { get; set; }
|
||||
|
||||
public int BanId { get; set; }
|
||||
public ServerRoleBan Ban { get; set; } = null!;
|
||||
|
||||
public Guid? UnbanningAdmin { get; set; }
|
||||
|
||||
public DateTime UnbanTime { get; set; }
|
||||
}
|
||||
|
||||
[Table("play_time")]
|
||||
public sealed class PlayTime
|
||||
{
|
||||
@@ -1248,31 +971,31 @@ namespace Content.Server.Database
|
||||
/// <summary>
|
||||
/// The reason for the ban.
|
||||
/// </summary>
|
||||
/// <seealso cref="ServerBan.Reason"/>
|
||||
/// <seealso cref="Ban.Reason"/>
|
||||
public string Reason { get; set; } = "";
|
||||
|
||||
/// <summary>
|
||||
/// Exemptions granted to the ban.
|
||||
/// </summary>
|
||||
/// <seealso cref="ServerBan.ExemptFlags"/>
|
||||
/// <seealso cref="Ban.ExemptFlags"/>
|
||||
public ServerBanExemptFlags ExemptFlags { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Severity of the ban
|
||||
/// </summary>
|
||||
/// <seealso cref="ServerBan.Severity"/>
|
||||
/// <seealso cref="Ban.Severity"/>
|
||||
public NoteSeverity Severity { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ban will be automatically deleted once expired.
|
||||
/// </summary>
|
||||
/// <seealso cref="ServerBan.AutoDelete"/>
|
||||
/// <seealso cref="Ban.AutoDelete"/>
|
||||
public bool AutoDelete { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ban is not visible to players in the remarks menu.
|
||||
/// </summary>
|
||||
/// <seealso cref="ServerBan.Hidden"/>
|
||||
/// <seealso cref="Ban.Hidden"/>
|
||||
public bool Hidden { get; set; }
|
||||
}
|
||||
|
||||
|
||||
@@ -39,10 +39,7 @@ namespace Content.Server.Database
|
||||
// ReSharper disable StringLiteralTypo
|
||||
// Enforce that an address cannot be IPv6-mapped IPv4.
|
||||
// So that IPv4 addresses are consistent between separate-socket and dual-stack socket modes.
|
||||
modelBuilder.Entity<ServerBan>().ToTable(t =>
|
||||
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
|
||||
|
||||
modelBuilder.Entity<ServerRoleBan>().ToTable( t =>
|
||||
modelBuilder.Entity<BanAddress>().ToTable(t =>
|
||||
t.HasCheckConstraint("AddressNotIPv6MappedIPv4", "NOT inet '::ffff:0.0.0.0/96' >>= address"));
|
||||
|
||||
modelBuilder.Entity<Player>().ToTable(t =>
|
||||
|
||||
@@ -58,13 +58,7 @@ namespace Content.Server.Database
|
||||
);
|
||||
|
||||
modelBuilder
|
||||
.Entity<ServerBan>()
|
||||
.Property(e => e.Address)
|
||||
.HasColumnType("TEXT")
|
||||
.HasConversion(ipMaskConverter);
|
||||
|
||||
modelBuilder
|
||||
.Entity<ServerRoleBan>()
|
||||
.Entity<BanAddress>()
|
||||
.Property(e => e.Address)
|
||||
.HasColumnType("TEXT")
|
||||
.HasConversion(ipMaskConverter);
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.EUI;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.Administration.BanList;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Eui;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
@@ -22,8 +25,8 @@ public sealed class BanListEui : BaseEui
|
||||
|
||||
private Guid BanListPlayer { get; set; }
|
||||
private string BanListPlayerName { get; set; } = string.Empty;
|
||||
private List<SharedServerBan> Bans { get; } = new();
|
||||
private List<SharedServerRoleBan> RoleBans { get; } = new();
|
||||
private List<SharedBan> Bans { get; } = new();
|
||||
private List<SharedBan> RoleBans { get; } = new();
|
||||
|
||||
public override void Opened()
|
||||
{
|
||||
@@ -54,74 +57,38 @@ public sealed class BanListEui : BaseEui
|
||||
|
||||
private async Task LoadBans(NetUserId userId)
|
||||
{
|
||||
foreach (var ban in await _db.GetServerBansAsync(null, userId, null, null))
|
||||
{
|
||||
SharedServerUnban? unban = null;
|
||||
if (ban.Unban is { } unbanDef)
|
||||
{
|
||||
var unbanningAdmin = unbanDef.UnbanningAdmin == null
|
||||
? null
|
||||
: (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username;
|
||||
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
|
||||
}
|
||||
|
||||
(string, int cidrMask)? ip = ("*Hidden*", 0);
|
||||
var hwid = "*Hidden*";
|
||||
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
|
||||
{
|
||||
ip = ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null;
|
||||
|
||||
hwid = ban.HWId?.ToString();
|
||||
}
|
||||
|
||||
Bans.Add(new SharedServerBan(
|
||||
ban.Id,
|
||||
ban.UserId,
|
||||
ip,
|
||||
hwid,
|
||||
ban.BanTime.UtcDateTime,
|
||||
ban.ExpirationTime?.UtcDateTime,
|
||||
ban.Reason,
|
||||
ban.BanningAdmin == null
|
||||
? null
|
||||
: (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username,
|
||||
unban
|
||||
));
|
||||
}
|
||||
await LoadBansCore(userId, BanType.Server, Bans);
|
||||
await LoadBansCore(userId, BanType.Role, RoleBans);
|
||||
}
|
||||
|
||||
private async Task LoadRoleBans(NetUserId userId)
|
||||
private async Task LoadBansCore(NetUserId userId, BanType banType, List<SharedBan> list)
|
||||
{
|
||||
foreach (var ban in await _db.GetServerRoleBansAsync(null, userId, null, null))
|
||||
foreach (var ban in await _db.GetBansAsync(null, userId, null, null, type: banType))
|
||||
{
|
||||
SharedServerUnban? unban = null;
|
||||
SharedUnban? unban = null;
|
||||
if (ban.Unban is { } unbanDef)
|
||||
{
|
||||
var unbanningAdmin = unbanDef.UnbanningAdmin == null
|
||||
? null
|
||||
: (await _playerLocator.LookupIdAsync(unbanDef.UnbanningAdmin.Value))?.Username;
|
||||
unban = new SharedServerUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
|
||||
unban = new SharedUnban(unbanningAdmin, ban.Unban.UnbanTime.UtcDateTime);
|
||||
}
|
||||
|
||||
(string, int cidrMask)? ip = ("*Hidden*", 0);
|
||||
var hwid = "*Hidden*";
|
||||
ImmutableArray<(string, int cidrMask)> ips = [("*Hidden*", 0)];
|
||||
ImmutableArray<string> hwids = ["*Hidden*"];
|
||||
|
||||
if (_admins.HasAdminFlag(Player, AdminFlags.Pii))
|
||||
{
|
||||
ip = ban.Address is { } address
|
||||
? (address.address.ToString(), address.cidrMask)
|
||||
: null;
|
||||
|
||||
hwid = ban.HWId?.ToString();
|
||||
ips = [..ban.Addresses.Select(a => (a.address.ToString(), a.cidrMask))];
|
||||
hwids = [..ban.HWIds.Select(h => h.ToString())];
|
||||
}
|
||||
RoleBans.Add(new SharedServerRoleBan(
|
||||
|
||||
list.Add(new SharedBan(
|
||||
ban.Id,
|
||||
ban.UserId,
|
||||
ip,
|
||||
hwid,
|
||||
ban.Type,
|
||||
ban.UserIds,
|
||||
ips,
|
||||
hwids,
|
||||
ban.BanTime.UtcDateTime,
|
||||
ban.ExpirationTime?.UtcDateTime,
|
||||
ban.Reason,
|
||||
@@ -129,7 +96,7 @@ public sealed class BanListEui : BaseEui
|
||||
? null
|
||||
: (await _playerLocator.LookupIdAsync(ban.BanningAdmin.Value))?.Username,
|
||||
unban,
|
||||
ban.Role
|
||||
ban.Roles
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -144,7 +111,6 @@ public sealed class BanListEui : BaseEui
|
||||
string.Empty;
|
||||
|
||||
await LoadBans(userId);
|
||||
await LoadRoleBans(userId);
|
||||
|
||||
StateDirty();
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ public sealed class BanPanelEui : BaseEui
|
||||
private string PlayerName { get; set; } = string.Empty;
|
||||
private IPAddress? LastAddress { get; set; }
|
||||
private ImmutableTypedHwid? LastHwid { get; set; }
|
||||
private const int Ipv4_CIDR = 32;
|
||||
private const int Ipv6_CIDR = 64;
|
||||
private const int Ipv4_CIDR = CreateBanInfo.DefaultMaskIpv4;
|
||||
private const int Ipv6_CIDR = CreateBanInfo.DefaultMaskIpv6;
|
||||
|
||||
public BanPanelEui()
|
||||
{
|
||||
@@ -73,6 +73,15 @@ public sealed class BanPanelEui : BaseEui
|
||||
return;
|
||||
}
|
||||
|
||||
var isRoleBan = ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0;
|
||||
|
||||
CreateBanInfo banInfo = isRoleBan ? new CreateRoleBanInfo(ban.Reason) : new CreateServerBanInfo(ban.Reason);
|
||||
|
||||
banInfo.WithBanningAdmin(Player.UserId);
|
||||
banInfo.WithSeverity(ban.Severity);
|
||||
if (ban.BanDurationMinutes > 0)
|
||||
banInfo.WithMinutes(ban.BanDurationMinutes);
|
||||
|
||||
(IPAddress, int)? addressRange = null;
|
||||
if (ban.IpAddress is not null)
|
||||
{
|
||||
@@ -113,69 +122,46 @@ public sealed class BanPanelEui : BaseEui
|
||||
targetHWid = ban.UseLastHwid ? located.LastHWId : ban.Hwid;
|
||||
}
|
||||
|
||||
if (ban.BannedJobs?.Length > 0 || ban.BannedAntags?.Length > 0)
|
||||
if (addressRange != null)
|
||||
banInfo.AddAddressRange(addressRange.Value);
|
||||
|
||||
if (targetUid != null)
|
||||
banInfo.AddUser(targetUid.Value, ban.Target!);
|
||||
|
||||
banInfo.AddHWId(targetHWid);
|
||||
|
||||
if (isRoleBan)
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
foreach (var role in ban.BannedJobs ?? [])
|
||||
var roleBanInfo = (CreateRoleBanInfo)banInfo;
|
||||
foreach (var row in ban.BannedJobs ?? [])
|
||||
{
|
||||
_banManager.CreateRoleBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
role,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason,
|
||||
now
|
||||
);
|
||||
roleBanInfo.AddJob(row);
|
||||
}
|
||||
|
||||
foreach (var role in ban.BannedAntags ?? [])
|
||||
foreach (var row in ban.BannedAntags ?? [])
|
||||
{
|
||||
_banManager.CreateRoleBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
role,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason,
|
||||
now
|
||||
);
|
||||
roleBanInfo.AddAntag(row);
|
||||
}
|
||||
|
||||
Close();
|
||||
|
||||
return;
|
||||
_banManager.CreateRoleBan(roleBanInfo);
|
||||
}
|
||||
|
||||
if (ban.Erase && targetUid is not null)
|
||||
else
|
||||
{
|
||||
try
|
||||
if (ban.Erase && targetUid is not null)
|
||||
{
|
||||
if (_entities.TrySystem(out AdminSystem? adminSystem))
|
||||
adminSystem.Erase(targetUid.Value);
|
||||
try
|
||||
{
|
||||
if (_entities.TrySystem(out AdminSystem? adminSystem))
|
||||
adminSystem.Erase(targetUid.Value);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while erasing banned player:\n{e}");
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while erasing banned player:\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
_banManager.CreateServerBan(
|
||||
targetUid,
|
||||
ban.Target,
|
||||
Player.UserId,
|
||||
addressRange,
|
||||
targetHWid,
|
||||
ban.BanDurationMinutes,
|
||||
ban.Severity,
|
||||
ban.Reason
|
||||
);
|
||||
_banManager.CreateServerBan((CreateServerBanInfo)banInfo);
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
@@ -90,7 +90,15 @@ public sealed class BanCommand : LocalizedCommands
|
||||
var targetUid = located.UserId;
|
||||
var targetHWid = located.LastHWId;
|
||||
|
||||
_bans.CreateServerBan(targetUid, target, player?.UserId, null, targetHWid, minutes, severity, reason);
|
||||
var banInfo = new CreateServerBanInfo(reason);
|
||||
banInfo.WithBanningAdmin(player?.UserId);
|
||||
banInfo.AddUser(targetUid, target);
|
||||
banInfo.AddHWId(targetHWid);
|
||||
if (minutes > 0)
|
||||
banInfo.WithMinutes(minutes);
|
||||
banInfo.WithSeverity(severity);
|
||||
|
||||
_bans.CreateServerBan(banInfo);
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -39,7 +39,7 @@ public sealed class BanListCommand : LocalizedCommands
|
||||
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
var bans = await _dbManager.GetServerBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
|
||||
var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, false);
|
||||
|
||||
if (bans.Count == 0)
|
||||
{
|
||||
|
||||
@@ -96,13 +96,20 @@ public sealed class DepartmentBanCommand : IConsoleCommand
|
||||
var targetUid = located.UserId;
|
||||
var targetHWid = located.LastHWId;
|
||||
|
||||
// If you are trying to remove the following variable, please don't. It's there because the note system groups role bans by time, reason and banning admin.
|
||||
// Without it the note list will get needlessly cluttered.
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var banInfo = new CreateRoleBanInfo(reason);
|
||||
if (minutes > 0)
|
||||
banInfo.WithMinutes(minutes);
|
||||
banInfo.AddUser(targetUid, located.Username);
|
||||
banInfo.WithBanningAdmin(shell.Player?.UserId);
|
||||
banInfo.AddHWId(targetHWid);
|
||||
banInfo.WithSeverity(severity);
|
||||
|
||||
foreach (var job in departmentProto.Roles)
|
||||
{
|
||||
_banManager.CreateRoleBan(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, job, minutes, severity, reason, now);
|
||||
banInfo.AddJob(job);
|
||||
}
|
||||
|
||||
_banManager.CreateRoleBan(banInfo);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Server.Administration.Notes;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
@@ -46,7 +47,7 @@ public sealed class OpenAdminNotesCommand : LocalizedCommands
|
||||
return;
|
||||
}
|
||||
|
||||
await _adminNotes.OpenEui(player, notedPlayer);
|
||||
await _adminNotes.OpenEui(player, new NetUserId(notedPlayer));
|
||||
}
|
||||
|
||||
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
var ban = await _dbManager.GetServerBanAsync(banId);
|
||||
var ban = await _dbManager.GetBanAsync(banId);
|
||||
|
||||
if (ban == null)
|
||||
{
|
||||
@@ -50,7 +50,7 @@ namespace Content.Server.Administration.Commands
|
||||
return;
|
||||
}
|
||||
|
||||
await _dbManager.AddServerUnbanAsync(new ServerUnbanDef(banId, player?.UserId, DateTimeOffset.Now));
|
||||
await _dbManager.AddUnbanAsync(new UnbanDef(banId, player?.UserId, DateTimeOffset.Now));
|
||||
|
||||
shell.WriteLine(Loc.GetString($"cmd-pardon-success", ("id", banId)));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Database;
|
||||
@@ -99,12 +97,29 @@ public sealed class RoleBanCommand : IConsoleCommand
|
||||
var targetUid = located.UserId;
|
||||
var targetHWid = located.LastHWId;
|
||||
|
||||
var banInfo = new CreateRoleBanInfo(reason);
|
||||
if (minutes > 0)
|
||||
banInfo.WithMinutes(minutes);
|
||||
banInfo.AddUser(targetUid, located.Username);
|
||||
banInfo.WithBanningAdmin(shell.Player?.UserId);
|
||||
banInfo.AddHWId(targetHWid);
|
||||
banInfo.WithSeverity(severity);
|
||||
|
||||
if (_proto.HasIndex<JobPrototype>(role))
|
||||
_bans.CreateRoleBan<JobPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
|
||||
{
|
||||
banInfo.AddJob(new ProtoId<JobPrototype>(role));
|
||||
}
|
||||
else if (_proto.HasIndex<AntagPrototype>(role))
|
||||
_bans.CreateRoleBan<AntagPrototype>(targetUid, located.Username, shell.Player?.UserId, null, targetHWid, role, minutes, severity, reason, DateTimeOffset.UtcNow);
|
||||
{
|
||||
banInfo.AddAntag(new ProtoId<AntagPrototype>(role));
|
||||
}
|
||||
else
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-roleban-job-parse", ("job", role)));
|
||||
return;
|
||||
}
|
||||
|
||||
_bans.CreateRoleBan(banInfo);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Content.Server.Administration.BanList;
|
||||
using Content.Server.Administration.BanList;
|
||||
using Content.Server.EUI;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Administration;
|
||||
using Robust.Server.Player;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
@@ -48,7 +46,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
|
||||
if (shell.Player is not { } player)
|
||||
{
|
||||
|
||||
var bans = await _dbManager.GetServerRoleBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned);
|
||||
var bans = await _dbManager.GetBansAsync(data.LastAddress, data.UserId, data.LastLegacyHWId, data.LastModernHWIds, includeUnbanned, type: BanType.Role);
|
||||
|
||||
if (bans.Count == 0)
|
||||
{
|
||||
@@ -58,7 +56,7 @@ public sealed class RoleBanListCommand : IConsoleCommand
|
||||
|
||||
foreach (var ban in bans)
|
||||
{
|
||||
var msg = $"ID: {ban.Id}: Role: {ban.Role} Reason: {ban.Reason}";
|
||||
var msg = $"ID: {ban.Id}: Role(s): {string.Join(",", ban.Roles ?? [])} Reason: {ban.Reason}";
|
||||
shell.WriteLine(msg);
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -41,14 +41,8 @@ public sealed partial class BanManager
|
||||
|
||||
private async void ProcessBanNotification(BanNotificationData data)
|
||||
{
|
||||
if ((await _entryManager.ServerEntity).Id == data.ServerId)
|
||||
{
|
||||
_sawmill.Verbose("Not processing ban notification: came from this server");
|
||||
return;
|
||||
}
|
||||
|
||||
_sawmill.Verbose($"Processing ban notification for ban {data.BanId}");
|
||||
var ban = await _db.GetServerBanAsync(data.BanId);
|
||||
var ban = await _db.GetBanAsync(data.BanId);
|
||||
if (ban == null)
|
||||
{
|
||||
_sawmill.Warning($"Ban in notification ({data.BanId}) didn't exist?");
|
||||
@@ -86,15 +80,5 @@ public sealed partial class BanManager
|
||||
/// </summary>
|
||||
[JsonRequired, JsonPropertyName("ban_id")]
|
||||
public int BanId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// The id of the server the ban was made on.
|
||||
/// This is used to avoid double work checking the ban on the originating server.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is optional in case the ban was made outside a server (SS14.Admin)
|
||||
/// </remarks>
|
||||
[JsonPropertyName("server_id")]
|
||||
public int? ServerId { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -21,6 +20,7 @@ using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -43,10 +43,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public const string SawmillId = "admin.bans";
|
||||
public const string PrefixAntag = "Antag:";
|
||||
public const string PrefixJob = "Job:";
|
||||
public const string DbTypeAntag = "Antag";
|
||||
public const string DbTypeJob = "Job";
|
||||
|
||||
private readonly Dictionary<ICommonSession, List<ServerRoleBanDef>> _cachedRoleBans = new();
|
||||
private readonly Dictionary<ICommonSession, List<BanDef>> _cachedRoleBans = new();
|
||||
// Cached ban exemption flags are used to handle
|
||||
private readonly Dictionary<ICommonSession, ServerBanExemptFlags> _cachedBanExemptions = new();
|
||||
|
||||
@@ -72,9 +72,15 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
var netChannel = player.Channel;
|
||||
ImmutableArray<byte>? hwId = netChannel.UserData.HWId.Length == 0 ? null : netChannel.UserData.HWId;
|
||||
var modernHwids = netChannel.UserData.ModernHWIds;
|
||||
var roleBans = await _db.GetServerRoleBansAsync(netChannel.RemoteEndPoint.Address, player.UserId, hwId, modernHwids, false);
|
||||
var roleBans = await _db.GetBansAsync(
|
||||
netChannel.RemoteEndPoint.Address,
|
||||
player.UserId,
|
||||
hwId,
|
||||
modernHwids,
|
||||
false,
|
||||
type: BanType.Role);
|
||||
|
||||
var userRoleBans = new List<ServerRoleBanDef>();
|
||||
var userRoleBans = new List<BanDef>();
|
||||
foreach (var ban in roleBans)
|
||||
{
|
||||
userRoleBans.Add(ban);
|
||||
@@ -115,43 +121,37 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
}
|
||||
|
||||
#region Server Bans
|
||||
public async void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason)
|
||||
public async void CreateServerBan(CreateServerBanInfo banInfo)
|
||||
{
|
||||
DateTimeOffset? expires = null;
|
||||
if (minutes > 0)
|
||||
var (banDef, expires) = await CreateBanDef(banInfo, BanType.Server, null);
|
||||
|
||||
await _db.AddBanAsync(banDef);
|
||||
|
||||
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules))
|
||||
{
|
||||
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
|
||||
// Reset their last read rules. They probably need a refresher!
|
||||
foreach (var (userId, _) in banInfo.Users)
|
||||
{
|
||||
await _db.SetLastReadRules(userId, null);
|
||||
}
|
||||
}
|
||||
|
||||
_systems.TryGetEntitySystem<GameTicker>(out var ticker);
|
||||
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
|
||||
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
|
||||
|
||||
var banDef = new ServerBanDef(
|
||||
null,
|
||||
target,
|
||||
addressRange,
|
||||
hwid,
|
||||
DateTimeOffset.Now,
|
||||
expires,
|
||||
roundId,
|
||||
playtime,
|
||||
reason,
|
||||
severity,
|
||||
banningAdmin,
|
||||
null);
|
||||
|
||||
await _db.AddServerBanAsync(banDef);
|
||||
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
|
||||
await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher!
|
||||
var adminName = banningAdmin == null
|
||||
var adminName = banInfo.BanningAdmin == null
|
||||
? Loc.GetString("system-user")
|
||||
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
|
||||
var targetName = target is null ? "null" : $"{targetUsername} ({target})";
|
||||
var addressRangeString = addressRange != null
|
||||
? $"{addressRange.Value.Item1}/{addressRange.Value.Item2}"
|
||||
: "null";
|
||||
var hwidString = hwid?.ToString() ?? "null";
|
||||
: (await _db.GetPlayerRecordByUserId(banInfo.BanningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
|
||||
|
||||
var targetName = banInfo.Users.Count == 0
|
||||
? "null"
|
||||
: string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})"));
|
||||
|
||||
var addressRangeString = banInfo.AddressRanges.Count != 0
|
||||
? "null"
|
||||
: string.Join(", ", banInfo.AddressRanges.Select(a => $"{a.Address}/{a.Mask}"));
|
||||
|
||||
var hwidString = banInfo.HWIds.Count == 0
|
||||
? "null"
|
||||
: string.Join(", ", banInfo.HWIds);
|
||||
|
||||
var expiresString = expires == null ? Loc.GetString("server-ban-string-never") : $"{expires}";
|
||||
|
||||
var key = _cfg.GetCVar(CCVars.AdminShowPIIOnBan) ? "server-ban-string" : "server-ban-string-no-pii";
|
||||
@@ -159,12 +159,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
var logMessage = Loc.GetString(
|
||||
key,
|
||||
("admin", adminName),
|
||||
("severity", severity),
|
||||
("severity", banDef.Severity),
|
||||
("expires", expiresString),
|
||||
("name", targetName),
|
||||
("ip", addressRangeString),
|
||||
("hwid", hwidString),
|
||||
("reason", reason));
|
||||
("reason", banInfo.Reason));
|
||||
|
||||
_sawmill.Info(logMessage);
|
||||
_chat.SendAdminAlert(logMessage);
|
||||
@@ -172,7 +172,19 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
KickMatchingConnectedPlayers(banDef, "newly placed ban");
|
||||
}
|
||||
|
||||
private void KickMatchingConnectedPlayers(ServerBanDef def, string source)
|
||||
private NoteSeverity GetSeverityForServerBan(CreateBanInfo banInfo, CVarDef<string> defaultCVar)
|
||||
{
|
||||
if (banInfo.Severity != null)
|
||||
return banInfo.Severity.Value;
|
||||
|
||||
if (Enum.TryParse(_cfg.GetCVar(defaultCVar), true, out NoteSeverity parsedSeverity))
|
||||
return parsedSeverity;
|
||||
|
||||
_sawmill.Error($"CVar {defaultCVar.Name} has invalid ban severity!");
|
||||
return NoteSeverity.None;
|
||||
}
|
||||
|
||||
private void KickMatchingConnectedPlayers(BanDef def, string source)
|
||||
{
|
||||
foreach (var player in _playerManager.Sessions)
|
||||
{
|
||||
@@ -184,7 +196,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
}
|
||||
}
|
||||
|
||||
private bool BanMatchesPlayer(ICommonSession player, ServerBanDef ban)
|
||||
private bool BanMatchesPlayer(ICommonSession player, BanDef ban)
|
||||
{
|
||||
var playerInfo = new BanMatcher.PlayerInfo
|
||||
{
|
||||
@@ -201,7 +213,7 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
return BanMatcher.BanMatches(ban, playerInfo);
|
||||
}
|
||||
|
||||
private void KickForBanDef(ICommonSession player, ServerBanDef def)
|
||||
private void KickForBanDef(ICommonSession player, BanDef def)
|
||||
{
|
||||
var message = def.FormatBanMessage(_cfg, _localizationManager);
|
||||
player.Channel.Disconnect(message);
|
||||
@@ -211,108 +223,154 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
#region Role Bans
|
||||
|
||||
// If you are trying to remove timeOfBan, please don't. It's there because the note system groups role bans by time, reason and banning admin.
|
||||
// Removing it will clutter the note list. Please also make sure that department bans are applied to roles with the same DateTimeOffset.
|
||||
public async void CreateRoleBan<T>(
|
||||
NetUserId? target,
|
||||
string? targetUsername,
|
||||
NetUserId? banningAdmin,
|
||||
(IPAddress, int)? addressRange,
|
||||
ImmutableTypedHwid? hwid,
|
||||
ProtoId<T> role,
|
||||
uint? minutes,
|
||||
NoteSeverity severity,
|
||||
string reason,
|
||||
DateTimeOffset timeOfBan
|
||||
) where T : class, IPrototype
|
||||
public async void CreateRoleBan(CreateRoleBanInfo banInfo)
|
||||
{
|
||||
string encodedRole;
|
||||
ImmutableArray<BanRoleDef> roleDefs =
|
||||
[
|
||||
.. ToBanRoleDef(banInfo.JobPrototypes),
|
||||
.. ToBanRoleDef(banInfo.AntagPrototypes),
|
||||
];
|
||||
|
||||
// TODO: Note that it's possible to clash IDs here between a job and an antag. The refactor that introduced
|
||||
// this check has consciously avoided refactoring Job and Antag prototype.
|
||||
// Refactor Job- and Antag- Prototype to introduce a common RolePrototype, which will fix this possible clash.
|
||||
if (roleDefs.Length == 0)
|
||||
throw new ArgumentException("Must specify at least one role to ban!");
|
||||
|
||||
//TODO remove this check as part of the above refactor
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(role) && _prototypeManager.HasIndex<AntagPrototype>(role))
|
||||
var (banDef, expires) = await CreateBanDef(banInfo, BanType.Role, roleDefs);
|
||||
|
||||
await AddRoleBan(banDef);
|
||||
|
||||
var length = expires == null
|
||||
? Loc.GetString("cmd-roleban-inf")
|
||||
: Loc.GetString("cmd-roleban-until", ("expires", expires));
|
||||
|
||||
var targetName = banInfo.Users.Count == 0
|
||||
? "null"
|
||||
: string.Join(", ", banInfo.Users.Select(u => $"{u.UserName} ({u.UserId})"));
|
||||
|
||||
_chat.SendAdminAlert(Loc.GetString(
|
||||
"cmd-roleban-success",
|
||||
("target", targetName),
|
||||
("role", string.Join(", ", roleDefs)),
|
||||
("reason", banInfo.Reason),
|
||||
("length", length)));
|
||||
|
||||
foreach (var (userId, _) in banInfo.Users)
|
||||
{
|
||||
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
|
||||
|
||||
return;
|
||||
if (_playerManager.TryGetSessionById(userId, out var session))
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
// Don't trust the input: make sure the job or antag actually exists.
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(role))
|
||||
encodedRole = PrefixJob + role;
|
||||
else if (_prototypeManager.HasIndex<AntagPrototype>(role))
|
||||
encodedRole = PrefixAntag + role;
|
||||
else
|
||||
{
|
||||
_sawmill.Error($"Creating role ban for {role}: cannot create role ban, role is not a JobPrototype or an AntagPrototype.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
DateTimeOffset? expires = null;
|
||||
|
||||
if (minutes > 0)
|
||||
expires = DateTimeOffset.Now + TimeSpan.FromMinutes(minutes.Value);
|
||||
|
||||
_systems.TryGetEntitySystem(out GameTicker? ticker);
|
||||
int? roundId = ticker == null || ticker.RoundId == 0 ? null : ticker.RoundId;
|
||||
var playtime = target == null ? TimeSpan.Zero : (await _db.GetPlayTimes(target.Value)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)?.TimeSpent ?? TimeSpan.Zero;
|
||||
|
||||
var banDef = new ServerRoleBanDef(
|
||||
null,
|
||||
target,
|
||||
addressRange,
|
||||
hwid,
|
||||
timeOfBan,
|
||||
expires,
|
||||
roundId,
|
||||
playtime,
|
||||
reason,
|
||||
severity,
|
||||
banningAdmin,
|
||||
null,
|
||||
encodedRole);
|
||||
|
||||
if (!await AddRoleBan(banDef))
|
||||
{
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-existing", ("target", targetUsername ?? "null"), ("role", role)));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var length = expires == null ? Loc.GetString("cmd-roleban-inf") : Loc.GetString("cmd-roleban-until", ("expires", expires));
|
||||
_chat.SendAdminAlert(Loc.GetString("cmd-roleban-success", ("target", targetUsername ?? "null"), ("role", role), ("reason", reason), ("length", length)));
|
||||
|
||||
if (target is not null && _playerManager.TryGetSessionById(target.Value, out var session))
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
private async Task<bool> AddRoleBan(ServerRoleBanDef banDef)
|
||||
private async Task<(BanDef Ban, DateTimeOffset? Expires)> CreateBanDef(
|
||||
CreateBanInfo banInfo,
|
||||
BanType type,
|
||||
ImmutableArray<BanRoleDef>? roleBans)
|
||||
{
|
||||
banDef = await _db.AddServerRoleBanAsync(banDef);
|
||||
if (banInfo.Users.Count == 0 && banInfo.HWIds.Count == 0 && banInfo.AddressRanges.Count == 0)
|
||||
throw new ArgumentException("Must specify at least one user, HWID, or address range");
|
||||
|
||||
if (banDef.UserId != null
|
||||
&& _playerManager.TryGetSessionById(banDef.UserId, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
DateTimeOffset? expires = null;
|
||||
if (banInfo.Duration is { } duration)
|
||||
expires = DateTimeOffset.Now + duration;
|
||||
|
||||
ImmutableArray<int> roundIds;
|
||||
if (banInfo.RoundIds.Count > 0)
|
||||
{
|
||||
cachedBans.Add(banDef);
|
||||
roundIds = [..banInfo.RoundIds];
|
||||
}
|
||||
else if (_systems.TryGetEntitySystem<GameTicker>(out var ticker) && ticker.RoundId != 0)
|
||||
{
|
||||
roundIds = [ticker.RoundId];
|
||||
}
|
||||
else
|
||||
{
|
||||
roundIds = [];
|
||||
}
|
||||
|
||||
return true;
|
||||
return (new BanDef(
|
||||
null,
|
||||
type,
|
||||
[..banInfo.Users.Select(u => u.UserId)],
|
||||
[..banInfo.AddressRanges],
|
||||
[..banInfo.HWIds],
|
||||
DateTimeOffset.Now,
|
||||
expires,
|
||||
roundIds,
|
||||
await GetPlayTime(banInfo),
|
||||
banInfo.Reason,
|
||||
GetSeverityForServerBan(banInfo, CCVars.ServerBanDefaultSeverity),
|
||||
banInfo.BanningAdmin,
|
||||
null,
|
||||
roles: roleBans), expires);
|
||||
}
|
||||
|
||||
private async Task<TimeSpan> GetPlayTime(CreateBanInfo banInfo)
|
||||
{
|
||||
var firstPlayer = banInfo.Users.FirstOrNull()?.UserId;
|
||||
if (firstPlayer == null)
|
||||
return TimeSpan.Zero;
|
||||
|
||||
return (await _db.GetPlayTimes(firstPlayer.Value))
|
||||
.Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall)
|
||||
?.TimeSpent ?? TimeSpan.Zero;
|
||||
}
|
||||
|
||||
private IEnumerable<BanRoleDef> ToBanRoleDef<T>(IEnumerable<ProtoId<T>> protoIds) where T : class, IPrototype
|
||||
{
|
||||
return protoIds.Select(protoId =>
|
||||
{
|
||||
// TODO: I have no idea if this check is necessary. The previous code was a complete mess,
|
||||
// so out of safety I'm leaving this in.
|
||||
if (_prototypeManager.HasIndex<JobPrototype>(protoId) && _prototypeManager.HasIndex<AntagPrototype>(protoId))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Creating role ban for {protoId}: cannot create role ban, role is both JobPrototype and AntagPrototype.");
|
||||
}
|
||||
|
||||
// Don't trust the input: make sure the role actually exists.
|
||||
if (!_prototypeManager.HasIndex(protoId))
|
||||
throw new UnknownPrototypeException(protoId, typeof(T));
|
||||
|
||||
return new BanRoleDef(PrototypeKindToDbType<T>(), protoId);
|
||||
});
|
||||
}
|
||||
|
||||
private static string PrototypeKindToDbType<T>() where T : class, IPrototype
|
||||
{
|
||||
if (typeof(T) == typeof(JobPrototype))
|
||||
return DbTypeJob;
|
||||
|
||||
if (typeof(T) == typeof(AntagPrototype))
|
||||
return DbTypeAntag;
|
||||
|
||||
throw new ArgumentException($"Unknown prototype kind for role bans: {typeof(T)}");
|
||||
}
|
||||
|
||||
private async Task AddRoleBan(BanDef banDef)
|
||||
{
|
||||
banDef = await _db.AddBanAsync(banDef);
|
||||
|
||||
foreach (var user in banDef.UserIds)
|
||||
{
|
||||
if (_playerManager.TryGetSessionById(user, out var player)
|
||||
&& _cachedRoleBans.TryGetValue(player, out var cachedBans))
|
||||
{
|
||||
cachedBans.Add(banDef);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> PardonRoleBan(int banId, NetUserId? unbanningAdmin, DateTimeOffset unbanTime)
|
||||
{
|
||||
var ban = await _db.GetServerRoleBanAsync(banId);
|
||||
var ban = await _db.GetBanAsync(banId);
|
||||
|
||||
if (ban == null)
|
||||
{
|
||||
return $"No ban found with id {banId}";
|
||||
}
|
||||
|
||||
if (ban.Type != BanType.Role)
|
||||
throw new InvalidOperationException("Ban was not a role ban!");
|
||||
|
||||
if (ban.Unban != null)
|
||||
{
|
||||
var response = new StringBuilder("This ban has already been pardoned");
|
||||
@@ -326,14 +384,17 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
return response.ToString();
|
||||
}
|
||||
|
||||
await _db.AddServerRoleUnbanAsync(new ServerRoleUnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
|
||||
await _db.AddUnbanAsync(new UnbanDef(banId, unbanningAdmin, DateTimeOffset.Now));
|
||||
|
||||
if (ban.UserId is { } player
|
||||
&& _playerManager.TryGetSessionById(player, out var session)
|
||||
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
foreach (var user in ban.UserIds)
|
||||
{
|
||||
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(session);
|
||||
if (_playerManager.TryGetSessionById(user, out var session)
|
||||
&& _cachedRoleBans.TryGetValue(session, out var roleBans))
|
||||
{
|
||||
roleBans.RemoveAll(roleBan => roleBan.Id == ban.Id);
|
||||
SendRoleBans(session);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $"Pardoned ban with id {banId}";
|
||||
@@ -341,64 +402,69 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public HashSet<ProtoId<JobPrototype>>? GetJobBans(NetUserId playerUserId)
|
||||
{
|
||||
return GetRoleBans<JobPrototype>(playerUserId, PrefixJob);
|
||||
return GetRoleBans<JobPrototype>(playerUserId);
|
||||
}
|
||||
|
||||
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId)
|
||||
{
|
||||
return GetRoleBans<AntagPrototype>(playerUserId, PrefixAntag);
|
||||
return GetRoleBans<AntagPrototype>(playerUserId);
|
||||
}
|
||||
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId, string prefix) where T : class, IPrototype
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(NetUserId playerUserId) where T : class, IPrototype
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return GetRoleBans<T>(session, prefix);
|
||||
return GetRoleBans<T>(session);
|
||||
}
|
||||
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession, string prefix) where T : class, IPrototype
|
||||
private HashSet<ProtoId<T>>? GetRoleBans<T>(ICommonSession playerSession) where T : class, IPrototype
|
||||
{
|
||||
if (!_cachedRoleBans.TryGetValue(playerSession, out var roleBans))
|
||||
return null;
|
||||
|
||||
var dbType = PrototypeKindToDbType<T>();
|
||||
|
||||
return roleBans
|
||||
.Where(ban => ban.Role.StartsWith(prefix, StringComparison.Ordinal))
|
||||
.Select(ban => new ProtoId<T>(ban.Role[prefix.Length..]))
|
||||
.SelectMany(ban => ban.Roles!.Value)
|
||||
.Where(role => role.RoleType == dbType)
|
||||
.Select(role => new ProtoId<T>(role.RoleId))
|
||||
.ToHashSet();
|
||||
}
|
||||
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId)
|
||||
public HashSet<BanRoleDef>? GetRoleBans(NetUserId playerUserId)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(playerUserId, out var session))
|
||||
return null;
|
||||
|
||||
return _cachedRoleBans.TryGetValue(session, out var roleBans)
|
||||
? roleBans.Select(banDef => banDef.Role).ToHashSet()
|
||||
? roleBans.SelectMany(banDef => banDef.Roles ?? []).ToHashSet()
|
||||
: null;
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<JobPrototype>> jobs)
|
||||
{
|
||||
return IsRoleBanned(player, jobs, PrefixJob);
|
||||
return IsRoleBanned<JobPrototype>(player, jobs);
|
||||
}
|
||||
|
||||
public bool IsRoleBanned(ICommonSession player, List<ProtoId<AntagPrototype>> antags)
|
||||
{
|
||||
return IsRoleBanned(player, antags, PrefixAntag);
|
||||
return IsRoleBanned<AntagPrototype>(player, antags);
|
||||
}
|
||||
|
||||
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles, string prefix) where T : class, IPrototype
|
||||
private bool IsRoleBanned<T>(ICommonSession player, List<ProtoId<T>> roles) where T : class, IPrototype
|
||||
{
|
||||
var bans = GetRoleBans(player.UserId);
|
||||
|
||||
if (bans is null || bans.Count == 0)
|
||||
return false;
|
||||
|
||||
var dbType = PrototypeKindToDbType<T>();
|
||||
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var role in roles)
|
||||
{
|
||||
if (bans.Contains(prefix + role))
|
||||
if (bans.Contains(new BanRoleDef(dbType, role)))
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -407,34 +473,10 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
|
||||
public void SendRoleBans(ICommonSession pSession)
|
||||
{
|
||||
var jobBans = GetRoleBans<JobPrototype>(pSession, PrefixJob);
|
||||
var jobBansList = new List<string>(jobBans?.Count ?? 0);
|
||||
|
||||
if (jobBans is not null)
|
||||
{
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var encodedId in jobBans)
|
||||
{
|
||||
jobBansList.Add(encodedId.ToString().Replace(PrefixJob, ""));
|
||||
}
|
||||
}
|
||||
|
||||
var antagBans = GetRoleBans<AntagPrototype>(pSession, PrefixAntag);
|
||||
var antagBansList = new List<string>(antagBans?.Count ?? 0);
|
||||
|
||||
if (antagBans is not null)
|
||||
{
|
||||
// ReSharper disable once ForeachCanBeConvertedToQueryUsingAnotherGetEnumerator
|
||||
foreach (var encodedId in antagBans)
|
||||
{
|
||||
antagBansList.Add(encodedId.ToString().Replace(PrefixAntag, ""));
|
||||
}
|
||||
}
|
||||
|
||||
var bans = new MsgRoleBans()
|
||||
{
|
||||
JobBans = jobBansList,
|
||||
AntagBans = antagBansList,
|
||||
JobBans = (GetRoleBans<JobPrototype>(pSession) ?? []).ToList(),
|
||||
AntagBans = (GetRoleBans<AntagPrototype>(pSession) ?? []).ToList(),
|
||||
};
|
||||
|
||||
_sawmill.Debug($"Sent role bans to {pSession.Name}");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Roles;
|
||||
@@ -13,6 +14,11 @@ public interface IBanManager
|
||||
public void Initialize();
|
||||
public void Restart();
|
||||
|
||||
/// <summary>
|
||||
/// Create a server ban in the database, blocking connection for matching players.
|
||||
/// </summary>
|
||||
void CreateServerBan(CreateServerBanInfo banInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Bans the specified target, address range and / or HWID. One of them must be non-null
|
||||
/// </summary>
|
||||
@@ -23,12 +29,44 @@ public interface IBanManager
|
||||
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
|
||||
/// <param name="severity">Severity of the resulting ban note</param>
|
||||
/// <param name="reason">Reason for the ban</param>
|
||||
public void CreateServerBan(NetUserId? target, string? targetUsername, NetUserId? banningAdmin, (IPAddress, int)? addressRange, ImmutableTypedHwid? hwid, uint? minutes, NoteSeverity severity, string reason);
|
||||
[Obsolete("Use CreateServerBan(CreateBanInfo) instead")]
|
||||
public void CreateServerBan(NetUserId? target,
|
||||
string? targetUsername,
|
||||
NetUserId? banningAdmin,
|
||||
(IPAddress, int)? addressRange,
|
||||
ImmutableTypedHwid? hwid,
|
||||
uint? minutes,
|
||||
NoteSeverity severity,
|
||||
string reason)
|
||||
{
|
||||
var info = new CreateServerBanInfo(reason);
|
||||
if (target != null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(targetUsername);
|
||||
info.AddUser(target.Value, targetUsername);
|
||||
}
|
||||
|
||||
if (addressRange != null)
|
||||
info.AddAddressRange(addressRange.Value);
|
||||
|
||||
if (hwid != null)
|
||||
info.AddHWId(hwid);
|
||||
|
||||
if (minutes > 0)
|
||||
info.WithMinutes(minutes.Value);
|
||||
|
||||
if (banningAdmin != null)
|
||||
info.WithBanningAdmin(banningAdmin.Value);
|
||||
|
||||
info.WithSeverity(severity);
|
||||
|
||||
CreateServerBan(info);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of prefixed prototype IDs with the player's role bans.
|
||||
/// </summary>
|
||||
public HashSet<string>? GetRoleBans(NetUserId playerUserId);
|
||||
public HashSet<BanRoleDef>? GetRoleBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the player is currently banned from any of the listed roles.
|
||||
@@ -57,33 +95,12 @@ public interface IBanManager
|
||||
public HashSet<ProtoId<AntagPrototype>>? GetAntagBans(NetUserId playerUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a job ban for the specified target, username or GUID
|
||||
/// Creates a role ban, preventing matching players from playing said roles.
|
||||
/// </summary>
|
||||
/// <param name="target">Target user, username or GUID, null for none</param>
|
||||
/// <param name="targetUsername">The username of the target, if known</param>
|
||||
/// <param name="banningAdmin">The responsible admin for the ban</param>
|
||||
/// <param name="addressRange">The range of IPs that are to be banned, if known</param>
|
||||
/// <param name="hwid">The HWID to be banned, if known</param>
|
||||
/// <param name="role">The role ID to be banned from. Either an AntagPrototype or a JobPrototype</param>
|
||||
/// <param name="minutes">Number of minutes to ban for. 0 and null mean permanent</param>
|
||||
/// <param name="severity">Severity of the resulting ban note</param>
|
||||
/// <param name="reason">Reason for the ban</param>
|
||||
/// <param name="timeOfBan">Time when the ban was applied, used for grouping role bans</param>
|
||||
public void CreateRoleBan<T>(
|
||||
NetUserId? target,
|
||||
string? targetUsername,
|
||||
NetUserId? banningAdmin,
|
||||
(IPAddress, int)? addressRange,
|
||||
ImmutableTypedHwid? hwid,
|
||||
ProtoId<T> role,
|
||||
uint? minutes,
|
||||
NoteSeverity severity,
|
||||
string reason,
|
||||
DateTimeOffset timeOfBan
|
||||
) where T : class, IPrototype;
|
||||
public void CreateRoleBan(CreateRoleBanInfo banInfo);
|
||||
|
||||
/// <summary>
|
||||
/// Pardons a role ban for the specified target, username or GUID
|
||||
/// Pardons a role ban by its ID.
|
||||
/// </summary>
|
||||
/// <param name="banId">The id of the role ban to pardon.</param>
|
||||
/// <param name="unbanningAdmin">The admin, if any, that pardoned the role ban.</param>
|
||||
@@ -96,3 +113,287 @@ public interface IBanManager
|
||||
/// <param name="pSession">Player's session</param>
|
||||
public void SendRoleBans(ICommonSession pSession);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base info to fill out in created ban records.
|
||||
/// </summary>
|
||||
/// <seealso cref="CreateServerBanInfo"/>
|
||||
/// <seealso cref="CreateRoleBanInfo"/>
|
||||
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
|
||||
public abstract class CreateBanInfo
|
||||
{
|
||||
[Access(Other = AccessPermissions.Read)]
|
||||
public const int DefaultMaskIpv4 = 32;
|
||||
[Access(Other = AccessPermissions.Read)]
|
||||
public const int DefaultMaskIpv6 = 64;
|
||||
|
||||
internal readonly HashSet<(NetUserId UserId, string UserName)> Users = [];
|
||||
internal readonly HashSet<(IPAddress Address, int Mask)> AddressRanges = [];
|
||||
internal readonly HashSet<ImmutableTypedHwid> HWIds = [];
|
||||
internal readonly HashSet<int> RoundIds = [];
|
||||
internal TimeSpan? Duration;
|
||||
internal NoteSeverity? Severity;
|
||||
internal string Reason;
|
||||
internal NetUserId? BanningAdmin;
|
||||
|
||||
protected CreateBanInfo(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a user to be matched by the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bans can target multiple users at once.
|
||||
/// </remarks>
|
||||
/// <param name="userId">The ID of the user.</param>
|
||||
/// <param name="username">The name of the user (used for logging purposes).</param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddUser(NetUserId userId, string username)
|
||||
{
|
||||
Users.Add((userId, username));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an IP address to be matched by the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bans can target multiple addresses at once.
|
||||
/// </remarks>
|
||||
/// <param name="address">
|
||||
/// The IP address to add. If null, nothing is done.
|
||||
/// </param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddAddress(IPAddress? address)
|
||||
{
|
||||
if (address == null)
|
||||
return this;
|
||||
|
||||
return AddAddressRange(
|
||||
address,
|
||||
address.AddressFamily == AddressFamily.InterNetwork ? DefaultMaskIpv4 : DefaultMaskIpv6);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an IP address range to be matched by the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bans can target multiple address ranges at once.
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddAddressRange((IPAddress Address, int Mask) addressRange)
|
||||
{
|
||||
return AddAddressRange(addressRange.Address, addressRange.Mask);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an IP address range to be matched by the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bans can target multiple address ranges at once.
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddAddressRange(IPAddress address, int mask)
|
||||
{
|
||||
AddressRanges.Add((address, mask));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a hardware IP (HWID) to be matched by the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Bans can target multiple HWIDs at once.
|
||||
/// </remarks>
|
||||
/// <param name="hwId">
|
||||
/// The HWID to add. If null, nothing is done.
|
||||
/// </param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddHWId(ImmutableTypedHwid? hwId)
|
||||
{
|
||||
if (hwId != null)
|
||||
HWIds.Add(hwId);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a relevant round ID to this ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If not specified, the current round ID is used for the ban.
|
||||
/// Therefore, the first call to this function will <i>replace</i> the round ID,
|
||||
/// and further calls will add additional round IDs.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Bans can target multiple round IDs at once.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo AddRoundId(int roundId)
|
||||
{
|
||||
RoundIds.Add(roundId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set how long the ban will last, in minutes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no duration is specified, the ban is permanent.
|
||||
/// </remarks>
|
||||
/// <param name="minutes">The duration of the ban, in minutes.</param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if <see cref="minutes"/> is not a positive number.
|
||||
/// </exception>
|
||||
public CreateBanInfo WithMinutes(int minutes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
|
||||
return WithMinutes((uint)minutes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set how long the ban will last, in minutes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no duration is specified, the ban is permanent.
|
||||
/// </remarks>
|
||||
/// <param name="minutes">The duration of the ban, in minutes.</param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if <see cref="minutes"/> is not a positive number.
|
||||
/// </exception>
|
||||
public CreateBanInfo WithMinutes(uint minutes)
|
||||
{
|
||||
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(minutes);
|
||||
return WithDuration(TimeSpan.FromMinutes(minutes));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set how long the ban will last.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no duration is specified, the ban is permanent.
|
||||
/// </remarks>
|
||||
/// <param name="duration">The duration of the ban.</param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
/// <exception cref="ArgumentOutOfRangeException">
|
||||
/// Thrown if <see cref="duration"/> is not a positive amount of time.
|
||||
/// </exception>
|
||||
public CreateBanInfo WithDuration(TimeSpan duration)
|
||||
{
|
||||
if (duration <= TimeSpan.Zero)
|
||||
throw new ArgumentOutOfRangeException(nameof(duration), "Duration must be greater than zero.");
|
||||
|
||||
Duration = duration;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the severity of the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If no severity is specified, the default is specified through server configuration.
|
||||
/// </remarks>
|
||||
/// <param name="severity"></param>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo WithSeverity(NoteSeverity severity)
|
||||
{
|
||||
Severity = severity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set the reason for the ban.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This replaces the value given via the object constructor.
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo WithReason(string reason)
|
||||
{
|
||||
Reason = reason;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specify the admin responsible for placing the ban.
|
||||
/// </summary>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateBanInfo WithBanningAdmin(NetUserId? banningAdmin)
|
||||
{
|
||||
BanningAdmin = banningAdmin;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores info to create server ban records.
|
||||
/// </summary>
|
||||
/// <seealso cref="IBanManager.CreateServerBan(CreateServerBanInfo)"/>
|
||||
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
|
||||
public sealed class CreateServerBanInfo : CreateBanInfo
|
||||
{
|
||||
/// <param name="reason">The reason for the server ban.</param>
|
||||
public CreateServerBanInfo(string reason) : base(reason)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stores info to create role ban records.
|
||||
/// </summary>
|
||||
/// <seealso cref="IBanManager.CreateRoleBan(CreateRoleBanInfo)"/>
|
||||
[Access(typeof(BanManager), Other = AccessPermissions.Execute)]
|
||||
public sealed class CreateRoleBanInfo : CreateBanInfo
|
||||
{
|
||||
internal readonly HashSet<ProtoId<AntagPrototype>> AntagPrototypes = [];
|
||||
internal readonly HashSet<ProtoId<JobPrototype>> JobPrototypes = [];
|
||||
|
||||
/// <param name="reason">The reason for the role ban.</param>
|
||||
public CreateRoleBanInfo(string reason) : base(reason)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add an antag role that will be unavailable for banned players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Bans can have multiple roles at once.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// While not checked in this function, adding a ban with invalid role IDs will cause a
|
||||
/// <see cref="UnknownPrototypeException"/> when actually creating the ban.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateRoleBanInfo AddAntag(ProtoId<AntagPrototype> protoId)
|
||||
{
|
||||
AntagPrototypes.Add(protoId);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a job role that will be unavailable for banned players.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Bans can have multiple roles at once.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// While not checked in this function, adding a ban with invalid role IDs will cause a
|
||||
/// <see cref="UnknownPrototypeException"/> when actually creating the ban.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <returns>The current object, for easy chaining.</returns>
|
||||
public CreateRoleBanInfo AddJob(ProtoId<JobPrototype> protoId)
|
||||
{
|
||||
JobPrototypes.Add(protoId);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class AdminNotesEui : BaseEui
|
||||
IoCManager.InjectDependencies(this);
|
||||
}
|
||||
|
||||
private Guid NotedPlayer { get; set; }
|
||||
private NetUserId NotedPlayer { get; set; }
|
||||
private string NotedPlayerName { get; set; } = string.Empty;
|
||||
private bool HasConnectedBefore { get; set; }
|
||||
private Dictionary<(int, NoteType), SharedAdminNote> Notes { get; set; } = new();
|
||||
@@ -112,7 +112,7 @@ public sealed class AdminNotesEui : BaseEui
|
||||
}
|
||||
}
|
||||
|
||||
public async Task ChangeNotedPlayer(Guid notedPlayer)
|
||||
public async Task ChangeNotedPlayer(NetUserId notedPlayer)
|
||||
{
|
||||
NotedPlayer = notedPlayer;
|
||||
await LoadFromDb();
|
||||
@@ -120,7 +120,7 @@ public sealed class AdminNotesEui : BaseEui
|
||||
|
||||
private void NoteModified(SharedAdminNote note)
|
||||
{
|
||||
if (note.Player != NotedPlayer)
|
||||
if (!note.Players.Contains(NotedPlayer))
|
||||
return;
|
||||
|
||||
Notes[(note.Id, note.NoteType)] = note;
|
||||
@@ -129,7 +129,7 @@ public sealed class AdminNotesEui : BaseEui
|
||||
|
||||
private void NoteDeleted(SharedAdminNote note)
|
||||
{
|
||||
if (note.Player != NotedPlayer)
|
||||
if (!note.Players.Contains(NotedPlayer))
|
||||
return;
|
||||
|
||||
Notes.Remove((note.Id, note.NoteType));
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Administration.Notes;
|
||||
using Content.Shared.Database;
|
||||
@@ -11,7 +13,7 @@ public static class AdminNotesExtensions
|
||||
NoteSeverity? severity = null;
|
||||
var secret = false;
|
||||
NoteType type;
|
||||
string[]? bannedRoles = null;
|
||||
ImmutableArray<BanRoleDef>? bannedRoles = null;
|
||||
string? unbannedByName = null;
|
||||
DateTime? unbannedTime = null;
|
||||
bool? seen = null;
|
||||
@@ -30,13 +32,13 @@ public static class AdminNotesExtensions
|
||||
type = NoteType.Message;
|
||||
seen = adminMessage.Seen;
|
||||
break;
|
||||
case ServerBanNoteRecord ban:
|
||||
case BanNoteRecord { Type: BanType.Server } ban:
|
||||
type = NoteType.ServerBan;
|
||||
severity = ban.Severity;
|
||||
unbannedTime = ban.UnbanTime;
|
||||
unbannedByName = ban.UnbanningAdmin?.LastSeenUserName ?? Loc.GetString("system-user");
|
||||
break;
|
||||
case ServerRoleBanNoteRecord roleBan:
|
||||
case BanNoteRecord { Type: BanType.Role } roleBan:
|
||||
type = NoteType.RoleBan;
|
||||
severity = roleBan.Severity;
|
||||
bannedRoles = roleBan.Roles;
|
||||
@@ -48,14 +50,14 @@ public static class AdminNotesExtensions
|
||||
}
|
||||
|
||||
// There may be bans without a user, but why would we ever be converting them to shared notes?
|
||||
if (note.Player is null)
|
||||
throw new ArgumentNullException(nameof(note), "Player user ID cannot be null for a note");
|
||||
if (note.Players.Length == 0)
|
||||
throw new ArgumentNullException(nameof(note), "Player user ID cannot be empty for a note");
|
||||
|
||||
return new SharedAdminNote(
|
||||
note.Id,
|
||||
note.Player!.UserId,
|
||||
note.Round?.Id,
|
||||
note.Round?.Server.Name,
|
||||
[..note.Players.Select(p => p.UserId)],
|
||||
[..note.Rounds.Select(r => r.Id)],
|
||||
note.Rounds.SingleOrDefault()?.Server.Name, // TODO: Show all server names?
|
||||
note.PlaytimeAtNote,
|
||||
type,
|
||||
note.Message,
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
|
||||
return _admins.HasAdminFlag(admin, AdminFlags.ViewNotes);
|
||||
}
|
||||
|
||||
public async Task OpenEui(ICommonSession admin, Guid notedPlayer)
|
||||
public async Task OpenEui(ICommonSession admin, NetUserId notedPlayer)
|
||||
{
|
||||
var ui = new AdminNotesEui();
|
||||
_euis.OpenEui(ui, admin);
|
||||
@@ -144,8 +144,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
|
||||
|
||||
var note = new SharedAdminNote(
|
||||
noteId,
|
||||
(NetUserId) player,
|
||||
roundId,
|
||||
[(NetUserId) player],
|
||||
roundId.HasValue ? [roundId.Value] : [],
|
||||
serverName,
|
||||
playtime,
|
||||
type,
|
||||
@@ -172,8 +172,7 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
|
||||
NoteType.Note => (await _db.GetAdminNote(id))?.ToShared(),
|
||||
NoteType.Watchlist => (await _db.GetAdminWatchlist(id))?.ToShared(),
|
||||
NoteType.Message => (await _db.GetAdminMessage(id))?.ToShared(),
|
||||
NoteType.ServerBan => (await _db.GetServerBanAsNoteAsync(id))?.ToShared(),
|
||||
NoteType.RoleBan => (await _db.GetServerRoleBanAsNoteAsync(id))?.ToShared(),
|
||||
NoteType.ServerBan or NoteType.RoleBan => (await _db.GetBanAsNoteAsync(id))?.ToShared(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type")
|
||||
};
|
||||
}
|
||||
@@ -200,11 +199,8 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
|
||||
case NoteType.Message:
|
||||
await _db.DeleteAdminMessage(noteId, deletedBy.UserId, deletedAt);
|
||||
break;
|
||||
case NoteType.ServerBan:
|
||||
await _db.HideServerBanFromNotes(noteId, deletedBy.UserId, deletedAt);
|
||||
break;
|
||||
case NoteType.RoleBan:
|
||||
await _db.HideServerRoleBanFromNotes(noteId, deletedBy.UserId, deletedAt);
|
||||
case NoteType.ServerBan or NoteType.RoleBan:
|
||||
await _db.HideBanFromNotes(noteId, deletedBy.UserId, deletedAt);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type");
|
||||
@@ -280,15 +276,10 @@ public sealed class AdminNotesManager : IAdminNotesManager, IPostInjectInit
|
||||
case NoteType.Message:
|
||||
await _db.EditAdminMessage(noteId, message, editedBy.UserId, editedAt, expiryTime);
|
||||
break;
|
||||
case NoteType.ServerBan:
|
||||
case NoteType.ServerBan or NoteType.RoleBan:
|
||||
if (severity is null)
|
||||
throw new ArgumentException("Severity cannot be null for a ban", nameof(severity));
|
||||
await _db.EditServerBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt);
|
||||
break;
|
||||
case NoteType.RoleBan:
|
||||
if (severity is null)
|
||||
throw new ArgumentException("Severity cannot be null for a role ban", nameof(severity));
|
||||
await _db.EditServerRoleBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt);
|
||||
await _db.EditBan(noteId, message, severity.Value, expiryTime, editedBy.UserId, editedAt);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(type), type, "Unknown note type");
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Threading.Tasks;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.Administration.Notes;
|
||||
using Content.Shared.Database;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Administration.Notes;
|
||||
@@ -16,7 +17,7 @@ public interface IAdminNotesManager
|
||||
bool CanDelete(ICommonSession admin);
|
||||
bool CanEdit(ICommonSession admin);
|
||||
bool CanView(ICommonSession admin);
|
||||
Task OpenEui(ICommonSession admin, Guid notedPlayer);
|
||||
Task OpenEui(ICommonSession admin, NetUserId notedPlayer);
|
||||
Task OpenUserNotesEui(ICommonSession player);
|
||||
Task AddAdminRemark(ICommonSession createdBy, Guid player, NoteType type, string message, NoteSeverity? severity, bool secret, DateTime? expiryTime);
|
||||
Task DeleteAdminRemark(int noteId, NoteType type, ICommonSession deletedBy);
|
||||
|
||||
@@ -186,11 +186,8 @@ public sealed class PlayerPanelEui : BaseEui
|
||||
{
|
||||
_whitelisted = await _db.GetWhitelistStatusAsync(_targetPlayer.UserId);
|
||||
// This won't get associated ip or hwid bans but they were not placed on this account anyways
|
||||
_bans = (await _db.GetServerBansAsync(null, _targetPlayer.UserId, null, null)).Count;
|
||||
// Unfortunately role bans for departments and stuff are issued individually. This means that a single role ban can have many individual role bans internally
|
||||
// The only way to distinguish whether a role ban is the same is to compare the ban time.
|
||||
// This is horrible and I would love to just erase the database and start from scratch instead but that's what I can do for now.
|
||||
_roleBans = (await _db.GetServerRoleBansAsync(null, _targetPlayer.UserId, null, null)).DistinctBy(rb => rb.BanTime).Count();
|
||||
_bans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null)).Count;
|
||||
_roleBans = (await _db.GetBansAsync(null, _targetPlayer.UserId, null, null, type: BanType.Role)).Count();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -172,7 +172,7 @@ namespace Content.Server.Administration.Systems
|
||||
}
|
||||
|
||||
// Check if the user has been banned
|
||||
var ban = await _dbManager.GetServerBanAsync(null, e.Session.UserId, null, null);
|
||||
var ban = await _dbManager.GetBanAsync(null, e.Session.UserId, null, null);
|
||||
if (ban != null)
|
||||
{
|
||||
var banMessage = Loc.GetString("bwoink-system-player-banned", ("banReason", ban.Reason));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user