Merge branch 'master' into bow-and-quiver
This commit is contained in:
2
.github/workflows/build-docfx.yml
vendored
2
.github/workflows/build-docfx.yml
vendored
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/build-map-renderer.yml
vendored
2
.github/workflows/build-map-renderer.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/build-test-debug.yml
vendored
2
.github/workflows/build-test-debug.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Get Engine Tag
|
||||
run: |
|
||||
|
||||
2
.github/workflows/test-packaging.yml
vendored
2
.github/workflows/test-packaging.yml
vendored
@@ -51,7 +51,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
|
||||
2
.github/workflows/yaml-linter.yml
vendored
2
.github/workflows/yaml-linter.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup .NET Core
|
||||
uses: actions/setup-dotnet@v3.2.0
|
||||
with:
|
||||
dotnet-version: 8.0.x
|
||||
dotnet-version: 9.0.x
|
||||
- name: Install dependencies
|
||||
run: dotnet restore
|
||||
- name: Build
|
||||
|
||||
@@ -88,8 +88,9 @@ namespace Content.Client.Access.UI
|
||||
button.Disabled = !interfaceEnabled;
|
||||
if (interfaceEnabled)
|
||||
{
|
||||
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains(accessName) ?? false;
|
||||
button.Disabled = (!state.AllowedModifyAccessList?.Contains(accessName)) ?? true;
|
||||
// Explicit cast because Rider gives a false error otherwise.
|
||||
button.Pressed = state.TargetAccessReaderIdAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName) ?? false;
|
||||
button.Disabled = (!state.AllowedModifyAccessList?.Contains((ProtoId<AccessLevelPrototype>) accessName)) ?? true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,21 @@
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Client.Administration.Systems;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Mind;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Administration;
|
||||
|
||||
internal sealed class AdminNameOverlay : Overlay
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _config = default!;
|
||||
|
||||
private readonly AdminSystem _system;
|
||||
private readonly IEntityManager _entityManager;
|
||||
private readonly IEyeManager _eyeManager;
|
||||
@@ -18,8 +23,16 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
private readonly IUserInterfaceManager _userInterfaceManager;
|
||||
private readonly Font _font;
|
||||
|
||||
//TODO make this adjustable via GUI
|
||||
private readonly ProtoId<RoleTypePrototype>[] _filter =
|
||||
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
|
||||
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
|
||||
private readonly Color _antagColorClassic = Color.OrangeRed;
|
||||
|
||||
public AdminNameOverlay(AdminSystem system, IEntityManager entityManager, IEyeManager eyeManager, IResourceCache resourceCache, EntityLookupSystem entityLookup, IUserInterfaceManager userInterfaceManager)
|
||||
{
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_system = system;
|
||||
_entityManager = entityManager;
|
||||
_eyeManager = eyeManager;
|
||||
@@ -35,6 +48,9 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
{
|
||||
var viewport = args.WorldAABB;
|
||||
|
||||
//TODO make this adjustable via GUI
|
||||
var classic = _config.GetCVar(CCVars.AdminOverlayClassic);
|
||||
|
||||
foreach (var playerInfo in _system.PlayerList)
|
||||
{
|
||||
var entity = _entityManager.GetEntity(playerInfo.NetEntity);
|
||||
@@ -64,12 +80,20 @@ internal sealed class AdminNameOverlay : Overlay
|
||||
var screenCoordinates = _eyeManager.WorldToScreen(aabb.Center +
|
||||
new Angle(-_eyeManager.CurrentEye.Rotation).RotateVec(
|
||||
aabb.TopRight - aabb.Center)) + new Vector2(1f, 7f);
|
||||
if (playerInfo.Antag)
|
||||
|
||||
if (classic && playerInfo.Antag)
|
||||
{
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), "ANTAG", uiScale, Color.OrangeRed);
|
||||
;
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), _antagLabelClassic, uiScale, _antagColorClassic);
|
||||
}
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates+lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
else if (!classic && _filter.Contains(playerInfo.RoleProto.ID))
|
||||
{
|
||||
var label = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
|
||||
var color = playerInfo.RoleProto.Color;
|
||||
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + (lineoffset * 2), label, uiScale, color);
|
||||
}
|
||||
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates + lineoffset, playerInfo.Username, uiScale, playerInfo.Connected ? Color.Yellow : Color.White);
|
||||
args.ScreenHandle.DrawString(_font, screenCoordinates, playerInfo.CharacterName, uiScale, playerInfo.Connected ? Color.Aquamarine : Color.White);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
xmlns:tabs="clr-namespace:Content.Client.Administration.UI.Tabs"
|
||||
xmlns:playerTab="clr-namespace:Content.Client.Administration.UI.Tabs.PlayerTab"
|
||||
xmlns:objectsTab="clr-namespace:Content.Client.Administration.UI.Tabs.ObjectsTab"
|
||||
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab"
|
||||
xmlns:baby="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab">
|
||||
xmlns:panic="clr-namespace:Content.Client.Administration.UI.Tabs.PanicBunkerTab">
|
||||
<TabContainer Name="MasterTabContainer">
|
||||
<adminTab:AdminTab />
|
||||
<adminbusTab:AdminbusTab />
|
||||
@@ -15,7 +14,6 @@
|
||||
<tabs:RoundTab />
|
||||
<tabs:ServerTab />
|
||||
<panic:PanicBunkerTab Name="PanicBunkerControl" Access="Public" />
|
||||
<baby:BabyJailTab Name="BabyJailControl" Access="Public" />
|
||||
<playerTab:PlayerTab Name="PlayerTabControl" Access="Public" />
|
||||
<objectsTab:ObjectsTab Name="ObjectsTabControl" Access="Public" />
|
||||
</TabContainer>
|
||||
|
||||
@@ -21,10 +21,6 @@ public sealed partial class AdminMenuWindow : DefaultWindow
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Round, Loc.GetString("admin-menu-round-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Server, Loc.GetString("admin-menu-server-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.PanicBunker, Loc.GetString("admin-menu-panic-bunker-tab"));
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.BabyJail, Loc.GetString("admin-menu-baby-jail-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Players, Loc.GetString("admin-menu-players-tab"));
|
||||
MasterTabContainer.SetTabTitle((int) TabIndex.Objects, Loc.GetString("admin-menu-objects-tab"));
|
||||
MasterTabContainer.OnTabChanged += OnTabChanged;
|
||||
@@ -52,7 +48,6 @@ public sealed partial class AdminMenuWindow : DefaultWindow
|
||||
Round,
|
||||
Server,
|
||||
PanicBunker,
|
||||
BabyJail,
|
||||
Players,
|
||||
Objects,
|
||||
}
|
||||
|
||||
@@ -130,6 +130,7 @@ namespace Content.Client.Administration.UI
|
||||
}
|
||||
|
||||
var title = string.IsNullOrWhiteSpace(popup.TitleEdit.Text) ? null : popup.TitleEdit.Text;
|
||||
var suspended = popup.SuspendedCheckbox.Pressed;
|
||||
|
||||
if (popup.SourceData is { } src)
|
||||
{
|
||||
@@ -139,7 +140,8 @@ namespace Content.Client.Administration.UI
|
||||
Title = title,
|
||||
PosFlags = pos,
|
||||
NegFlags = neg,
|
||||
RankId = rank
|
||||
RankId = rank,
|
||||
Suspended = suspended,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -152,7 +154,8 @@ namespace Content.Client.Administration.UI
|
||||
Title = title,
|
||||
PosFlags = pos,
|
||||
NegFlags = neg,
|
||||
RankId = rank
|
||||
RankId = rank,
|
||||
Suspended = suspended,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,7 +174,7 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
Id = src,
|
||||
Flags = flags,
|
||||
Name = name
|
||||
Name = name,
|
||||
});
|
||||
}
|
||||
else
|
||||
@@ -351,6 +354,7 @@ namespace Content.Client.Administration.UI
|
||||
public readonly OptionButton RankButton;
|
||||
public readonly Button SaveButton;
|
||||
public readonly Button? RemoveButton;
|
||||
public readonly CheckBox SuspendedCheckbox;
|
||||
|
||||
public readonly Dictionary<AdminFlags, (Button inherit, Button sub, Button plus)> FlagButtons
|
||||
= new();
|
||||
@@ -381,6 +385,12 @@ namespace Content.Client.Administration.UI
|
||||
RankButton = new OptionButton();
|
||||
SaveButton = new Button { Text = Loc.GetString("permissions-eui-edit-admin-window-save-button"), HorizontalAlignment = HAlignment.Right };
|
||||
|
||||
SuspendedCheckbox = new CheckBox
|
||||
{
|
||||
Text = Loc.GetString("permissions-eui-edit-admin-window-suspended"),
|
||||
Pressed = data?.Suspended ?? false,
|
||||
};
|
||||
|
||||
RankButton.AddItem(Loc.GetString("permissions-eui-edit-admin-window-no-rank-button"), NoRank);
|
||||
foreach (var (rId, rank) in ui._ranks)
|
||||
{
|
||||
@@ -488,7 +498,8 @@ namespace Content.Client.Administration.UI
|
||||
{
|
||||
nameControl,
|
||||
TitleEdit,
|
||||
RankButton
|
||||
RankButton,
|
||||
SuspendedCheckbox,
|
||||
}
|
||||
},
|
||||
permGrid
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<controls:BabyJailStatusWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
|
||||
Title="{Loc admin-ui-baby-jail-window-title}">
|
||||
<RichTextLabel Name="MessageLabel" Access="Public" />
|
||||
</controls:BabyJailStatusWindow>
|
||||
@@ -1,21 +0,0 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
|
||||
/*
|
||||
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BabyJailStatusWindow : FancyWindow
|
||||
{
|
||||
public BabyJailStatusWindow()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
MessageLabel.SetMarkup(Loc.GetString("admin-ui-baby-jail-is-enabled"));
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<controls:BabyJailTab
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.Administration.UI.Tabs.BabyJailTab"
|
||||
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Margin="4">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<cc:CommandButton Name="EnabledButton" Command="babyjail" ToggleMode="True"
|
||||
Text="{Loc admin-ui-baby-jail-disabled}"
|
||||
ToolTip="{Loc admin-ui-baby-jail-tooltip}" />
|
||||
<cc:CommandButton Name="ShowReasonButton" Command="babyjail_show_reason"
|
||||
ToggleMode="True" Text="{Loc admin-ui-baby-jail-show-reason}"
|
||||
ToolTip="{Loc admin-ui-baby-jail-show-reason-tooltip}" />
|
||||
<BoxContainer Orientation="Vertical" Margin="0 10 0 0">
|
||||
<BoxContainer Orientation="Horizontal" Margin="2">
|
||||
<Label Text="{Loc admin-ui-baby-jail-max-account-age}" MinWidth="175" />
|
||||
<LineEdit Name="MaxAccountAge" MinWidth="50" Margin="0 0 5 0" />
|
||||
<Label Text="{Loc generic-minutes}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" Margin="2">
|
||||
<Label Text="{Loc admin-ui-baby-jail-max-overall-minutes}" MinWidth="175" />
|
||||
<LineEdit Name="MaxOverallMinutes" MinWidth="50" Margin="0 0 5 0" />
|
||||
<Label Text="{Loc generic-minutes}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:BabyJailTab>
|
||||
@@ -1,75 +0,0 @@
|
||||
using Content.Shared.Administration.Events;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
/*
|
||||
* TODO: Remove me once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
namespace Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BabyJailTab : Control
|
||||
{
|
||||
[Dependency] private readonly IConsoleHost _console = default!;
|
||||
|
||||
private string _maxAccountAge;
|
||||
private string _maxOverallMinutes;
|
||||
|
||||
public BabyJailTab()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
MaxAccountAge.OnTextEntered += args => SendMaxAccountAge(args.Text);
|
||||
MaxAccountAge.OnFocusExit += args => SendMaxAccountAge(args.Text);
|
||||
_maxAccountAge = MaxAccountAge.Text;
|
||||
|
||||
MaxOverallMinutes.OnTextEntered += args => SendMaxOverallMinutes(args.Text);
|
||||
MaxOverallMinutes.OnFocusExit += args => SendMaxOverallMinutes(args.Text);
|
||||
_maxOverallMinutes = MaxOverallMinutes.Text;
|
||||
}
|
||||
|
||||
private void SendMaxAccountAge(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) ||
|
||||
text == _maxAccountAge ||
|
||||
!int.TryParse(text, out var minutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_console.ExecuteCommand($"babyjail_max_account_age {minutes}");
|
||||
}
|
||||
|
||||
private void SendMaxOverallMinutes(string text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text) ||
|
||||
text == _maxOverallMinutes ||
|
||||
!int.TryParse(text, out var minutes))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_console.ExecuteCommand($"babyjail_max_overall_minutes {minutes}");
|
||||
}
|
||||
|
||||
public void UpdateStatus(BabyJailStatus status)
|
||||
{
|
||||
EnabledButton.Pressed = status.Enabled;
|
||||
EnabledButton.Text = Loc.GetString(status.Enabled
|
||||
? "admin-ui-baby-jail-enabled"
|
||||
: "admin-ui-baby-jail-disabled"
|
||||
);
|
||||
EnabledButton.ModulateSelfOverride = status.Enabled ? Color.Red : null;
|
||||
ShowReasonButton.Pressed = status.ShowReason;
|
||||
|
||||
MaxAccountAge.Text = status.MaxAccountAgeMinutes.ToString();
|
||||
_maxAccountAge = MaxAccountAge.Text;
|
||||
|
||||
MaxOverallMinutes.Text = status.MaxOverallMinutes.ToString();
|
||||
_maxOverallMinutes = MaxOverallMinutes.Text;
|
||||
}
|
||||
}
|
||||
@@ -197,6 +197,7 @@ public sealed partial class PlayerTab : Control
|
||||
Header.Character => Compare(x.CharacterName, y.CharacterName),
|
||||
Header.Job => Compare(x.StartingJob, y.StartingJob),
|
||||
Header.Antagonist => x.Antag.CompareTo(y.Antag),
|
||||
Header.RoleType => Compare(x.RoleProto.Name , y.RoleProto.Name),
|
||||
Header.Playtime => TimeSpan.Compare(x.OverallPlaytime ?? default, y.OverallPlaytime ?? default),
|
||||
_ => 1
|
||||
};
|
||||
|
||||
@@ -24,6 +24,11 @@
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="RoleTypeLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"/>
|
||||
<customControls:VSeparator/>
|
||||
<Label Name="OverallPlaytimeLabel"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
|
||||
@@ -23,6 +23,8 @@ public sealed partial class PlayerTabEntry : PanelContainer
|
||||
if (player.IdentityName != player.CharacterName)
|
||||
CharacterLabel.Text += $" [{player.IdentityName}]";
|
||||
AntagonistLabel.Text = Loc.GetString(player.Antag ? "player-tab-is-antag-yes" : "player-tab-is-antag-no");
|
||||
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
|
||||
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
|
||||
BackgroundColorPanel.PanelOverride = styleBoxFlat;
|
||||
OverallPlaytimeLabel.Text = player.PlaytimeString;
|
||||
PlayerEntity = player.NetEntity;
|
||||
|
||||
@@ -32,6 +32,13 @@
|
||||
Text="{Loc player-tab-antagonist}"
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="RoleTypeLabel"
|
||||
SizeFlagsStretchRatio="2"
|
||||
HorizontalExpand="True"
|
||||
ClipText="True"
|
||||
Text="{Loc player-tab-roletype}"
|
||||
MouseFilter="Pass"/>
|
||||
<cc:VSeparator/>
|
||||
<Label Name="PlaytimeLabel"
|
||||
SizeFlagsStretchRatio="1"
|
||||
HorizontalExpand="True"
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed partial class PlayerTabHeader : Control
|
||||
CharacterLabel.OnKeyBindDown += CharacterClicked;
|
||||
JobLabel.OnKeyBindDown += JobClicked;
|
||||
AntagonistLabel.OnKeyBindDown += AntagonistClicked;
|
||||
RoleTypeLabel.OnKeyBindDown += RoleTypeClicked;
|
||||
PlaytimeLabel.OnKeyBindDown += PlaytimeClicked;
|
||||
}
|
||||
|
||||
@@ -30,6 +31,7 @@ public sealed partial class PlayerTabHeader : Control
|
||||
Header.Character => CharacterLabel,
|
||||
Header.Job => JobLabel,
|
||||
Header.Antagonist => AntagonistLabel,
|
||||
Header.RoleType => RoleTypeLabel,
|
||||
Header.Playtime => PlaytimeLabel,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(header), header, null)
|
||||
};
|
||||
@@ -41,6 +43,7 @@ public sealed partial class PlayerTabHeader : Control
|
||||
CharacterLabel.Text = Loc.GetString("player-tab-character");
|
||||
JobLabel.Text = Loc.GetString("player-tab-job");
|
||||
AntagonistLabel.Text = Loc.GetString("player-tab-antagonist");
|
||||
RoleTypeLabel.Text = Loc.GetString("player-tab-roletype");
|
||||
PlaytimeLabel.Text = Loc.GetString("player-tab-playtime");
|
||||
}
|
||||
|
||||
@@ -75,6 +78,11 @@ public sealed partial class PlayerTabHeader : Control
|
||||
HeaderClicked(args, Header.Antagonist);
|
||||
}
|
||||
|
||||
private void RoleTypeClicked(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
HeaderClicked(args, Header.RoleType);
|
||||
}
|
||||
|
||||
private void PlaytimeClicked(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
HeaderClicked(args, Header.Playtime);
|
||||
@@ -90,6 +98,7 @@ public sealed partial class PlayerTabHeader : Control
|
||||
CharacterLabel.OnKeyBindDown -= CharacterClicked;
|
||||
JobLabel.OnKeyBindDown -= JobClicked;
|
||||
AntagonistLabel.OnKeyBindDown -= AntagonistClicked;
|
||||
RoleTypeLabel.OnKeyBindDown -= RoleTypeClicked;
|
||||
PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked;
|
||||
}
|
||||
}
|
||||
@@ -100,6 +109,7 @@ public sealed partial class PlayerTabHeader : Control
|
||||
Character,
|
||||
Job,
|
||||
Antagonist,
|
||||
RoleType,
|
||||
Playtime
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,8 +130,8 @@ public sealed partial class AirAlarmWindow : FancyWindow
|
||||
if (!_pumps.TryGetValue(addr, out var pumpControl))
|
||||
{
|
||||
var control= new PumpControl(pump, addr);
|
||||
control.PumpDataChanged += AtmosDeviceDataChanged!.Invoke;
|
||||
control.PumpDataCopied += AtmosDeviceDataCopied!.Invoke;
|
||||
control.PumpDataChanged += AtmosDeviceDataChanged;
|
||||
control.PumpDataCopied += AtmosDeviceDataCopied;
|
||||
_pumps.Add(addr, control);
|
||||
CVentContainer.AddChild(control);
|
||||
}
|
||||
@@ -145,8 +145,8 @@ public sealed partial class AirAlarmWindow : FancyWindow
|
||||
if (!_scrubbers.TryGetValue(addr, out var scrubberControl))
|
||||
{
|
||||
var control = new ScrubberControl(scrubber, addr);
|
||||
control.ScrubberDataChanged += AtmosDeviceDataChanged!.Invoke;
|
||||
control.ScrubberDataCopied += AtmosDeviceDataCopied!.Invoke;
|
||||
control.ScrubberDataChanged += AtmosDeviceDataChanged;
|
||||
control.ScrubberDataCopied += AtmosDeviceDataCopied;
|
||||
_scrubbers.Add(addr, control);
|
||||
CScrubberContainer.AddChild(control);
|
||||
}
|
||||
@@ -161,6 +161,7 @@ public sealed partial class AirAlarmWindow : FancyWindow
|
||||
{
|
||||
var control = new SensorInfo(sensor, addr);
|
||||
control.OnThresholdUpdate += AtmosAlarmThresholdChanged;
|
||||
control.SensorDataCopied += AtmosDeviceDataCopied;
|
||||
_sensors.Add(addr, control);
|
||||
CSensorContainer.AddChild(control);
|
||||
}
|
||||
|
||||
@@ -83,10 +83,10 @@ public sealed partial class PumpControl : BoxContainer
|
||||
PumpDataChanged?.Invoke(_address, _data);
|
||||
};
|
||||
|
||||
_copySettings.OnPressed += _ =>
|
||||
{
|
||||
PumpDataCopied?.Invoke(_data);
|
||||
};
|
||||
_copySettings.OnPressed += _ =>
|
||||
{
|
||||
PumpDataCopied?.Invoke(_data);
|
||||
};
|
||||
}
|
||||
|
||||
public void ChangeData(GasVentPumpData data)
|
||||
|
||||
@@ -72,10 +72,10 @@ public sealed partial class ScrubberControl : BoxContainer
|
||||
ScrubberDataChanged?.Invoke(_address, _data);
|
||||
};
|
||||
|
||||
_copySettings.OnPressed += _ =>
|
||||
{
|
||||
ScrubberDataCopied?.Invoke(_data);
|
||||
};
|
||||
_copySettings.OnPressed += _ =>
|
||||
{
|
||||
ScrubberDataCopied?.Invoke(_data);
|
||||
};
|
||||
|
||||
foreach (var value in Enum.GetValues<Gas>())
|
||||
{
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<CollapsibleHeading Name="SensorAddress" />
|
||||
<CollapsibleBody Margin="20 2 2 2">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal" Margin ="0 0 0 2">
|
||||
<Button Name="CCopySettings" Text="{Loc 'air-alarm-ui-thresholds-copy'}" ToolTip="{Loc 'air-alarm-ui-thresholds-copy-tooltip'}" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" Margin="0 0 2 0" HorizontalExpand="True">
|
||||
<RichTextLabel Name="AlarmStateLabel" />
|
||||
<RichTextLabel Name="PressureLabel" />
|
||||
|
||||
@@ -12,12 +12,14 @@ namespace Content.Client.Atmos.Monitor.UI.Widgets;
|
||||
public sealed partial class SensorInfo : BoxContainer
|
||||
{
|
||||
public Action<string, AtmosMonitorThresholdType, AtmosAlarmThreshold, Gas?>? OnThresholdUpdate;
|
||||
public event Action<AtmosSensorData>? SensorDataCopied;
|
||||
private string _address;
|
||||
|
||||
private ThresholdControl _pressureThreshold;
|
||||
private ThresholdControl _temperatureThreshold;
|
||||
private Dictionary<Gas, ThresholdControl> _gasThresholds = new();
|
||||
private Dictionary<Gas, RichTextLabel> _gasLabels = new();
|
||||
private Button _copySettings => CCopySettings;
|
||||
|
||||
public SensorInfo(AtmosSensorData data, string address)
|
||||
{
|
||||
@@ -56,7 +58,7 @@ public sealed partial class SensorInfo : BoxContainer
|
||||
gasThresholdControl.Margin = new Thickness(20, 2, 2, 2);
|
||||
gasThresholdControl.ThresholdDataChanged += (type, alarmThreshold, arg3) =>
|
||||
{
|
||||
OnThresholdUpdate!(_address, type, alarmThreshold, arg3);
|
||||
OnThresholdUpdate?.Invoke(_address, type, alarmThreshold, arg3);
|
||||
};
|
||||
|
||||
_gasThresholds.Add(gas, gasThresholdControl);
|
||||
@@ -72,12 +74,17 @@ public sealed partial class SensorInfo : BoxContainer
|
||||
|
||||
_pressureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
|
||||
{
|
||||
OnThresholdUpdate!(_address, type, threshold, arg3);
|
||||
OnThresholdUpdate?.Invoke(_address, type, threshold, arg3);
|
||||
};
|
||||
|
||||
_temperatureThreshold.ThresholdDataChanged += (type, threshold, arg3) =>
|
||||
{
|
||||
OnThresholdUpdate!(_address, type, threshold, arg3);
|
||||
OnThresholdUpdate?.Invoke(_address, type, threshold, arg3);
|
||||
};
|
||||
|
||||
_copySettings.OnPressed += _ =>
|
||||
{
|
||||
SensorDataCopied?.Invoke(data);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.Cargo.Systems;
|
||||
|
||||
@@ -10,9 +11,9 @@ public sealed class ClientPriceGunSystem : SharedPriceGunSystem
|
||||
{
|
||||
[Dependency] private readonly UseDelaySystem _useDelay = default!;
|
||||
|
||||
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
|
||||
protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
|
||||
if (!TryComp(entity, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((entity, useDelay)))
|
||||
return false;
|
||||
|
||||
// It feels worse if the cooldown is predicted but the popup isn't! So only do the cooldown reset on the server.
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Linq;
|
||||
using Content.Client.PDA;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Content.Shared.Inventory;
|
||||
@@ -51,6 +52,15 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
|
||||
{
|
||||
sprite.CopyFrom(otherSprite);
|
||||
}
|
||||
|
||||
// Edgecase for PDAs to include visuals when UI is open
|
||||
if (TryComp(uid, out PdaBorderColorComponent? borderColor)
|
||||
&& proto.TryGetComponent(out PdaBorderColorComponent? otherBorderColor, _factory))
|
||||
{
|
||||
borderColor.BorderColor = otherBorderColor.BorderColor;
|
||||
borderColor.AccentHColor = otherBorderColor.AccentHColor;
|
||||
borderColor.AccentVColor = otherBorderColor.AccentVColor;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
using Content.Client.Clothing.Systems;
|
||||
using Content.Client.Clothing.Systems;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Tag;
|
||||
using Content.Shared.Prototypes;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Client.Clothing.UI;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class ChameleonBoundUserInterface : BoundUserInterface
|
||||
{
|
||||
[Dependency] private readonly IComponentFactory _factory = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
private readonly ChameleonClothingSystem _chameleon;
|
||||
private readonly TagSystem _tag;
|
||||
|
||||
[ViewVariables]
|
||||
private ChameleonMenu? _menu;
|
||||
@@ -17,6 +23,7 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
|
||||
public ChameleonBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
|
||||
{
|
||||
_chameleon = EntMan.System<ChameleonClothingSystem>();
|
||||
_tag = EntMan.System<TagSystem>();
|
||||
}
|
||||
|
||||
protected override void Open()
|
||||
@@ -34,7 +41,24 @@ public sealed class ChameleonBoundUserInterface : BoundUserInterface
|
||||
return;
|
||||
|
||||
var targets = _chameleon.GetValidTargets(st.Slot);
|
||||
_menu?.UpdateState(targets, st.SelectedId);
|
||||
if (st.RequiredTag != null)
|
||||
{
|
||||
var newTargets = new List<string>();
|
||||
foreach (var target in targets)
|
||||
{
|
||||
if (string.IsNullOrEmpty(target) || !_proto.TryIndex(target, out EntityPrototype? proto))
|
||||
continue;
|
||||
|
||||
if (!proto.TryGetComponent(out TagComponent? tag, _factory) || !_tag.HasTag(tag, st.RequiredTag))
|
||||
continue;
|
||||
|
||||
newTargets.Add(target);
|
||||
}
|
||||
_menu?.UpdateState(newTargets, st.SelectedId);
|
||||
} else
|
||||
{
|
||||
_menu?.UpdateState(targets, st.SelectedId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnIdSelected(string selectedId)
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
<Label Name="CallStatusText" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||
<BoxContainer Name="CallerIdContainer" Orientation="Vertical" ReservesSpace="False">
|
||||
<RichTextLabel Name="CallerIdText" HorizontalAlignment="Center" Margin="0 0 0 0"/>
|
||||
<Label Text="{Loc 'holopad-window-relay-label'}" Margin="10 5 10 0" ReservesSpace="False"/>
|
||||
<Label Text="{Loc 'holopad-window-relay-label'}" Margin="10 10 10 0" ReservesSpace="False"/>
|
||||
<RichTextLabel Name="HolopadIdText" HorizontalAlignment="Center" Margin="0 0 0 10"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -5,19 +5,21 @@ using Content.Client.Lobby.UI;
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Systems.Chat;
|
||||
using Content.Client.Voting;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.ResourceManagement;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
|
||||
namespace Content.Client.Lobby
|
||||
{
|
||||
public sealed class LobbyState : Robust.Client.State.State
|
||||
{
|
||||
[Dependency] private readonly IBaseClient _baseClient = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IEntityManager _entityManager = default!;
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
@@ -49,7 +51,17 @@ namespace Content.Client.Lobby
|
||||
|
||||
_voteManager.SetPopupContainer(Lobby.VoteContainer);
|
||||
LayoutContainer.SetAnchorPreset(Lobby, LayoutContainer.LayoutPreset.Wide);
|
||||
Lobby.ServerName.Text = _baseClient.GameInfo?.ServerName; //The eye of refactor gazes upon you...
|
||||
|
||||
var lobbyNameCvar = _cfg.GetCVar(CCVars.ServerLobbyName);
|
||||
var serverName = _baseClient.GameInfo?.ServerName ?? string.Empty;
|
||||
|
||||
Lobby.ServerName.Text = string.IsNullOrEmpty(lobbyNameCvar)
|
||||
? Loc.GetString("ui-lobby-title", ("serverName", serverName))
|
||||
: lobbyNameCvar;
|
||||
|
||||
var width = _cfg.GetCVar(CCVars.ServerLobbyRightPanelWidth);
|
||||
Lobby.RightSide.SetWidth = width;
|
||||
|
||||
UpdateLobbyUi();
|
||||
|
||||
Lobby.CharacterPreview.CharacterSetupButton.OnPressed += OnSetupPressed;
|
||||
|
||||
@@ -62,14 +62,12 @@
|
||||
<Control Access="Public" Visible="False" Name="CharacterSetupState" VerticalExpand="True" />
|
||||
</BoxContainer>
|
||||
<!-- Right Panel -->
|
||||
<PanelContainer Name="RightSide" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
<PanelContainer Name="RightSide" Access="Public" StyleClasses="AngleRect" HorizontalAlignment="Right" VerticalExpand="True"
|
||||
VerticalAlignment="Stretch">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<!-- Top row -->
|
||||
<BoxContainer Orientation="Horizontal" MinSize="0 40" Name="HeaderContainer" Access="Public"
|
||||
SeparationOverride="4">
|
||||
<Label Margin="8 0 0 0" StyleClasses="LabelHeadingBigger" VAlign="Center"
|
||||
Text="{Loc 'ui-lobby-title'}" />
|
||||
<Label Name="ServerName" Access="Public" StyleClasses="LabelHeadingBigger" VAlign="Center"
|
||||
HorizontalExpand="True" HorizontalAlignment="Center" />
|
||||
</BoxContainer>
|
||||
|
||||
@@ -2,7 +2,6 @@ using System.Numerics;
|
||||
using Content.Client.Parallax;
|
||||
using Content.Client.Weather;
|
||||
using Content.Shared._CP14.DayCycle.Components;
|
||||
using Content.Shared._CP14.WorldEdge;
|
||||
using Content.Shared.Salvage;
|
||||
using Content.Shared.Weather;
|
||||
using Robust.Client.GameObjects;
|
||||
@@ -84,13 +83,6 @@ public sealed partial class StencilOverlay : Overlay
|
||||
}
|
||||
//CP14 Overlays end
|
||||
|
||||
//CP14 Overlays
|
||||
if (_entManager.TryGetComponent<CP14WorldEdgeComponent>(mapUid, out var worldEdge))
|
||||
{
|
||||
DrawWorldEdge(args, worldEdge, invMatrix);
|
||||
}
|
||||
//CP14 Overlays end
|
||||
|
||||
args.WorldHandle.UseShader(null);
|
||||
args.WorldHandle.SetTransform(Matrix3x2.Identity);
|
||||
}
|
||||
|
||||
@@ -141,6 +141,11 @@ namespace Content.Client.PDA
|
||||
_pdaOwner = state.PdaOwnerInfo.ActualOwnerName;
|
||||
PdaOwnerLabel.SetMarkup(Loc.GetString("comp-pda-ui-owner",
|
||||
("actualOwnerName", _pdaOwner)));
|
||||
PdaOwnerLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
PdaOwnerLabel.Visible = false;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,48 +1,8 @@
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Light;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PDA;
|
||||
|
||||
public sealed class PdaSystem : SharedPdaSystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<PdaComponent, AppearanceChangeEvent>(OnAppearanceChange);
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(EntityUid uid, PdaComponent component, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (Appearance.TryGetData<bool>(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
|
||||
args.Sprite.LayerSetVisible(PdaVisualLayers.Flashlight, isFlashlightOn);
|
||||
|
||||
if (Appearance.TryGetData<bool>(uid, PdaVisuals.IdCardInserted, out var isCardInserted, args.Component))
|
||||
args.Sprite.LayerSetVisible(PdaVisualLayers.IdLight, isCardInserted);
|
||||
}
|
||||
|
||||
protected override void OnComponentInit(EntityUid uid, PdaComponent component, ComponentInit args)
|
||||
{
|
||||
base.OnComponentInit(uid, component, args);
|
||||
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
if (component.State != null)
|
||||
sprite.LayerSetState(PdaVisualLayers.Base, component.State);
|
||||
|
||||
sprite.LayerSetVisible(PdaVisualLayers.Flashlight, component.FlashlightOn);
|
||||
sprite.LayerSetVisible(PdaVisualLayers.IdLight, component.IdSlot.StartingItem != null);
|
||||
}
|
||||
|
||||
public enum PdaVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Flashlight,
|
||||
IdLight
|
||||
}
|
||||
}
|
||||
|
||||
30
Content.Client/PDA/PdaVisualizerSystem.cs
Normal file
30
Content.Client/PDA/PdaVisualizerSystem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Light;
|
||||
using Content.Shared.PDA;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.PDA;
|
||||
|
||||
public sealed class PdaVisualizerSystem : VisualizerSystem<PdaVisualsComponent>
|
||||
{
|
||||
protected override void OnAppearanceChange(EntityUid uid, PdaVisualsComponent comp, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(uid, PdaVisuals.PdaType, out var pdaType, args.Component))
|
||||
args.Sprite.LayerSetState(PdaVisualLayers.Base, pdaType);
|
||||
|
||||
if (AppearanceSystem.TryGetData<bool>(uid, UnpoweredFlashlightVisuals.LightOn, out var isFlashlightOn, args.Component))
|
||||
args.Sprite.LayerSetVisible(PdaVisualLayers.Flashlight, isFlashlightOn);
|
||||
|
||||
if (AppearanceSystem.TryGetData<bool>(uid, PdaVisuals.IdCardInserted, out var isCardInserted, args.Component))
|
||||
args.Sprite.LayerSetVisible(PdaVisualLayers.IdLight, isCardInserted);
|
||||
}
|
||||
|
||||
public enum PdaVisualLayers : byte
|
||||
{
|
||||
Base,
|
||||
Flashlight,
|
||||
IdLight
|
||||
}
|
||||
}
|
||||
14
Content.Client/PDA/PdaVisualsComponent.cs
Normal file
14
Content.Client/PDA/PdaVisualsComponent.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Content.Client.PDA;
|
||||
|
||||
/// <summary>
|
||||
/// Used for visualizing PDA visuals.
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class PdaVisualsComponent : Component
|
||||
{
|
||||
public string? BorderColor;
|
||||
|
||||
public string? AccentHColor;
|
||||
|
||||
public string? AccentVColor;
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace Content.Client
|
||||
{
|
||||
internal static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
ContentStart.Start(args);
|
||||
|
||||
@@ -3,7 +3,6 @@ using Content.Client.Administration.Systems;
|
||||
using Content.Client.Administration.UI;
|
||||
using Content.Client.Administration.UI.Tabs.ObjectsTab;
|
||||
using Content.Client.Administration.UI.Tabs.PanicBunkerTab;
|
||||
using Content.Client.Administration.UI.Tabs.BabyJailTab;
|
||||
using Content.Client.Administration.UI.Tabs.PlayerTab;
|
||||
using Content.Client.Gameplay;
|
||||
using Content.Client.Lobby;
|
||||
@@ -38,13 +37,11 @@ public sealed class AdminUIController : UIController,
|
||||
private AdminMenuWindow? _window;
|
||||
private MenuButton? AdminButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.AdminButton;
|
||||
private PanicBunkerStatus? _panicBunker;
|
||||
private BabyJailStatus? _babyJail;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
SubscribeNetworkEvent<PanicBunkerChangedEvent>(OnPanicBunkerUpdated);
|
||||
SubscribeNetworkEvent<BabyJailChangedEvent>(OnBabyJailUpdated);
|
||||
}
|
||||
|
||||
private void OnPanicBunkerUpdated(PanicBunkerChangedEvent msg, EntitySessionEventArgs args)
|
||||
@@ -59,18 +56,6 @@ public sealed class AdminUIController : UIController,
|
||||
}
|
||||
}
|
||||
|
||||
private void OnBabyJailUpdated(BabyJailChangedEvent msg, EntitySessionEventArgs args)
|
||||
{
|
||||
var showDialog = _babyJail == null && msg.Status.Enabled;
|
||||
_babyJail = msg.Status;
|
||||
_window?.BabyJailControl.UpdateStatus(msg.Status);
|
||||
|
||||
if (showDialog)
|
||||
{
|
||||
UIManager.CreateWindow<BabyJailStatusWindow>().OpenCentered();
|
||||
}
|
||||
}
|
||||
|
||||
public void OnStateEntered(GameplayState state)
|
||||
{
|
||||
EnsureWindow();
|
||||
@@ -116,13 +101,6 @@ public sealed class AdminUIController : UIController,
|
||||
if (_panicBunker != null)
|
||||
_window.PanicBunkerControl.UpdateStatus(_panicBunker);
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
if (_babyJail != null)
|
||||
_window.BabyJailControl.UpdateStatus(_babyJail);
|
||||
|
||||
_window.PlayerTabControl.OnEntryKeyBindDown += PlayerTabEntryKeyBindDown;
|
||||
_window.ObjectsTabControl.OnEntryKeyBindDown += ObjectsTabEntryKeyBindDown;
|
||||
_window.OnOpen += OnWindowOpen;
|
||||
|
||||
@@ -7,7 +7,9 @@ using Content.Client.UserInterface.Systems.Character.Controls;
|
||||
using Content.Client.UserInterface.Systems.Character.Windows;
|
||||
using Content.Client.UserInterface.Systems.Objectives.Controls;
|
||||
using Content.Shared.Input;
|
||||
using Content.Shared.Objectives.Systems;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.Mind.Components;
|
||||
using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
@@ -15,6 +17,7 @@ using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.Controllers;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared.Input.Binding;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Utility;
|
||||
using static Content.Client.CharacterInfo.CharacterInfoSystem;
|
||||
using static Robust.Client.UserInterface.Controls.BaseButton;
|
||||
@@ -24,10 +27,25 @@ namespace Content.Client.UserInterface.Systems.Character;
|
||||
[UsedImplicitly]
|
||||
public sealed class CharacterUIController : UIController, IOnStateEntered<GameplayState>, IOnStateExited<GameplayState>, IOnSystemChanged<CharacterInfoSystem>
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _ent = default!;
|
||||
[Dependency] private readonly ILogManager _logMan = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
|
||||
[UISystemDependency] private readonly CharacterInfoSystem _characterInfo = default!;
|
||||
[UISystemDependency] private readonly SpriteSystem _sprite = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_sawmill = _logMan.GetSawmill("character");
|
||||
|
||||
SubscribeNetworkEvent<MindRoleTypeChangedEvent>(OnRoleTypeChanged);
|
||||
}
|
||||
|
||||
private CharacterWindow? _window;
|
||||
private MenuButton? CharacterButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.CharacterButton;
|
||||
|
||||
@@ -110,6 +128,9 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
|
||||
var (entity, job, objectives, briefing, entityName) = data;
|
||||
|
||||
_window.SpriteView.SetEntity(entity);
|
||||
|
||||
UpdateRoleType();
|
||||
|
||||
_window.NameLabel.Text = entityName;
|
||||
_window.SubText.Text = job;
|
||||
_window.Objectives.RemoveAllChildren();
|
||||
@@ -173,6 +194,37 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
|
||||
_window.RolePlaceholder.Visible = briefing == null && !controls.Any() && !objectives.Any();
|
||||
}
|
||||
|
||||
private void OnRoleTypeChanged(MindRoleTypeChangedEvent ev, EntitySessionEventArgs _)
|
||||
{
|
||||
UpdateRoleType();
|
||||
}
|
||||
|
||||
private void UpdateRoleType()
|
||||
{
|
||||
if (_window == null || !_window.IsOpen)
|
||||
return;
|
||||
|
||||
if (!_ent.TryGetComponent<MindContainerComponent>(_player.LocalEntity, out var container)
|
||||
|| container.Mind is null)
|
||||
return;
|
||||
|
||||
if (!_ent.TryGetComponent<MindComponent>(container.Mind.Value, out var mind))
|
||||
return;
|
||||
|
||||
var roleText = Loc.GetString("role-type-crew-aligned-name");
|
||||
var color = Color.White;
|
||||
if (_prototypeManager.TryIndex(mind.RoleType, out var proto))
|
||||
{
|
||||
roleText = Loc.GetString(proto.Name);
|
||||
color = proto.Color;
|
||||
}
|
||||
else
|
||||
_sawmill.Error($"{_player.LocalEntity} has invalid Role Type '{mind.RoleType}'. Displaying '{roleText}' instead");
|
||||
|
||||
_window.RoleType.Text = roleText;
|
||||
_window.RoleType.FontColorOverride = color;
|
||||
}
|
||||
|
||||
private void CharacterDetached(EntityUid uid)
|
||||
{
|
||||
CloseWindow();
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
MinHeight="545">
|
||||
<ScrollContainer>
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<Label Name="RoleType" VerticalAlignment="Top" Margin="0 6 0 10" HorizontalAlignment="Center" StyleClasses="LabelHeading" Access="Public"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<SpriteView OverrideDirection="South" Scale="2 2" Name="SpriteView" Access="Public" SetSize="64 64"/>
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Top">
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared._CP14.WorldEdge;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Client.Overlays;
|
||||
|
||||
public sealed partial class StencilOverlay
|
||||
{
|
||||
private void DrawWorldEdge(in OverlayDrawArgs args, CP14WorldEdgeComponent rangeComp, Matrix3x2 invMatrix)
|
||||
{
|
||||
var worldHandle = args.WorldHandle;
|
||||
var renderScale = args.Viewport.RenderScale.X;
|
||||
// TODO: This won't handle non-standard zooms so uhh yeah, not sure how to structure it on the shader side.
|
||||
var zoom = args.Viewport.Eye?.Zoom ?? Vector2.One;
|
||||
var length = zoom.X;
|
||||
var bufferRange = MathF.Min(10f, rangeComp.Range);
|
||||
|
||||
var pixelCenter = Vector2.Transform(rangeComp.Origin, invMatrix);
|
||||
// Something something offset?
|
||||
var vertical = args.Viewport.Size.Y;
|
||||
|
||||
var pixelMaxRange = rangeComp.Range * renderScale / length * EyeManager.PixelsPerMeter;
|
||||
var pixelBufferRange = bufferRange * renderScale / length * EyeManager.PixelsPerMeter;
|
||||
var pixelMinRange = pixelMaxRange - pixelBufferRange;
|
||||
|
||||
_shader.SetParameter("position", new Vector2(pixelCenter.X, vertical - pixelCenter.Y));
|
||||
_shader.SetParameter("maxRange", pixelMaxRange);
|
||||
_shader.SetParameter("minRange", pixelMinRange);
|
||||
_shader.SetParameter("bufferRange", pixelBufferRange);
|
||||
_shader.SetParameter("gradient", 0.80f);
|
||||
|
||||
var worldAABB = args.WorldAABB;
|
||||
var worldBounds = args.WorldBounds;
|
||||
var position = args.Viewport.Eye?.Position.Position ?? Vector2.Zero;
|
||||
var localAABB = invMatrix.TransformBox(worldAABB);
|
||||
|
||||
// Cut out the irrelevant bits via stencil
|
||||
// This is why we don't just use parallax; we might want specific tiles to get drawn over
|
||||
// particularly for planet maps or stations.
|
||||
worldHandle.RenderInRenderTarget(_blep!, () =>
|
||||
{
|
||||
worldHandle.UseShader(_shader);
|
||||
worldHandle.DrawRect(localAABB, Color.White);
|
||||
}, Color.Transparent);
|
||||
|
||||
worldHandle.SetTransform(Matrix3x2.Identity);
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilMask").Instance());
|
||||
worldHandle.DrawTextureRect(_blep!.Texture, worldBounds);
|
||||
var curTime = _timing.RealTime;
|
||||
var sprite = _sprite.GetFrame(new SpriteSpecifier.Texture(new ResPath("/Textures/Parallaxes/noise.png")), curTime);
|
||||
|
||||
// Draw the rain
|
||||
worldHandle.UseShader(_protoManager.Index<ShaderPrototype>("StencilDraw").Instance());
|
||||
_parallax.DrawParallax(worldHandle, worldAABB, sprite, curTime, position, new Vector2(0.2f, 0.1f));
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ public sealed class FluidSpill
|
||||
|
||||
#pragma warning disable NUnit2045 // Interdependent tests
|
||||
Assert.That(puddle, Is.Not.Null);
|
||||
Assert.That(puddleSystem.CurrentVolume(puddle!.Value.Owner, puddle), Is.EqualTo(FixedPoint2.New(100)));
|
||||
//Assert.That(puddleSystem.CurrentVolume(puddle!.Value.Owner, puddle), Is.EqualTo(FixedPoint2.New(100))); //TOOD: CP14 fix failing test because force undersky evaporation :(
|
||||
#pragma warning restore NUnit2045
|
||||
|
||||
for (var x = 0; x < 3; x++)
|
||||
|
||||
@@ -35,7 +35,7 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs
|
||||
{
|
||||
playerUid = serverPlayerManager.Sessions.Single().AttachedEntity.GetValueOrDefault();
|
||||
#pragma warning disable NUnit2045 // Interdependent assertions.
|
||||
Assert.That(playerUid, Is.Not.EqualTo(default));
|
||||
Assert.That(playerUid, Is.Not.EqualTo(default(EntityUid)));
|
||||
// Making sure it exists
|
||||
Assert.That(entManager.HasComponent<AlertsComponent>(playerUid));
|
||||
#pragma warning restore NUnit2045
|
||||
|
||||
@@ -17,7 +17,6 @@ using Robust.Shared.Map;
|
||||
using Robust.Shared.Map.Components;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Content.Shared.Station.Components;
|
||||
using FastAccessors;
|
||||
using Robust.Shared.Utility;
|
||||
using YamlDotNet.RepresentationModel;
|
||||
|
||||
|
||||
@@ -61,7 +61,13 @@ public static class ClientPackaging
|
||||
var graph = new RobustClientAssetGraph();
|
||||
pass.Dependencies.Add(new AssetPassDependency(graph.Output.Name));
|
||||
|
||||
AssetGraph.CalculateGraph(graph.AllPasses.Append(pass).ToArray(), logger);
|
||||
var dropSvgPass = new AssetPassFilterDrop(f => f.Path.EndsWith(".svg"))
|
||||
{
|
||||
Name = "DropSvgPass",
|
||||
};
|
||||
dropSvgPass.AddDependency(graph.Input).AddBefore(graph.PresetPasses);
|
||||
|
||||
AssetGraph.CalculateGraph([pass, dropSvgPass, ..graph.AllPasses], logger);
|
||||
|
||||
var inputPass = graph.Input;
|
||||
|
||||
@@ -72,7 +78,7 @@ public static class ClientPackaging
|
||||
new[] { "Content.Client", "Content.Shared", "Content.Shared.Database" },
|
||||
cancel: cancel);
|
||||
|
||||
await RobustClientPackaging.WriteClientResources(contentDir, pass, cancel);
|
||||
await RobustClientPackaging.WriteClientResources(contentDir, inputPass, cancel);
|
||||
|
||||
inputPass.InjectFinished();
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
|
||||
2104
Content.Server.Database/Migrations/Postgres/20241122174243_IPIntel.Designer.cs
generated
Normal file
2104
Content.Server.Database/Migrations/Postgres/20241122174243_IPIntel.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Net;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class IPIntel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ipintel_cache",
|
||||
columns: table => new
|
||||
{
|
||||
ipintel_cache_id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
address = table.Column<IPAddress>(type: "inet", nullable: false),
|
||||
time = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
score = table.Column<float>(type: "real", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ipintel_cache", x => x.ipintel_cache_id);
|
||||
});
|
||||
|
||||
migrationBuilder.Sql("CREATE UNIQUE INDEX idx_ipintel_cache_address ON ipintel_cache(address)");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ipintel_cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
2084
Content.Server.Database/Migrations/Postgres/20241223235939_AdminStatus.Designer.cs
generated
Normal file
2084
Content.Server.Database/Migrations/Postgres/20241223235939_AdminStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Postgres
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deadminned",
|
||||
table: "admin",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "suspended",
|
||||
table: "admin",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deadminned",
|
||||
table: "admin");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "suspended",
|
||||
table: "admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,14 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("admin_rank_id");
|
||||
|
||||
b.Property<bool>("Deadminned")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("deadminned");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("boolean")
|
||||
.HasColumnName("suspended");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("title");
|
||||
@@ -627,6 +635,34 @@ namespace Content.Server.Database.Migrations.Postgres
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer")
|
||||
.HasColumnName("ipintel_cache_id");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<IPAddress>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("inet")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<float>("Score")
|
||||
.HasColumnType("real")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<DateTime>("Time")
|
||||
.HasColumnType("timestamp with time zone")
|
||||
.HasColumnName("time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_ipintel_cache");
|
||||
|
||||
b.ToTable("ipintel_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
2028
Content.Server.Database/Migrations/Sqlite/20241122174236_IPIntel.Designer.cs
generated
Normal file
2028
Content.Server.Database/Migrations/Sqlite/20241122174236_IPIntel.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class IPIntel : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ipintel_cache",
|
||||
columns: table => new
|
||||
{
|
||||
ipintel_cache_id = table.Column<int>(type: "INTEGER", nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
address = table.Column<string>(type: "TEXT", nullable: false),
|
||||
time = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
score = table.Column<float>(type: "REAL", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ipintel_cache", x => x.ipintel_cache_id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ipintel_cache_address",
|
||||
table: "ipintel_cache",
|
||||
column: "address",
|
||||
unique: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ipintel_cache");
|
||||
}
|
||||
}
|
||||
}
|
||||
2007
Content.Server.Database/Migrations/Sqlite/20241223235932_AdminStatus.Designer.cs
generated
Normal file
2007
Content.Server.Database/Migrations/Sqlite/20241223235932_AdminStatus.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,40 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Content.Server.Database.Migrations.Sqlite
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AdminStatus : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "deadminned",
|
||||
table: "admin",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "suspended",
|
||||
table: "admin",
|
||||
type: "INTEGER",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "deadminned",
|
||||
table: "admin");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "suspended",
|
||||
table: "admin");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,14 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("admin_rank_id");
|
||||
|
||||
b.Property<bool>("Deadminned")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("deadminned");
|
||||
|
||||
b.Property<bool>("Suspended")
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("suspended");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("title");
|
||||
@@ -591,6 +599,35 @@ namespace Content.Server.Database.Migrations.Sqlite
|
||||
b.ToTable("connection_log", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.IPIntelCache", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("INTEGER")
|
||||
.HasColumnName("ipintel_cache_id");
|
||||
|
||||
b.Property<string>("Address")
|
||||
.IsRequired()
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("address");
|
||||
|
||||
b.Property<float>("Score")
|
||||
.HasColumnType("REAL")
|
||||
.HasColumnName("score");
|
||||
|
||||
b.Property<DateTime>("Time")
|
||||
.HasColumnType("TEXT")
|
||||
.HasColumnName("time");
|
||||
|
||||
b.HasKey("Id")
|
||||
.HasName("PK_ipintel_cache");
|
||||
|
||||
b.HasIndex("Address")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("ipintel_cache", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Content.Server.Database.Job", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
|
||||
@@ -45,6 +45,7 @@ namespace Content.Server.Database
|
||||
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
|
||||
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
|
||||
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
|
||||
public DbSet<IPIntelCache> IPIntelCache { get; set; } = null!;
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
@@ -609,6 +610,16 @@ namespace Content.Server.Database
|
||||
[Key] public Guid UserId { get; set; }
|
||||
public string? Title { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the admin is voluntarily deadminned. They can re-admin at any time.
|
||||
/// </summary>
|
||||
public bool Deadminned { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the admin is suspended by an admin with <c>PERMISSIONS</c>. They will not have in-game permissions.
|
||||
/// </summary>
|
||||
public bool Suspended { get; set; }
|
||||
|
||||
public int? AdminRankId { get; set; }
|
||||
public AdminRank? AdminRank { get; set; }
|
||||
public List<AdminFlag> Flags { get; set; } = default!;
|
||||
@@ -962,12 +973,14 @@ namespace Content.Server.Database
|
||||
Full = 2,
|
||||
Panic = 3,
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*
|
||||
* If baby jail is removed, please reserve this value for as long as can reasonably be done to prevent causing ambiguity in connection denial reasons.
|
||||
* Reservation by commenting out the value is likely sufficient for this purpose, but may impact projects which depend on SS14 like SS14.Admin.
|
||||
*
|
||||
* Edit: It has
|
||||
*/
|
||||
BabyJail = 4,
|
||||
/// Results from rejected connections with external API checking tools
|
||||
IPChecks = 5,
|
||||
}
|
||||
|
||||
public class ServerBanHit
|
||||
@@ -1284,4 +1297,28 @@ namespace Content.Server.Database
|
||||
return new ImmutableTypedHwid(hwid.Hwid.ToImmutableArray(), hwid.Type);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Cache for the IPIntel system
|
||||
/// </summary>
|
||||
public class IPIntelCache
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The IP address (duh). This is made unique manually for psql cause of ef core bug.
|
||||
/// </summary>
|
||||
public IPAddress Address { get; set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Date this record was added. Used to check if our cache is out of date.
|
||||
/// </summary>
|
||||
public DateTime Time { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The score IPIntel returned
|
||||
/// </summary>
|
||||
public float Score { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@ namespace Content.Server.Database
|
||||
modelBuilder.Entity<Profile>()
|
||||
.Property(log => log.Markings)
|
||||
.HasConversion(jsonByteArrayConverter);
|
||||
|
||||
// EF core can make this automatically unique on sqlite but not psql.
|
||||
modelBuilder.Entity<IPIntelCache>()
|
||||
.HasIndex(p => p.Address)
|
||||
.IsUnique();
|
||||
}
|
||||
|
||||
public override int CountAdminLogs()
|
||||
|
||||
@@ -116,10 +116,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent<MetaDataComponent>(component.TargetAccessReaderId).EntityName;
|
||||
targetLabelColor = Color.White;
|
||||
|
||||
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent))
|
||||
if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderEnt))
|
||||
return;
|
||||
|
||||
var currentAccessHashsets = accessReaderComponent.AccessLists;
|
||||
var currentAccessHashsets = accessReaderEnt.Value.Comp.AccessLists;
|
||||
currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray();
|
||||
}
|
||||
|
||||
@@ -210,10 +210,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader))
|
||||
if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReaderEnt))
|
||||
return;
|
||||
|
||||
var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists);
|
||||
var oldTags = ConvertAccessHashSetsToList(accessReaderEnt.Value.Comp.AccessLists);
|
||||
var privilegedId = component.PrivilegedIdSlot.Item;
|
||||
|
||||
if (oldTags.SequenceEqual(newAccessList))
|
||||
@@ -242,10 +242,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList();
|
||||
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Medium,
|
||||
$"{ToPrettyString(player):player} has modified {ToPrettyString(component.TargetAccessReaderId):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||
|
||||
accessReader.AccessLists = ConvertAccessListToHashSet(newAccessList);
|
||||
Dirty(component.TargetAccessReaderId, accessReader);
|
||||
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
|
||||
Dirty(accessReaderEnt.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -33,16 +33,16 @@ public sealed class AdminWhoCommand : IConsoleCommand
|
||||
var first = true;
|
||||
foreach (var admin in adminMgr.ActiveAdmins)
|
||||
{
|
||||
if (!first)
|
||||
sb.Append('\n');
|
||||
first = false;
|
||||
|
||||
var adminData = adminMgr.GetAdminData(admin)!;
|
||||
DebugTools.AssertNotNull(adminData);
|
||||
|
||||
if (adminData.Stealth && !seeStealth)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append('\n');
|
||||
first = false;
|
||||
|
||||
sb.Append(admin.Name);
|
||||
if (adminData.Title is { } title)
|
||||
sb.Append($": [{title}]");
|
||||
|
||||
@@ -1,139 +0,0 @@
|
||||
using Content.Shared.Administration;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
namespace Content.Server.Administration.Commands;
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var toggle = Toggle(CCVars.BabyJailEnabled, shell, args, _cfg);
|
||||
if (toggle == null)
|
||||
return;
|
||||
|
||||
shell.WriteLine(Loc.GetString(toggle.Value ? "babyjail-command-enabled" : "babyjail-command-disabled"));
|
||||
}
|
||||
|
||||
public static bool? Toggle(CVarDef<bool> cvar, IConsoleShell shell, string[] args, IConfigurationManager config)
|
||||
{
|
||||
if (args.Length > 1)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return null;
|
||||
}
|
||||
|
||||
var enabled = config.GetCVar(cvar);
|
||||
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
enabled = !enabled;
|
||||
break;
|
||||
case 1 when !bool.TryParse(args[0], out enabled):
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-boolean"));
|
||||
return null;
|
||||
}
|
||||
|
||||
config.SetCVar(cvar, enabled);
|
||||
|
||||
return enabled;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailShowReasonCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_show_reason";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var toggle = BabyJailCommand.Toggle(CCVars.BabyJailShowReason, shell, args, _cfg);
|
||||
if (toggle == null)
|
||||
return;
|
||||
|
||||
shell.WriteLine(Loc.GetString(toggle.Value
|
||||
? "babyjail-command-show-reason-enabled"
|
||||
: "babyjail-command-show-reason-disabled"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailMinAccountAgeCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_max_account_age";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
var current = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-is", ("minutes", current)));
|
||||
break;
|
||||
}
|
||||
case > 1:
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out var minutes))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.SetCVar(CCVars.BabyJailMaxAccountAge, minutes);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-account-age-set", ("minutes", minutes)));
|
||||
}
|
||||
}
|
||||
|
||||
[AdminCommand(AdminFlags.Server)]
|
||||
public sealed class BabyJailMinOverallHoursCommand : LocalizedCommands
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public override string Command => "babyjail_max_overall_minutes";
|
||||
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
var current = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-max-overall-minutes-is", ("minutes", current)));
|
||||
break;
|
||||
}
|
||||
case > 1:
|
||||
shell.WriteError(Loc.GetString("shell-need-between-arguments",("lower", 0), ("upper", 1)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!int.TryParse(args[0], out var hours))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("shell-argument-must-be-number"));
|
||||
return;
|
||||
}
|
||||
|
||||
_cfg.SetCVar(CCVars.BabyJailMaxOverallMinutes, hours);
|
||||
shell.WriteLine(Loc.GetString("babyjail-command-overall-minutes-set", ("hours", hours)));
|
||||
}
|
||||
}
|
||||
@@ -91,14 +91,29 @@ namespace Content.Server.Administration.Managers
|
||||
_chat.SendAdminAnnouncement(Loc.GetString("admin-manager-self-de-admin-message", ("exAdminName", session.Name)));
|
||||
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-normal-player-message"));
|
||||
|
||||
var plyData = session.ContentData()!;
|
||||
plyData.ExplicitlyDeadminned = true;
|
||||
UpdateDatabaseDeadminnedState(session, true);
|
||||
reg.Data.Active = false;
|
||||
|
||||
SendPermsChangedEvent(session);
|
||||
UpdateAdminStatus(session);
|
||||
}
|
||||
|
||||
private async void UpdateDatabaseDeadminnedState(ICommonSession player, bool newState)
|
||||
{
|
||||
try
|
||||
{
|
||||
// NOTE: This function gets called if you deadmin/readmin from a transient admin status.
|
||||
// (e.g. loginlocal)
|
||||
// In which case there may not be a database record.
|
||||
// The DB function handles this scenario fine, but it's worth noting.
|
||||
await _dbManager.UpdateAdminDeadminnedAsync(player.UserId, newState);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error("Failed to save deadmin state to database for {Admin}", player.UserId);
|
||||
}
|
||||
}
|
||||
|
||||
public void Stealth(ICommonSession session)
|
||||
{
|
||||
if (!_admins.TryGetValue(session, out var reg))
|
||||
@@ -151,8 +166,7 @@ namespace Content.Server.Administration.Managers
|
||||
|
||||
_chat.DispatchServerMessage(session, Loc.GetString("admin-manager-became-admin-message"));
|
||||
|
||||
var plyData = session.ContentData()!;
|
||||
plyData.ExplicitlyDeadminned = false;
|
||||
UpdateDatabaseDeadminnedState(session, false);
|
||||
reg.Data.Active = true;
|
||||
|
||||
if (!reg.Data.Stealth)
|
||||
@@ -208,13 +222,13 @@ namespace Content.Server.Administration.Managers
|
||||
curAdmin.IsSpecialLogin = special;
|
||||
curAdmin.RankId = rankId;
|
||||
curAdmin.Data = aData;
|
||||
}
|
||||
|
||||
if (!player.ContentData()!.ExplicitlyDeadminned)
|
||||
{
|
||||
aData.Active = true;
|
||||
if (curAdmin.Data.Active)
|
||||
{
|
||||
aData.Active = true;
|
||||
|
||||
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
|
||||
_chat.DispatchServerMessage(player, Loc.GetString("admin-manager-admin-permissions-updated-message"));
|
||||
}
|
||||
}
|
||||
|
||||
if (player.ContentData()!.Stealthed)
|
||||
@@ -381,10 +395,8 @@ namespace Content.Server.Administration.Managers
|
||||
if (session.ContentData()!.Stealthed)
|
||||
reg.Data.Stealth = true;
|
||||
|
||||
if (!session.ContentData()!.ExplicitlyDeadminned)
|
||||
if (reg.Data.Active)
|
||||
{
|
||||
reg.Data.Active = true;
|
||||
|
||||
if (_cfg.GetCVar(CCVars.AdminAnnounceLogin))
|
||||
{
|
||||
if (reg.Data.Stealth)
|
||||
@@ -430,6 +442,7 @@ namespace Content.Server.Administration.Managers
|
||||
{
|
||||
Title = Loc.GetString("admin-manager-admin-data-host-title"),
|
||||
Flags = AdminFlagsHelper.Everything,
|
||||
Active = true,
|
||||
};
|
||||
|
||||
return (data, null, true);
|
||||
@@ -444,6 +457,12 @@ namespace Content.Server.Administration.Managers
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dbData.Suspended)
|
||||
{
|
||||
// Suspended admins don't count.
|
||||
return null;
|
||||
}
|
||||
|
||||
var flags = AdminFlags.None;
|
||||
|
||||
if (dbData.AdminRank != null)
|
||||
@@ -466,7 +485,8 @@ namespace Content.Server.Administration.Managers
|
||||
|
||||
var data = new AdminData
|
||||
{
|
||||
Flags = flags
|
||||
Flags = flags,
|
||||
Active = !dbData.Deadminned,
|
||||
};
|
||||
|
||||
if (dbData.Title != null && _cfg.GetCVar(CCVars.AdminUseCustomNamesAdminRank))
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
using Content.Server.Administration.Notes;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Discord;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using System.Linq;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// This manager sends a webhook notification whenever a player with an active
|
||||
/// watchlist joins the server.
|
||||
/// </summary>
|
||||
public interface IWatchlistWebhookManager
|
||||
{
|
||||
void Initialize();
|
||||
void Update();
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
using Content.Server.Administration.Notes;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.Discord;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// This manager sends a Discord webhook notification whenever a player with an active
|
||||
/// watchlist joins the server.
|
||||
/// </summary>
|
||||
public sealed class WatchlistWebhookManager : IWatchlistWebhookManager
|
||||
{
|
||||
[Dependency] private readonly IAdminNotesManager _adminNotes = default!;
|
||||
[Dependency] private readonly IBaseServer _baseServer = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly DiscordWebhook _discord = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
|
||||
private string _webhookUrl = default!;
|
||||
private TimeSpan _bufferTime;
|
||||
|
||||
private List<WatchlistConnection> watchlistConnections = new();
|
||||
private TimeSpan? _bufferStartTime;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = Logger.GetSawmill("discord");
|
||||
_cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionBufferTime, SetBufferTime, true);
|
||||
_cfg.OnValueChanged(CCVars.DiscordWatchlistConnectionWebhook, SetWebhookUrl, true);
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
}
|
||||
|
||||
private void SetBufferTime(float bufferTimeSeconds)
|
||||
{
|
||||
_bufferTime = TimeSpan.FromSeconds(bufferTimeSeconds);
|
||||
}
|
||||
|
||||
private void SetWebhookUrl(string webhookUrl)
|
||||
{
|
||||
_webhookUrl = webhookUrl;
|
||||
}
|
||||
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (e.NewStatus != SessionStatus.Connected)
|
||||
return;
|
||||
|
||||
var watchlists = await _adminNotes.GetActiveWatchlists(e.Session.UserId);
|
||||
|
||||
if (watchlists.Count == 0)
|
||||
return;
|
||||
|
||||
watchlistConnections.Add(new WatchlistConnection(e.Session.Name, watchlists));
|
||||
|
||||
if (_bufferTime > TimeSpan.Zero)
|
||||
{
|
||||
if (_bufferStartTime == null)
|
||||
_bufferStartTime = _gameTiming.RealTime;
|
||||
}
|
||||
else
|
||||
{
|
||||
SendDiscordMessage();
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_bufferStartTime != null && _gameTiming.RealTime > (_bufferStartTime + _bufferTime))
|
||||
{
|
||||
SendDiscordMessage();
|
||||
_bufferStartTime = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async void SendDiscordMessage()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(_webhookUrl))
|
||||
return;
|
||||
|
||||
var webhookData = await _discord.GetWebhook(_webhookUrl);
|
||||
if (webhookData == null)
|
||||
return;
|
||||
|
||||
var webhookIdentifier = webhookData.Value.ToIdentifier();
|
||||
|
||||
var messageBuilder = new StringBuilder(Loc.GetString("discord-watchlist-connection-header",
|
||||
("players", watchlistConnections.Count),
|
||||
("serverName", _baseServer.ServerName)));
|
||||
|
||||
foreach (var connection in watchlistConnections)
|
||||
{
|
||||
messageBuilder.Append('\n');
|
||||
|
||||
var watchlist = connection.Watchlists.First();
|
||||
var expiry = watchlist.ExpirationTime?.ToUnixTimeSeconds();
|
||||
messageBuilder.Append(Loc.GetString("discord-watchlist-connection-entry",
|
||||
("playerName", connection.PlayerName),
|
||||
("message", watchlist.Message),
|
||||
("expiry", expiry ?? 0),
|
||||
("otherWatchlists", connection.Watchlists.Count - 1)));
|
||||
}
|
||||
|
||||
var payload = new WebhookPayload { Content = messageBuilder.ToString() };
|
||||
|
||||
await _discord.CreateMessage(webhookIdentifier, payload);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error($"Error while sending discord watchlist connection message:\n{e}");
|
||||
}
|
||||
|
||||
// Clear the buffered list regardless of whether the message is sent successfully
|
||||
// This prevents infinitely buffering connections if we fail to send a message
|
||||
watchlistConnections.Clear();
|
||||
}
|
||||
|
||||
private sealed class WatchlistConnection
|
||||
{
|
||||
public string PlayerName;
|
||||
public List<AdminWatchlistRecord> Watchlists;
|
||||
|
||||
public WatchlistConnection(string playerName, List<AdminWatchlistRecord> watchlists)
|
||||
{
|
||||
PlayerName = playerName;
|
||||
Watchlists = watchlists;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Mind;
|
||||
using Content.Shared.PDA;
|
||||
using Content.Shared.Players;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Roles;
|
||||
@@ -32,6 +31,7 @@ using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Administration.Systems;
|
||||
|
||||
@@ -48,6 +48,7 @@ public sealed class AdminSystem : EntitySystem
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
[Dependency] private readonly PhysicsSystem _physics = default!;
|
||||
[Dependency] private readonly PlayTimeTrackingManager _playTime = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly SharedRoleSystem _role = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
@@ -63,7 +64,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
|
||||
private readonly HashSet<NetUserId> _roundActivePlayers = new();
|
||||
public readonly PanicBunkerStatus PanicBunker = new();
|
||||
public readonly BabyJailStatus BabyJail = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -82,16 +82,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinAccountAge, OnPanicBunkerMinAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.PanicBunkerMinOverallMinutes, OnPanicBunkerMinOverallMinutesChanged, true);
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
// Baby Jail Settings
|
||||
Subs.CVar(_config, CCVars.BabyJailEnabled, OnBabyJailChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailShowReason, OnBabyJailShowReasonChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxAccountAge, OnBabyJailMaxAccountAgeChanged, true);
|
||||
Subs.CVar(_config, CCVars.BabyJailMaxOverallMinutes, OnBabyJailMaxOverallMinutesChanged, true);
|
||||
|
||||
SubscribeLocalEvent<IdentityChangedEvent>(OnIdentityChanged);
|
||||
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
|
||||
SubscribeLocalEvent<PlayerDetachedEvent>(OnPlayerDetached);
|
||||
@@ -165,7 +155,8 @@ public sealed class AdminSystem : EntitySystem
|
||||
private void OnRoleEvent(RoleEvent ev)
|
||||
{
|
||||
var session = _minds.GetSession(ev.Mind);
|
||||
if (!ev.Antagonist || session == null)
|
||||
|
||||
if (!ev.RoleTypeUpdate || session == null)
|
||||
return;
|
||||
|
||||
UpdatePlayerList(session);
|
||||
@@ -239,9 +230,16 @@ public sealed class AdminSystem : EntitySystem
|
||||
}
|
||||
|
||||
var antag = false;
|
||||
|
||||
RoleTypePrototype roleType = new();
|
||||
var startingRole = string.Empty;
|
||||
if (_minds.TryGetMind(session, out var mindId, out _))
|
||||
if (_minds.TryGetMind(session, out var mindId, out var mindComp))
|
||||
{
|
||||
if (_proto.TryIndex(mindComp.RoleType, out var role))
|
||||
roleType = role;
|
||||
else
|
||||
Log.Error($"{ToPrettyString(mindId)} has invalid Role Type '{mindComp.RoleType}'. Displaying '{Loc.GetString(roleType.Name)}' instead");
|
||||
|
||||
antag = _role.MindIsAntagonist(mindId);
|
||||
startingRole = _jobs.MindTryGetJobName(mindId);
|
||||
}
|
||||
@@ -255,7 +253,7 @@ public sealed class AdminSystem : EntitySystem
|
||||
overallPlaytime = playTime;
|
||||
}
|
||||
|
||||
return new PlayerInfo(name, entityName, identityName, startingRole, antag, GetNetEntity(session?.AttachedEntity), data.UserId,
|
||||
return new PlayerInfo(name, entityName, identityName, startingRole, antag, roleType, GetNetEntity(session?.AttachedEntity), data.UserId,
|
||||
connected, _roundActivePlayers.Contains(data.UserId), overallPlaytime);
|
||||
}
|
||||
|
||||
@@ -270,17 +268,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailChanged(bool enabled)
|
||||
{
|
||||
BabyJail.Enabled = enabled;
|
||||
_chat.SendAdminAlert(Loc.GetString(enabled
|
||||
? "admin-ui-baby-jail-enabled-admin-alert"
|
||||
: "admin-ui-baby-jail-disabled-admin-alert"
|
||||
));
|
||||
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerDisableWithAdminsChanged(bool enabled)
|
||||
{
|
||||
PanicBunker.DisableWithAdmins = enabled;
|
||||
@@ -305,36 +292,18 @@ public sealed class AdminSystem : EntitySystem
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailShowReasonChanged(bool enabled)
|
||||
{
|
||||
BabyJail.ShowReason = enabled;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinAccountAgeChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinAccountAgeMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxAccountAgeChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxAccountAgeMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void OnPanicBunkerMinOverallMinutesChanged(int minutes)
|
||||
{
|
||||
PanicBunker.MinOverallMinutes = minutes;
|
||||
SendPanicBunkerStatusAll();
|
||||
}
|
||||
|
||||
private void OnBabyJailMaxOverallMinutesChanged(int minutes)
|
||||
{
|
||||
BabyJail.MaxOverallMinutes = minutes;
|
||||
SendBabyJailStatusAll();
|
||||
}
|
||||
|
||||
private void UpdatePanicBunker()
|
||||
{
|
||||
var hasAdmins = false;
|
||||
@@ -381,15 +350,6 @@ public sealed class AdminSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void SendBabyJailStatusAll()
|
||||
{
|
||||
var ev = new BabyJailChangedEvent(BabyJail);
|
||||
foreach (var admin in _adminManager.AllAdmins)
|
||||
{
|
||||
RaiseNetworkEvent(ev, admin);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Erases a player from the round.
|
||||
/// This removes them and any trace of them from the round, deleting their
|
||||
|
||||
@@ -76,7 +76,8 @@ namespace Content.Server.Administration.UI
|
||||
Title = p.a.Title,
|
||||
RankId = p.a.AdminRankId,
|
||||
UserId = new NetUserId(p.a.UserId),
|
||||
UserName = p.lastUserName
|
||||
UserName = p.lastUserName,
|
||||
Suspended = p.a.Suspended,
|
||||
}).ToArray(),
|
||||
|
||||
AdminRanks = _adminRanks.ToDictionary(a => a.Id, a => new PermissionsEuiState.AdminRankData
|
||||
@@ -255,6 +256,7 @@ namespace Content.Server.Administration.UI
|
||||
admin.Title = ua.Title;
|
||||
admin.AdminRankId = ua.RankId;
|
||||
admin.Flags = GenAdminFlagList(ua.PosFlags, ua.NegFlags);
|
||||
admin.Suspended = ua.Suspended;
|
||||
|
||||
await _db.UpdateAdminAsync(admin);
|
||||
|
||||
@@ -335,7 +337,8 @@ namespace Content.Server.Administration.UI
|
||||
Flags = GenAdminFlagList(ca.PosFlags, ca.NegFlags),
|
||||
AdminRankId = ca.RankId,
|
||||
UserId = userId.UserId,
|
||||
Title = ca.Title
|
||||
Title = ca.Title,
|
||||
Suspended = ca.Suspended,
|
||||
};
|
||||
|
||||
await _db.AddAdminAsync(admin);
|
||||
|
||||
@@ -97,6 +97,6 @@ public sealed partial class ReagentProducerAnomalyComponent : Component
|
||||
/// <summary>
|
||||
/// Solution where the substance is generated
|
||||
/// </summary>
|
||||
[DataField("solutionRef")]
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
}
|
||||
|
||||
@@ -153,7 +153,7 @@ public partial struct AntagSelectionDefinition()
|
||||
/// List of Mind Role Prototypes to be added to the player's mind.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<ProtoId<EntityPrototype>>? MindRoles;
|
||||
public List<EntProtoId>? MindRoles;
|
||||
|
||||
/// <summary>
|
||||
/// A set of starting gear that's equipped to the player.
|
||||
|
||||
@@ -131,6 +131,19 @@ public sealed class AirAlarmSystem : EntitySystem
|
||||
SyncDevice(uid, address);
|
||||
}
|
||||
|
||||
private void SetAllThresholds(EntityUid uid, string address, AtmosSensorData data)
|
||||
{
|
||||
var payload = new NetworkPayload
|
||||
{
|
||||
[DeviceNetworkConstants.Command] = AtmosMonitorSystem.AtmosMonitorSetAllThresholdsCmd,
|
||||
[AtmosMonitorSystem.AtmosMonitorAllThresholdData] = data
|
||||
};
|
||||
|
||||
_deviceNet.QueuePacket(uid, address, payload);
|
||||
|
||||
SyncDevice(uid, address);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sync this air alarm's mode with the rest of the network.
|
||||
/// </summary>
|
||||
@@ -341,6 +354,13 @@ public sealed class AirAlarmSystem : EntitySystem
|
||||
SetData(uid, addr, args.Data);
|
||||
}
|
||||
break;
|
||||
|
||||
case AtmosSensorData sensorData:
|
||||
foreach (string addr in component.SensorData.Keys)
|
||||
{
|
||||
SetAllThresholds(uid, addr, sensorData);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,10 +33,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
|
||||
|
||||
// Commands
|
||||
public const string AtmosMonitorSetThresholdCmd = "atmos_monitor_set_threshold";
|
||||
public const string AtmosMonitorSetAllThresholdsCmd = "atmos_monitor_set_all_thresholds";
|
||||
|
||||
// Packet data
|
||||
public const string AtmosMonitorThresholdData = "atmos_monitor_threshold_data";
|
||||
|
||||
public const string AtmosMonitorAllThresholdData = "atmos_monitor_all_threshold_data";
|
||||
public const string AtmosMonitorThresholdDataType = "atmos_monitor_threshold_type";
|
||||
|
||||
public const string AtmosMonitorThresholdGasType = "atmos_monitor_threshold_gas";
|
||||
@@ -138,7 +139,12 @@ public sealed class AtmosMonitorSystem : EntitySystem
|
||||
args.Data.TryGetValue(AtmosMonitorThresholdGasType, out Gas? gas);
|
||||
SetThreshold(uid, thresholdType.Value, thresholdData, gas);
|
||||
}
|
||||
|
||||
break;
|
||||
case AtmosMonitorSetAllThresholdsCmd:
|
||||
if (args.Data.TryGetValue(AtmosMonitorAllThresholdData, out AtmosSensorData? allThresholdData))
|
||||
{
|
||||
SetAllThresholds(uid, allThresholdData);
|
||||
}
|
||||
break;
|
||||
case AtmosDeviceNetworkSystem.SyncData:
|
||||
var payload = new NetworkPayload();
|
||||
@@ -403,4 +409,20 @@ public sealed class AtmosMonitorSystem : EntitySystem
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets all of a monitor's thresholds at once according to the incoming
|
||||
/// AtmosSensorData object's thresholds.
|
||||
/// </summary>
|
||||
/// <param name="uid">The entity's uid</param>
|
||||
/// <param name="allThresholdData">An AtmosSensorData object from which the thresholds will be loaded.</param>
|
||||
public void SetAllThresholds(EntityUid uid, AtmosSensorData allThresholdData)
|
||||
{
|
||||
SetThreshold(uid, AtmosMonitorThresholdType.Temperature, allThresholdData.TemperatureThreshold);
|
||||
SetThreshold(uid, AtmosMonitorThresholdType.Pressure, allThresholdData.PressureThreshold);
|
||||
foreach (var gas in Enum.GetValues<Gas>())
|
||||
{
|
||||
SetThreshold(uid, AtmosMonitorThresholdType.Gas, allThresholdData.GasThresholds[gas], gas);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed partial class GasCondenserComponent : Component
|
||||
/// <summary>
|
||||
/// The solution that gases are condensed into.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -157,22 +157,22 @@ namespace Content.Server.Body.Components
|
||||
/// <summary>
|
||||
/// Internal solution for blood storage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Entity<SolutionComponent>? BloodSolution = null;
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? BloodSolution;
|
||||
|
||||
/// <summary>
|
||||
/// Internal solution for reagent storage
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Entity<SolutionComponent>? ChemicalSolution = null;
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? ChemicalSolution;
|
||||
|
||||
/// <summary>
|
||||
/// Temporary blood solution.
|
||||
/// When blood is lost, it goes to this solution, and when this
|
||||
/// solution hits a certain cap, the blood is actually spilled as a puddle.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Entity<SolutionComponent>? TemporarySolution = null;
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? TemporarySolution;
|
||||
|
||||
/// <summary>
|
||||
/// Variable that stores the amount of status time added by having a low blood level.
|
||||
|
||||
@@ -26,7 +26,7 @@ public sealed partial class LungComponent : Component
|
||||
/// <summary>
|
||||
/// The solution on this entity that these lungs act on.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -25,8 +25,8 @@ namespace Content.Server.Body.Components
|
||||
/// <summary>
|
||||
/// The solution inside of this stomach this transfers reagents to the body.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Entity<SolutionComponent>? Solution = null;
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? Solution;
|
||||
|
||||
/// <summary>
|
||||
/// What solution should this stomach push reagents into, on the body?
|
||||
|
||||
@@ -96,6 +96,6 @@ public sealed partial class PlantHolderComponent : Component
|
||||
[DataField]
|
||||
public string SoilSolutionName = "soil";
|
||||
|
||||
[DataField]
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? SoilSolution = null;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Timing;
|
||||
using Content.Shared.Cargo.Systems;
|
||||
using Robust.Shared.Audio.Systems;
|
||||
|
||||
namespace Content.Server.Cargo.Systems;
|
||||
|
||||
@@ -11,12 +13,12 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
|
||||
[Dependency] private readonly PricingSystem _pricingSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly CargoSystem _bountySystem = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
|
||||
protected override bool GetPriceOrBounty(EntityUid priceGunUid, EntityUid target, EntityUid user)
|
||||
protected override bool GetPriceOrBounty(Entity<PriceGunComponent> entity, EntityUid target, EntityUid user)
|
||||
{
|
||||
if (!TryComp(priceGunUid, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((priceGunUid, useDelay)))
|
||||
if (!TryComp(entity.Owner, out UseDelayComponent? useDelay) || _useDelay.IsDelayed((entity.Owner, useDelay)))
|
||||
return false;
|
||||
|
||||
// Check if we're scanning a bounty crate
|
||||
if (_bountySystem.IsBountyComplete(target, out _))
|
||||
{
|
||||
@@ -25,10 +27,15 @@ public sealed class PriceGunSystem : SharedPriceGunSystem
|
||||
else // Otherwise appraise the price
|
||||
{
|
||||
var price = _pricingSystem.GetPrice(target);
|
||||
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result", ("object", Identity.Entity(target, EntityManager)), ("price", $"{price:F2}")), user, user);
|
||||
_popupSystem.PopupEntity(Loc.GetString("price-gun-pricing-result",
|
||||
("object", Identity.Entity(target, EntityManager)),
|
||||
("price", $"{price:F2}")),
|
||||
user,
|
||||
user);
|
||||
}
|
||||
|
||||
_useDelay.TryResetDelay((priceGunUid, useDelay));
|
||||
_audio.PlayPvs(entity.Comp.AppraisalSound, entity.Owner);
|
||||
_useDelay.TryResetDelay((entity.Owner, useDelay));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Shared.CartridgeLoader;
|
||||
using Content.Shared.CartridgeLoader.Cartridges;
|
||||
using Content.Shared.Database;
|
||||
|
||||
namespace Content.Server.CartridgeLoader.Cartridges;
|
||||
|
||||
public sealed class NotekeeperCartridgeSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly CartridgeLoaderSystem? _cartridgeLoaderSystem = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -36,16 +39,19 @@ public sealed class NotekeeperCartridgeSystem : EntitySystem
|
||||
if (message.Action == NotekeeperUiAction.Add)
|
||||
{
|
||||
component.Notes.Add(message.Note);
|
||||
_adminLogger.Add(LogType.PdaInteract, LogImpact.Low,
|
||||
$"{ToPrettyString(args.Actor)} added a note to PDA: '{message.Note}' contained on: {ToPrettyString(uid)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
component.Notes.Remove(message.Note);
|
||||
_adminLogger.Add(LogType.PdaInteract, LogImpact.Low,
|
||||
$"{ToPrettyString(args.Actor)} removed a note from PDA: '{message.Note}' was contained on: {ToPrettyString(uid)}");
|
||||
}
|
||||
|
||||
UpdateUiState(uid, GetEntity(args.LoaderUid), component);
|
||||
}
|
||||
|
||||
|
||||
private void UpdateUiState(EntityUid uid, EntityUid loaderUid, NotekeeperCartridgeComponent? component)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
|
||||
@@ -20,7 +20,7 @@ public sealed partial class SolutionRegenerationComponent : Component
|
||||
/// <summary>
|
||||
/// The solution to add reagents to.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[ViewVariables]
|
||||
public Entity<SolutionComponent>? SolutionRef = null;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.IdentityManagement;
|
||||
using Content.Server.IdentityManagement;
|
||||
using Content.Shared.Clothing.Components;
|
||||
using Content.Shared.Clothing.EntitySystems;
|
||||
using Content.Shared.IdentityManagement.Components;
|
||||
@@ -63,7 +63,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
|
||||
if (!Resolve(uid, ref component))
|
||||
return;
|
||||
|
||||
var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default);
|
||||
var state = new ChameleonBoundUserInterfaceState(component.Slot, component.Default, component.RequireTag);
|
||||
_uiSystem.SetUiState(uid, ChameleonUiKey.Key, state);
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ public sealed class ChameleonClothingSystem : SharedChameleonClothingSystem
|
||||
// make sure that it is valid change
|
||||
if (string.IsNullOrEmpty(protoId) || !_proto.TryIndex(protoId, out EntityPrototype? proto))
|
||||
return;
|
||||
if (!IsValidTarget(proto, component.Slot))
|
||||
if (!IsValidTarget(proto, component.Slot, component.RequireTag))
|
||||
return;
|
||||
component.Default = protoId;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ public sealed partial class ConnectionManager
|
||||
{
|
||||
private PlayerConnectionWhitelistPrototype[]? _whitelists;
|
||||
|
||||
public void PostInit()
|
||||
private void InitializeWhitelist()
|
||||
{
|
||||
_cfg.OnValueChanged(CCVars.WhitelistPrototypeList, UpdateWhitelists, true);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
|
||||
using System.Runtime.InteropServices;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Connection.IPIntel;
|
||||
using Content.Server.Database;
|
||||
using Content.Server.GameTicking;
|
||||
using Content.Server.Preferences.Managers;
|
||||
@@ -40,6 +41,8 @@ namespace Content.Server.Connection
|
||||
/// <param name="user">The user to give a temporary bypass.</param>
|
||||
/// <param name="duration">How long the bypass should last for.</param>
|
||||
void AddTemporaryConnectBypass(NetUserId user, TimeSpan duration);
|
||||
|
||||
void Update();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -57,16 +60,24 @@ namespace Content.Server.Connection
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
[Dependency] private readonly ILogManager _logManager = default!;
|
||||
[Dependency] private readonly IChatManager _chatManager = default!;
|
||||
[Dependency] private readonly IHttpClientHolder _http = default!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = default!;
|
||||
|
||||
private ISawmill _sawmill = default!;
|
||||
private readonly Dictionary<NetUserId, TimeSpan> _temporaryBypasses = [];
|
||||
private IPIntel.IPIntel _ipintel = default!;
|
||||
|
||||
public void PostInit()
|
||||
{
|
||||
InitializeWhitelist();
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("connections");
|
||||
|
||||
_ipintel = new IPIntel.IPIntel(new IPIntelApi(_http, _cfg), _db, _cfg, _logManager, _chatManager, _gameTiming);
|
||||
|
||||
_netMgr.Connecting += NetMgrOnConnecting;
|
||||
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
|
||||
_plyMgr.PlayerStatusChanged += PlayerStatusChanged;
|
||||
@@ -83,6 +94,18 @@ namespace Content.Server.Connection
|
||||
time = newTime;
|
||||
}
|
||||
|
||||
public async void Update()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _ipintel.Update();
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_sawmill.Error("IPIntel update failed:" + e);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
private async Task<NetApproval> HandleApproval(NetApprovalEventArgs eventArgs)
|
||||
{
|
||||
@@ -260,14 +283,6 @@ namespace Content.Server.Connection
|
||||
}
|
||||
}
|
||||
|
||||
if (_cfg.GetCVar(CCVars.BabyJailEnabled) && adminData == null)
|
||||
{
|
||||
var result = await IsInvalidConnectionDueToBabyJail(userId, e);
|
||||
|
||||
if (result.IsInvalid)
|
||||
return (ConnectionDenyReason.BabyJail, result.Reason, null);
|
||||
}
|
||||
|
||||
var wasInGame = EntitySystem.TryGet<GameTicker>(out var ticker) &&
|
||||
ticker.PlayerGameStatuses.TryGetValue(userId, out var status) &&
|
||||
status == PlayerGameStatus.JoinedGame;
|
||||
@@ -291,7 +306,7 @@ namespace Content.Server.Connection
|
||||
{
|
||||
_sawmill.Error("Whitelist enabled but no whitelists loaded.");
|
||||
// Misconfigured, deny everyone.
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("whitelist-misconfigured"), null);
|
||||
return (ConnectionDenyReason.Whitelist, Loc.GetString("generic-misconfigured"), null);
|
||||
}
|
||||
|
||||
foreach (var whitelist in _whitelists)
|
||||
@@ -314,75 +329,18 @@ namespace Content.Server.Connection
|
||||
}
|
||||
}
|
||||
|
||||
// ALWAYS keep this at the end, to preserve the API limit.
|
||||
if (_cfg.GetCVar(CCVars.GameIPIntelEnabled) && adminData == null)
|
||||
{
|
||||
var result = await _ipintel.IsVpnOrProxy(e);
|
||||
|
||||
if (result.IsBad)
|
||||
return (ConnectionDenyReason.IPChecks, result.Reason, null);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<(bool IsInvalid, string Reason)> IsInvalidConnectionDueToBabyJail(NetUserId userId, NetConnectingArgs e)
|
||||
{
|
||||
// If you're whitelisted then bypass this whole thing
|
||||
if (await _db.GetWhitelistStatusAsync(userId))
|
||||
return (false, "");
|
||||
|
||||
// Initial cvar retrieval
|
||||
var showReason = _cfg.GetCVar(CCVars.BabyJailShowReason);
|
||||
var reason = _cfg.GetCVar(CCVars.BabyJailCustomReason);
|
||||
var maxAccountAgeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxAccountAge);
|
||||
var maxPlaytimeMinutes = _cfg.GetCVar(CCVars.BabyJailMaxOverallMinutes);
|
||||
|
||||
// Wait some time to lookup data
|
||||
var record = await _db.GetPlayerRecordByUserId(userId);
|
||||
|
||||
// No player record = new account or the DB is having a skill issue
|
||||
if (record == null)
|
||||
return (false, "");
|
||||
|
||||
var isAccountAgeInvalid = record.FirstSeenTime.CompareTo(DateTimeOffset.UtcNow - TimeSpan.FromMinutes(maxAccountAgeMinutes)) <= 0;
|
||||
|
||||
if (isAccountAgeInvalid)
|
||||
{
|
||||
_sawmill.Debug($"Baby jail will deny {userId} for account age {record.FirstSeenTime}"); // Remove on or after 2024-09
|
||||
}
|
||||
|
||||
if (isAccountAgeInvalid && showReason)
|
||||
{
|
||||
var locAccountReason = reason != string.Empty
|
||||
? reason
|
||||
: Loc.GetString("baby-jail-account-denied-reason",
|
||||
("reason",
|
||||
Loc.GetString(
|
||||
"baby-jail-account-reason-account",
|
||||
("minutes", maxAccountAgeMinutes))));
|
||||
|
||||
return (true, locAccountReason);
|
||||
}
|
||||
|
||||
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
var isTotalPlaytimeInvalid = overallTime != null && overallTime.TimeSpent.TotalMinutes >= maxPlaytimeMinutes;
|
||||
|
||||
if (isTotalPlaytimeInvalid)
|
||||
{
|
||||
_sawmill.Debug($"Baby jail will deny {userId} for playtime {overallTime!.TimeSpent}"); // Remove on or after 2024-09
|
||||
}
|
||||
|
||||
if (isTotalPlaytimeInvalid && showReason)
|
||||
{
|
||||
var locPlaytimeReason = reason != string.Empty
|
||||
? reason
|
||||
: Loc.GetString("baby-jail-account-denied-reason",
|
||||
("reason",
|
||||
Loc.GetString(
|
||||
"baby-jail-account-reason-overall",
|
||||
("minutes", maxPlaytimeMinutes))));
|
||||
|
||||
return (true, locPlaytimeReason);
|
||||
}
|
||||
|
||||
if (!showReason && isTotalPlaytimeInvalid || isAccountAgeInvalid)
|
||||
return (true, Loc.GetString("baby-jail-account-denied"));
|
||||
|
||||
return (false, "");
|
||||
}
|
||||
|
||||
private bool HasTemporaryBypass(NetUserId user)
|
||||
{
|
||||
return _temporaryBypasses.TryGetValue(user, out var time) && time > _gameTiming.RealTime;
|
||||
|
||||
387
Content.Server/Connection/IPIntel/IPIntel.cs
Normal file
387
Content.Server/Connection/IPIntel/IPIntel.cs
Normal file
@@ -0,0 +1,387 @@
|
||||
using System.Buffers.Binary;
|
||||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Chat.Managers;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.Players.PlayTimeTracking;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Connection.IPIntel;
|
||||
|
||||
// Handles checking/warning if the connecting IP address is sus.
|
||||
public sealed class IPIntel
|
||||
{
|
||||
private readonly IIPIntelApi _api;
|
||||
private readonly IServerDbManager _db;
|
||||
private readonly IChatManager _chatManager;
|
||||
private readonly IGameTiming _gameTiming;
|
||||
|
||||
private readonly ISawmill _sawmill;
|
||||
|
||||
public IPIntel(IIPIntelApi api,
|
||||
IServerDbManager db,
|
||||
IConfigurationManager cfg,
|
||||
ILogManager logManager,
|
||||
IChatManager chatManager,
|
||||
IGameTiming gameTiming)
|
||||
{
|
||||
_api = api;
|
||||
_db = db;
|
||||
_chatManager = chatManager;
|
||||
_gameTiming = gameTiming;
|
||||
|
||||
_sawmill = logManager.GetSawmill("ipintel");
|
||||
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEmail, b => _contactEmail = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEnabled, b => _enabled = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectUnknown, b => _rejectUnknown = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectBad, b => _rejectBad = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelRejectRateLimited, b => _rejectLimited = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelMaxMinute, b => _minute.Limit = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelMaxDay, b => _day.Limit = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBackOffSeconds, b => _backoffSeconds = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelCleanupMins, b => _cleanupMins = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBadRating, b => _rating = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelCacheLength, b => _cacheDays = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelExemptPlaytime, b => _exemptPlaytime = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminReject, b => _alertAdminReject = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelAlertAdminWarnRating, b => _alertAdminWarn = b, true);
|
||||
}
|
||||
|
||||
internal struct Ratelimits
|
||||
{
|
||||
public bool RateLimited;
|
||||
public bool LimitHasBeenHandled;
|
||||
public int CurrentRequests;
|
||||
public int Limit;
|
||||
public TimeSpan LastRatelimited;
|
||||
}
|
||||
|
||||
// Self-managed preemptive rate limits.
|
||||
private Ratelimits _day;
|
||||
private Ratelimits _minute;
|
||||
|
||||
// Next time we need to clean the database of stale cached IPIntel results.
|
||||
private TimeSpan _nextClean;
|
||||
|
||||
// Responsive backoff if we hit a Too Many Requests API error.
|
||||
private int _failedRequests;
|
||||
private TimeSpan _releasePeriod;
|
||||
|
||||
// CCVars
|
||||
private string? _contactEmail;
|
||||
private bool _enabled;
|
||||
private bool _rejectUnknown;
|
||||
private bool _rejectBad;
|
||||
private bool _rejectLimited;
|
||||
private bool _alertAdminReject;
|
||||
private int _backoffSeconds;
|
||||
private int _cleanupMins;
|
||||
private TimeSpan _cacheDays;
|
||||
private TimeSpan _exemptPlaytime;
|
||||
private float _rating;
|
||||
private float _alertAdminWarn;
|
||||
|
||||
public async Task<(bool IsBad, string Reason)> IsVpnOrProxy(NetConnectingArgs e)
|
||||
{
|
||||
// Check Exemption flags, let them skip if they have them.
|
||||
var flags = await _db.GetBanExemption(e.UserId);
|
||||
if ((flags & (ServerBanExemptFlags.Datacenter | ServerBanExemptFlags.BlacklistedRange)) != 0)
|
||||
{
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
// Check playtime, if 0 we skip this check. If player has more playtime then _exemptPlaytime is configured for then they get to skip this check.
|
||||
// Helps with saving your limited request limit.
|
||||
if (_exemptPlaytime != TimeSpan.Zero)
|
||||
{
|
||||
var overallTime = ( await _db.GetPlayTimes(e.UserId)).Find(p => p.Tracker == PlayTimeTrackingShared.TrackerOverall);
|
||||
if (overallTime != null && overallTime.TimeSpent >= _exemptPlaytime)
|
||||
{
|
||||
return (false, string.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
var ip = e.IP.Address;
|
||||
var username = e.UserName;
|
||||
|
||||
// Is this a local ip address?
|
||||
if (IsAddressReservedIpv4(ip) || IsAddressReservedIpv6(ip))
|
||||
{
|
||||
_sawmill.Warning($"{e.UserName} joined using a local address. Do you need IPIntel? Or is something terribly misconfigured on your server? Trusting this connection.");
|
||||
return (false, string.Empty);
|
||||
}
|
||||
|
||||
// Check our cache
|
||||
var query = await _db.GetIPIntelCache(ip);
|
||||
|
||||
// Does it exist?
|
||||
if (query != null)
|
||||
{
|
||||
// Skip to score check if result is older than _cacheDays
|
||||
if (DateTime.UtcNow - query.Time <= _cacheDays)
|
||||
{
|
||||
var score = query.Score;
|
||||
return ScoreCheck(score, username);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure our contact email is good to use.
|
||||
if (string.IsNullOrEmpty(_contactEmail) || !_contactEmail.Contains('@') || !_contactEmail.Contains('.'))
|
||||
{
|
||||
_sawmill.Error("IPIntel is enabled, but contact email is empty or not a valid email, treating this connection like an unknown IPIntel response.");
|
||||
return _rejectUnknown ? (true, Loc.GetString("generic-misconfigured")) : (false, string.Empty);
|
||||
}
|
||||
|
||||
var apiResult = await QueryIPIntelRateLimited(ip);
|
||||
switch (apiResult.Code)
|
||||
{
|
||||
case IPIntelResultCode.Success:
|
||||
await Task.Run(() => _db.UpsertIPIntelCache(DateTime.UtcNow, ip, apiResult.Score));
|
||||
return ScoreCheck(apiResult.Score, username);
|
||||
|
||||
case IPIntelResultCode.RateLimited:
|
||||
return _rejectLimited ? (true, Loc.GetString("ipintel-server-ratelimited")) : (false, string.Empty);
|
||||
|
||||
case IPIntelResultCode.Errored:
|
||||
return _rejectUnknown ? (true, Loc.GetString("ipintel-unknown")) : (false, string.Empty);
|
||||
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IPIntelResult> QueryIPIntelRateLimited(IPAddress ip)
|
||||
{
|
||||
IncrementAndTestRateLimit(ref _day, TimeSpan.FromDays(1), "daily");
|
||||
IncrementAndTestRateLimit(ref _minute, TimeSpan.FromMinutes(1), "minute");
|
||||
|
||||
if (_minute.RateLimited || _day.RateLimited || CheckSuddenRateLimit())
|
||||
return new IPIntelResult(0, IPIntelResultCode.RateLimited);
|
||||
|
||||
// Info about flag B: https://getipintel.net/free-proxy-vpn-tor-detection-api/#flagsb
|
||||
// TLDR: We don't care about knowing if a connection is compromised.
|
||||
// We just want to know if it's a vpn. This also speeds up the request by quite a bit. (A full scan can take 200ms to 5 seconds. This will take at most 120ms)
|
||||
using var request = await _api.GetIPScore(ip);
|
||||
|
||||
if (request.StatusCode == HttpStatusCode.TooManyRequests)
|
||||
{
|
||||
_sawmill.Warning($"We hit the IPIntel request limit at some point. (Current limit count: Minute: {_minute.CurrentRequests} Day: {_day.CurrentRequests})");
|
||||
CalculateSuddenRatelimit();
|
||||
return new IPIntelResult(0, IPIntelResultCode.RateLimited);
|
||||
}
|
||||
|
||||
var response = await request.Content.ReadAsStringAsync();
|
||||
var score = Parse.Float(response);
|
||||
|
||||
if (request.StatusCode == HttpStatusCode.OK)
|
||||
{
|
||||
_failedRequests = 0;
|
||||
return new IPIntelResult(score, IPIntelResultCode.Success);
|
||||
}
|
||||
|
||||
if (ErrorMessages.TryGetValue(response, out var errorMessage))
|
||||
{
|
||||
_sawmill.Error($"IPIntel returned error {response}: {errorMessage}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Oh boy, we don't know this error.
|
||||
_sawmill.Error($"IPIntel returned {response} (Status code: {request.StatusCode})... we don't know what this error code is. Please make an issue in upstream!");
|
||||
}
|
||||
|
||||
return new IPIntelResult(0, IPIntelResultCode.Errored);
|
||||
}
|
||||
|
||||
private bool CheckSuddenRateLimit()
|
||||
{
|
||||
return _failedRequests >= 1 && _releasePeriod > _gameTiming.RealTime;
|
||||
}
|
||||
|
||||
private void CalculateSuddenRatelimit()
|
||||
{
|
||||
_failedRequests++;
|
||||
_releasePeriod = _gameTiming.RealTime + TimeSpan.FromSeconds(_failedRequests * _backoffSeconds);
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> ErrorMessages = new()
|
||||
{
|
||||
["-1"] = "Invalid/No input.",
|
||||
["-2"] = "Invalid IP address.",
|
||||
["-3"] = "Unroutable address / private address given to the api. Make an issue in upstream as it should have been handled.",
|
||||
["-4"] = "Unable to reach IPIntel database. Perhaps it's down?",
|
||||
["-5"] = "Server's IP/Contact may have been banned, go to getipintel.net and make contact to be unbanned.",
|
||||
["-6"] = "You did not provide any contact information with your query or the contact information is invalid.",
|
||||
};
|
||||
|
||||
private void IncrementAndTestRateLimit(ref Ratelimits ratelimits, TimeSpan expireInterval, string name)
|
||||
{
|
||||
if (ratelimits.CurrentRequests < ratelimits.Limit)
|
||||
{
|
||||
ratelimits.CurrentRequests += 1;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ShouldLiftRateLimit(in ratelimits, expireInterval))
|
||||
{
|
||||
_sawmill.Info($"IPIntel {name} rate limit lifted. We are back to normal.");
|
||||
ratelimits.RateLimited = false;
|
||||
ratelimits.CurrentRequests = 0;
|
||||
ratelimits.LimitHasBeenHandled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (ratelimits.LimitHasBeenHandled)
|
||||
return;
|
||||
|
||||
_sawmill.Warning($"We just hit our last {name} IPIntel limit ({ratelimits.Limit})");
|
||||
ratelimits.RateLimited = true;
|
||||
ratelimits.LimitHasBeenHandled = true;
|
||||
ratelimits.LastRatelimited = _gameTiming.RealTime;
|
||||
}
|
||||
|
||||
private bool ShouldLiftRateLimit(in Ratelimits ratelimits, TimeSpan liftingTime)
|
||||
{
|
||||
// Should we raise this limit now?
|
||||
return ratelimits.RateLimited && _gameTiming.RealTime >= ratelimits.LastRatelimited + liftingTime;
|
||||
}
|
||||
|
||||
private (bool, string Empty) ScoreCheck(float score, string username)
|
||||
{
|
||||
var decisionIsReject = score > _rating;
|
||||
|
||||
if (_alertAdminWarn != 0f && _alertAdminWarn < score && !decisionIsReject)
|
||||
{
|
||||
_chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-warning",
|
||||
("player", username),
|
||||
("percent", Math.Round(score))));
|
||||
}
|
||||
|
||||
if (!decisionIsReject)
|
||||
return (false, string.Empty);
|
||||
|
||||
if (_alertAdminReject)
|
||||
{
|
||||
_chatManager.SendAdminAlert(Loc.GetString("admin-alert-ipintel-blocked",
|
||||
("player", username),
|
||||
("percent", Math.Round(score))));
|
||||
}
|
||||
|
||||
return _rejectBad ? (true, Loc.GetString("ipintel-suspicious")) : (false, string.Empty);
|
||||
}
|
||||
|
||||
public async Task Update()
|
||||
{
|
||||
if (_enabled && _gameTiming.RealTime >= _nextClean)
|
||||
{
|
||||
_nextClean = _gameTiming.RealTime + TimeSpan.FromMinutes(_cleanupMins);
|
||||
await _db.CleanIPIntelCache(_cacheDays);
|
||||
}
|
||||
}
|
||||
|
||||
// Stolen from Lidgren.Network (Space Wizards Edition) (NetReservedAddress.cs)
|
||||
// Modified with IPV6 on top
|
||||
private static int Ipv4(byte a, byte b, byte c, byte d)
|
||||
{
|
||||
return (a << 24) | (b << 16) | (c << 8) | d;
|
||||
}
|
||||
|
||||
// From miniupnpc
|
||||
private static readonly (int ip, int mask)[] ReservedRangesIpv4 =
|
||||
[
|
||||
// @formatter:off
|
||||
(Ipv4(0, 0, 0, 0), 8 ), // RFC1122 "This host on this network"
|
||||
(Ipv4(10, 0, 0, 0), 8 ), // RFC1918 Private-Use
|
||||
(Ipv4(100, 64, 0, 0), 10), // RFC6598 Shared Address Space
|
||||
(Ipv4(127, 0, 0, 0), 8 ), // RFC1122 Loopback
|
||||
(Ipv4(169, 254, 0, 0), 16), // RFC3927 Link-Local
|
||||
(Ipv4(172, 16, 0, 0), 12), // RFC1918 Private-Use
|
||||
(Ipv4(192, 0, 0, 0), 24), // RFC6890 IETF Protocol Assignments
|
||||
(Ipv4(192, 0, 2, 0), 24), // RFC5737 Documentation (TEST-NET-1)
|
||||
(Ipv4(192, 31, 196, 0), 24), // RFC7535 AS112-v4
|
||||
(Ipv4(192, 52, 193, 0), 24), // RFC7450 AMT
|
||||
(Ipv4(192, 88, 99, 0), 24), // RFC7526 6to4 Relay Anycast
|
||||
(Ipv4(192, 168, 0, 0), 16), // RFC1918 Private-Use
|
||||
(Ipv4(192, 175, 48, 0), 24), // RFC7534 Direct Delegation AS112 Service
|
||||
(Ipv4(198, 18, 0, 0), 15), // RFC2544 Benchmarking
|
||||
(Ipv4(198, 51, 100, 0), 24), // RFC5737 Documentation (TEST-NET-2)
|
||||
(Ipv4(203, 0, 113, 0), 24), // RFC5737 Documentation (TEST-NET-3)
|
||||
(Ipv4(224, 0, 0, 0), 4 ), // RFC1112 Multicast
|
||||
(Ipv4(240, 0, 0, 0), 4 ), // RFC1112 Reserved for Future Use + RFC919 Limited Broadcast
|
||||
// @formatter:on
|
||||
];
|
||||
|
||||
private static UInt128 ToAddressBytes(string ip)
|
||||
{
|
||||
return BinaryPrimitives.ReadUInt128BigEndian(IPAddress.Parse(ip).GetAddressBytes());
|
||||
}
|
||||
|
||||
private static readonly (UInt128 ip, int mask)[] ReservedRangesIpv6 =
|
||||
[
|
||||
(ToAddressBytes("::1"), 128), // "This host on this network"
|
||||
(ToAddressBytes("::ffff:0:0"), 96), // IPv4-mapped addresses
|
||||
(ToAddressBytes("::ffff:0:0:0"), 96), // IPv4-translated addresses
|
||||
(ToAddressBytes("64:ff9b:1::"), 48), // IPv4/IPv6 translation
|
||||
(ToAddressBytes("100::"), 64), // Discard prefix
|
||||
(ToAddressBytes("2001:20::"), 28), // ORCHIDv2
|
||||
(ToAddressBytes("2001:db8::"), 32), // Addresses used in documentation and example source code
|
||||
(ToAddressBytes("3fff::"), 20), // Addresses used in documentation and example source code
|
||||
(ToAddressBytes("5f00::"), 16), // IPv6 Segment Routing (SRv6)
|
||||
(ToAddressBytes("fc00::"), 7), // Unique local address
|
||||
];
|
||||
|
||||
internal static bool IsAddressReservedIpv4(IPAddress address)
|
||||
{
|
||||
if (address.AddressFamily != AddressFamily.InterNetwork)
|
||||
return false;
|
||||
|
||||
Span<byte> ipBitsByte = stackalloc byte[4];
|
||||
address.TryWriteBytes(ipBitsByte, out _);
|
||||
var ipBits = BinaryPrimitives.ReadInt32BigEndian(ipBitsByte);
|
||||
|
||||
foreach (var (reservedIp, maskBits) in ReservedRangesIpv4)
|
||||
{
|
||||
var mask = uint.MaxValue << (32 - maskBits);
|
||||
if ((ipBits & mask) == (reservedIp & mask))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
internal static bool IsAddressReservedIpv6(IPAddress address)
|
||||
{
|
||||
if (address.AddressFamily != AddressFamily.InterNetworkV6)
|
||||
return false;
|
||||
|
||||
if (address.IsIPv4MappedToIPv6)
|
||||
return IsAddressReservedIpv4(address.MapToIPv4());
|
||||
|
||||
Span<byte> ipBitsByte = stackalloc byte[16];
|
||||
address.TryWriteBytes(ipBitsByte, out _);
|
||||
var ipBits = BinaryPrimitives.ReadInt128BigEndian(ipBitsByte);
|
||||
|
||||
foreach (var (reservedIp, maskBits) in ReservedRangesIpv6)
|
||||
{
|
||||
var mask = UInt128.MaxValue << (128 - maskBits);
|
||||
if (((UInt128) ipBits & mask ) == (reservedIp & mask))
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public readonly record struct IPIntelResult(float Score, IPIntelResultCode Code);
|
||||
|
||||
public enum IPIntelResultCode : byte
|
||||
{
|
||||
Success = 0,
|
||||
RateLimited,
|
||||
Errored,
|
||||
}
|
||||
}
|
||||
40
Content.Server/Connection/IPIntel/IPIntelAPI.cs
Normal file
40
Content.Server/Connection/IPIntel/IPIntelAPI.cs
Normal file
@@ -0,0 +1,40 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Content.Server.Connection.IPIntel;
|
||||
|
||||
public interface IIPIntelApi
|
||||
{
|
||||
Task<HttpResponseMessage> GetIPScore(IPAddress ip);
|
||||
}
|
||||
|
||||
public sealed class IPIntelApi : IIPIntelApi
|
||||
{
|
||||
// Holds-The-HttpClient
|
||||
private readonly IHttpClientHolder _http;
|
||||
|
||||
// CCvars
|
||||
private string? _contactEmail;
|
||||
private string? _baseUrl;
|
||||
private string? _flags;
|
||||
|
||||
public IPIntelApi(
|
||||
IHttpClientHolder http,
|
||||
IConfigurationManager cfg)
|
||||
{
|
||||
_http = http;
|
||||
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelEmail, b => _contactEmail = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelBase, b => _baseUrl = b, true);
|
||||
cfg.OnValueChanged(CCVars.GameIPIntelFlags, b => _flags = b, true);
|
||||
}
|
||||
|
||||
public Task<HttpResponseMessage> GetIPScore(IPAddress ip)
|
||||
{
|
||||
return _http.Client.GetAsync($"{_baseUrl}/check.php?ip={ip}&contact={_contactEmail}&flags={_flags}");
|
||||
}
|
||||
}
|
||||
@@ -751,6 +751,20 @@ namespace Content.Server.Database
|
||||
existing.Flags = admin.Flags;
|
||||
existing.Title = admin.Title;
|
||||
existing.AdminRankId = admin.AdminRankId;
|
||||
existing.Deadminned = admin.Deadminned;
|
||||
existing.Suspended = admin.Suspended;
|
||||
|
||||
await db.DbContext.SaveChangesAsync(cancel);
|
||||
}
|
||||
|
||||
public async Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel)
|
||||
{
|
||||
await using var db = await GetDb(cancel);
|
||||
|
||||
var adminRecord = db.DbContext.Admin.Where(a => a.UserId == userId);
|
||||
await adminRecord.ExecuteUpdateAsync(
|
||||
set => set.SetProperty(p => p.Deadminned, deadminned),
|
||||
cancellationToken: cancel);
|
||||
|
||||
await db.DbContext.SaveChangesAsync(cancel);
|
||||
}
|
||||
@@ -1720,6 +1734,73 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#endregion
|
||||
|
||||
# region IPIntel
|
||||
|
||||
public async Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
var existing = await db.DbContext.IPIntelCache
|
||||
.Where(w => ip.Equals(w.Address))
|
||||
.SingleOrDefaultAsync();
|
||||
|
||||
if (existing == null)
|
||||
{
|
||||
var newCache = new IPIntelCache
|
||||
{
|
||||
Time = time,
|
||||
Address = ip,
|
||||
Score = score,
|
||||
};
|
||||
db.DbContext.IPIntelCache.Add(newCache);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing.Time = time;
|
||||
existing.Score = score;
|
||||
}
|
||||
|
||||
await Task.Delay(5000);
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
_opsLog.Warning("IPIntel UPSERT failed with a db exception... retrying.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
return await db.DbContext.IPIntelCache
|
||||
.SingleOrDefaultAsync(w => ip.Equals(w.Address));
|
||||
}
|
||||
|
||||
public async Task<bool> CleanIPIntelCache(TimeSpan range)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
// Calculating this here cause otherwise sqlite whines.
|
||||
var cutoffTime = DateTime.UtcNow.Subtract(range);
|
||||
|
||||
await db.DbContext.IPIntelCache
|
||||
.Where(w => w.Time <= cutoffTime)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// SQLite returns DateTime as Kind=Unspecified, Npgsql actually knows for sure it's Kind=Utc.
|
||||
// Normalize DateTimes here so they're always Utc. Thanks.
|
||||
protected abstract DateTime NormalizeDatabaseTime(DateTime time);
|
||||
|
||||
@@ -217,6 +217,16 @@ namespace Content.Server.Database
|
||||
Task AddAdminAsync(Admin admin, CancellationToken cancel = default);
|
||||
Task UpdateAdminAsync(Admin admin, CancellationToken cancel = default);
|
||||
|
||||
/// <summary>
|
||||
/// Update whether an admin has voluntarily deadminned.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This does nothing if the player is not an admin.
|
||||
/// </remarks>
|
||||
/// <param name="userId">The user ID of the admin.</param>
|
||||
/// <param name="deadminned">Whether the admin is deadminned or not.</param>
|
||||
Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default);
|
||||
|
||||
Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default);
|
||||
Task AddAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
|
||||
Task UpdateAdminRankAsync(AdminRank rank, CancellationToken cancel = default);
|
||||
@@ -322,6 +332,14 @@ namespace Content.Server.Database
|
||||
|
||||
#endregion
|
||||
|
||||
#region IPintel
|
||||
|
||||
Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score);
|
||||
Task<IPIntelCache?> GetIPIntelCache(IPAddress ip);
|
||||
Task<bool> CleanIPIntelCache(TimeSpan range);
|
||||
|
||||
#endregion
|
||||
|
||||
#region DB Notifications
|
||||
|
||||
void SubscribeToNotifications(Action<DatabaseNotification> handler);
|
||||
@@ -666,6 +684,12 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.UpdateAdminAsync(admin, cancel));
|
||||
}
|
||||
|
||||
public Task UpdateAdminDeadminnedAsync(NetUserId userId, bool deadminned, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.UpdateAdminDeadminnedAsync(userId, deadminned, cancel));
|
||||
}
|
||||
|
||||
public Task RemoveAdminRankAsync(int rankId, CancellationToken cancel = default)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
@@ -991,6 +1015,23 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.RemoveJobWhitelist(player, job));
|
||||
}
|
||||
|
||||
public Task<bool> UpsertIPIntelCache(DateTime time, IPAddress ip, float score)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.UpsertIPIntelCache(time, ip, score));
|
||||
}
|
||||
|
||||
public Task<IPIntelCache?> GetIPIntelCache(IPAddress ip)
|
||||
{
|
||||
return RunDbCommand(() => _db.GetIPIntelCache(ip));
|
||||
}
|
||||
|
||||
public Task<bool> CleanIPIntelCache(TimeSpan range)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.CleanIPIntelCache(range));
|
||||
}
|
||||
|
||||
public void SubscribeToNotifications(Action<DatabaseNotification> handler)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
|
||||
@@ -47,6 +47,8 @@ namespace Content.Server.Entry
|
||||
private PlayTimeTrackingManager? _playTimeTracking;
|
||||
private IEntitySystemManager? _sysMan;
|
||||
private IServerDbManager? _dbManager;
|
||||
private IWatchlistWebhookManager _watchlistWebhookManager = default!;
|
||||
private IConnectionManager? _connectionManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Init()
|
||||
@@ -91,8 +93,10 @@ namespace Content.Server.Entry
|
||||
_voteManager = IoCManager.Resolve<IVoteManager>();
|
||||
_updateManager = IoCManager.Resolve<ServerUpdateManager>();
|
||||
_playTimeTracking = IoCManager.Resolve<PlayTimeTrackingManager>();
|
||||
_connectionManager = IoCManager.Resolve<IConnectionManager>();
|
||||
_sysMan = IoCManager.Resolve<IEntitySystemManager>();
|
||||
_dbManager = IoCManager.Resolve<IServerDbManager>();
|
||||
_watchlistWebhookManager = IoCManager.Resolve<IWatchlistWebhookManager>();
|
||||
|
||||
logManager.GetSawmill("Storage").Level = LogLevel.Info;
|
||||
logManager.GetSawmill("db.ef").Level = LogLevel.Info;
|
||||
@@ -110,6 +114,7 @@ namespace Content.Server.Entry
|
||||
_voteManager.Initialize();
|
||||
_updateManager.Initialize();
|
||||
_playTimeTracking.Initialize();
|
||||
_watchlistWebhookManager.Initialize();
|
||||
IoCManager.Resolve<JobWhitelistManager>().Initialize();
|
||||
IoCManager.Resolve<PlayerRateLimitManager>().Initialize();
|
||||
}
|
||||
@@ -166,6 +171,8 @@ namespace Content.Server.Entry
|
||||
case ModUpdateLevel.FramePostEngine:
|
||||
_updateManager.Update();
|
||||
_playTimeTracking?.Update();
|
||||
_watchlistWebhookManager.Update();
|
||||
_connectionManager?.Update();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +512,8 @@ public sealed partial class ExplosionSystem
|
||||
List<(Vector2i GridIndices, Tile Tile)> damagedTiles,
|
||||
ExplosionPrototype type)
|
||||
{
|
||||
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef)
|
||||
if (_tileDefinitionManager[tileRef.Tile.TypeId] is not ContentTileDefinition tileDef
|
||||
|| tileDef.Indestructible)
|
||||
return;
|
||||
|
||||
if (!CanCreateVacuum)
|
||||
|
||||
@@ -356,6 +356,11 @@ public sealed partial class ExplosionSystem : SharedExplosionSystem
|
||||
// + if the bomb is big enough, people outside of it too
|
||||
// this is capped to 30 because otherwise really huge bombs
|
||||
// will attempt to play regular audio for people who can't hear it anyway because the epicenter is so far away
|
||||
//
|
||||
// TODO EXPLOSION redo this.
|
||||
// Use the Filter.Pvs range-multiplier option instead of AddInRange.
|
||||
// Also the default PVS range is 25*2 = 50. So capping it at 30 makes no sense here.
|
||||
// So actually maybe don't use Filter.Pvs at all and only use AddInRange?
|
||||
var audioRange = Math.Min(iterationIntensity.Count * 2, MaxExplosionAudioRange);
|
||||
var filter = Filter.Pvs(pos).AddInRange(pos, audioRange);
|
||||
var sound = iterationIntensity.Count < queued.Proto.SmallSoundIterationThreshold
|
||||
|
||||
@@ -77,6 +77,8 @@ public sealed partial class PuddleSystem
|
||||
Spawn("PuddleSparkle", xformQuery.GetComponent(uid).Coordinates);
|
||||
QueueDel(uid);
|
||||
}
|
||||
|
||||
_solutionContainerSystem.UpdateChemicals(puddle.Solution.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,8 +222,6 @@ namespace Content.Server.GameTicking
|
||||
_mind.SetUserId(newMind, data.UserId);
|
||||
|
||||
var jobPrototype = _prototypeManager.Index<JobPrototype>(jobId);
|
||||
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
|
||||
var jobName = _jobs.MindTryGetJobName(newMind);
|
||||
|
||||
_playTimeTrackings.PlayerRolesChanged(player);
|
||||
|
||||
@@ -233,6 +231,9 @@ namespace Content.Server.GameTicking
|
||||
|
||||
_mind.TransferTo(newMind, mob);
|
||||
|
||||
_roles.MindAddJobRole(newMind, silent: silent, jobPrototype:jobId);
|
||||
var jobName = _jobs.MindTryGetJobName(newMind);
|
||||
|
||||
if (lateJoin && !silent)
|
||||
{
|
||||
if (jobPrototype.JoinNotifyCrew)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq;
|
||||
using System.Text.Json.Nodes;
|
||||
using Content.Shared.CCVar;
|
||||
using Content.Shared.GameTicking;
|
||||
@@ -43,15 +44,11 @@ namespace Content.Server.GameTicking
|
||||
jObject["name"] = _baseServer.ServerName;
|
||||
jObject["map"] = _gameMapManager.GetSelectedMap()?.MapName;
|
||||
jObject["round_id"] = _gameTicker.RoundId;
|
||||
jObject["players"] = _playerManager.PlayerCount;
|
||||
jObject["players"] = _cfg.GetCVar(CCVars.AdminsCountInReportedPlayerCount)
|
||||
? _playerManager.PlayerCount
|
||||
: _playerManager.PlayerCount - _adminManager.ActiveAdmins.Count();
|
||||
jObject["soft_max_players"] = _cfg.GetCVar(CCVars.SoftMaxPlayers);
|
||||
jObject["panic_bunker"] = _cfg.GetCVar(CCVars.PanicBunkerEnabled);
|
||||
|
||||
/*
|
||||
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
|
||||
*/
|
||||
|
||||
jObject["baby_jail"] = _cfg.GetCVar(CCVars.BabyJailEnabled);
|
||||
jObject["run_level"] = (int) _runLevel;
|
||||
if (preset != null)
|
||||
jObject["preset"] = Loc.GetString(preset.ModeTitle);
|
||||
|
||||
37
Content.Server/Ghost/Components/SpookySpeakerComponent.cs
Normal file
37
Content.Server/Ghost/Components/SpookySpeakerComponent.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using Content.Shared.Dataset;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.Server.Ghost.Components;
|
||||
|
||||
/// <summary>
|
||||
/// Causes this entity to react to ghost player using the "Boo!" action by speaking
|
||||
/// a randomly chosen message from a specified set.
|
||||
/// </summary>
|
||||
[RegisterComponent, AutoGenerateComponentPause]
|
||||
public sealed partial class SpookySpeakerComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// ProtoId of the LocalizedDataset to use for messages.
|
||||
/// </summary>
|
||||
[DataField(required: true)]
|
||||
public ProtoId<LocalizedDatasetPrototype> MessageSet;
|
||||
|
||||
/// <summary>
|
||||
/// Probability that this entity will speak if activated by a Boo action.
|
||||
/// This is so whole banks of entities don't trigger at the same time.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float SpeakChance = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Minimum time that must pass after speaking before this entity can speak again.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public TimeSpan Cooldown = TimeSpan.FromSeconds(30);
|
||||
|
||||
/// <summary>
|
||||
/// Time when the cooldown will have elapsed and the entity can speak again.
|
||||
/// </summary>
|
||||
[DataField, AutoPausedField]
|
||||
public TimeSpan NextSpeakTime;
|
||||
}
|
||||
@@ -24,6 +24,7 @@ using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Movement.Events;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.Player;
|
||||
@@ -33,6 +34,7 @@ using Robust.Shared.Physics.Components;
|
||||
using Robust.Shared.Physics.Systems;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Ghost
|
||||
@@ -61,6 +63,8 @@ namespace Content.Server.Ghost
|
||||
[Dependency] private readonly SharedMindSystem _mind = default!;
|
||||
[Dependency] private readonly GameTicker _gameTicker = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageable = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
|
||||
private EntityQuery<GhostComponent> _ghostQuery;
|
||||
private EntityQuery<PhysicsComponent> _physicsQuery;
|
||||
@@ -125,7 +129,9 @@ namespace Content.Server.Ghost
|
||||
if (args.Handled)
|
||||
return;
|
||||
|
||||
var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius);
|
||||
var entities = _lookup.GetEntitiesInRange(args.Performer, component.BooRadius).ToList();
|
||||
// Shuffle the possible targets so we don't favor any particular entities
|
||||
_random.Shuffle(entities);
|
||||
|
||||
var booCounter = 0;
|
||||
foreach (var ent in entities)
|
||||
@@ -139,6 +145,9 @@ namespace Content.Server.Ghost
|
||||
break;
|
||||
}
|
||||
|
||||
if (booCounter == 0)
|
||||
_popup.PopupEntity(Loc.GetString("ghost-component-boo-action-failed"), uid, uid);
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
namespace Content.Server.Ghost;
|
||||
using Content.Shared.Roles;
|
||||
|
||||
namespace Content.Server.Ghost;
|
||||
|
||||
/// <summary>
|
||||
/// This is used to mark Observers properly, as they get Minds
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class ObserverRoleComponent : Component
|
||||
public sealed partial class ObserverRoleComponent : BaseMindRoleComponent
|
||||
{
|
||||
public string Name => Loc.GetString("observer-role-name");
|
||||
}
|
||||
|
||||
@@ -72,12 +72,16 @@ public sealed partial class GhostRoleComponent : Component
|
||||
}
|
||||
}
|
||||
|
||||
[DataField("allowSpeech")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
/// <summary>
|
||||
/// The mind roles that will be added to the mob's mind entity
|
||||
/// </summary>
|
||||
[DataField, Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // Don't make eye contact
|
||||
public List<EntProtoId> MindRoles = new() { "MindRoleGhostRoleNeutral" };
|
||||
|
||||
[DataField]
|
||||
public bool AllowSpeech { get; set; } = true;
|
||||
|
||||
[DataField("allowMovement")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public bool AllowMovement { get; set; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
@@ -107,3 +111,4 @@ public sealed partial class GhostRoleComponent : Component
|
||||
[Access(typeof(GhostRoleSystem), Other = AccessPermissions.ReadWriteExecute)] // also FIXME Friends
|
||||
public ProtoId<JobPrototype>? JobProto = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,39 +9,81 @@ namespace Content.Server.Ghost.Roles.Components;
|
||||
[RegisterComponent, Access(typeof(ToggleableGhostRoleSystem))]
|
||||
public sealed partial class ToggleableGhostRoleComponent : Component
|
||||
{
|
||||
[DataField("examineTextMindPresent")]
|
||||
/// <summary>
|
||||
/// The text shown on the entity's Examine when it is controlled by a player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string ExamineTextMindPresent = string.Empty;
|
||||
|
||||
[DataField("examineTextMindSearching")]
|
||||
/// <summary>
|
||||
/// The text shown on the entity's Examine when it is waiting for a controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string ExamineTextMindSearching = string.Empty;
|
||||
|
||||
[DataField("examineTextNoMind")]
|
||||
/// <summary>
|
||||
/// The text shown on the entity's Examine when it has no controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string ExamineTextNoMind = string.Empty;
|
||||
|
||||
[DataField("beginSearchingText")]
|
||||
/// <summary>
|
||||
/// The popup text when the entity (PAI/positronic brain) it is activated to seek a controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string BeginSearchingText = string.Empty;
|
||||
|
||||
[DataField("roleName")]
|
||||
/// <summary>
|
||||
/// The name shown on the Ghost Role list
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string RoleName = string.Empty;
|
||||
|
||||
[DataField("roleDescription")]
|
||||
/// <summary>
|
||||
/// The description shown on the Ghost Role list
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string RoleDescription = string.Empty;
|
||||
|
||||
[DataField("roleRules")]
|
||||
/// <summary>
|
||||
/// The introductory message shown when trying to take the ghost role/join the raffle
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string RoleRules = string.Empty;
|
||||
|
||||
[DataField("wipeVerbText")]
|
||||
/// <summary>
|
||||
/// A list of mind roles that will be added to the entity's mind
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<EntProtoId> MindRoles;
|
||||
|
||||
/// <summary>
|
||||
/// The displayed name of the verb to wipe the controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string WipeVerbText = string.Empty;
|
||||
|
||||
[DataField("wipeVerbPopup")]
|
||||
/// /// <summary>
|
||||
/// The popup message when wiping the controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string WipeVerbPopup = string.Empty;
|
||||
|
||||
[DataField("stopSearchVerbText")]
|
||||
/// <summary>
|
||||
/// The displayed name of the verb to stop searching for a controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string StopSearchVerbText = string.Empty;
|
||||
|
||||
[DataField("stopSearchVerbPopup")]
|
||||
/// /// <summary>
|
||||
/// The popup message when stopping to search for a controlling player
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public string StopSearchVerbPopup = string.Empty;
|
||||
|
||||
/// /// <summary>
|
||||
/// The prototype ID of the job that will be given to the controlling mind
|
||||
/// </summary>
|
||||
[DataField("job")]
|
||||
public ProtoId<JobPrototype>? JobProto = null;
|
||||
public ProtoId<JobPrototype>? JobProto;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
namespace Content.Server.Ghost.Roles;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for round end display of ghost roles.
|
||||
/// It may also be used to ensure some ghost roles count as antagonists in future.
|
||||
/// Added to mind role entities to tag that they are a ghostrole.
|
||||
/// It also holds the name for the round end display
|
||||
/// </summary>
|
||||
[RegisterComponent]
|
||||
public sealed partial class GhostRoleMarkerRoleComponent : BaseMindRoleComponent
|
||||
{
|
||||
[DataField("name")] public string? Name;
|
||||
//TODO does anything still use this? It gets populated by GhostRolesystem but I don't see anything ever reading it
|
||||
[DataField] public string? Name;
|
||||
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ using Content.Server.Popups;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Collections;
|
||||
using Content.Shared.Ghost.Roles.Components;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
|
||||
namespace Content.Server.Ghost.Roles;
|
||||
|
||||
@@ -514,13 +513,13 @@ public sealed class GhostRoleSystem : EntitySystem
|
||||
var newMind = _mindSystem.CreateMind(player.UserId,
|
||||
EntityManager.GetComponent<MetaDataComponent>(mob).EntityName);
|
||||
|
||||
_roleSystem.MindAddRole(newMind, "MindRoleGhostMarker");
|
||||
|
||||
if(_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
|
||||
markerRole.Value.Comp2.Name = role.RoleName;
|
||||
|
||||
_mindSystem.SetUserId(newMind, player.UserId);
|
||||
_mindSystem.TransferTo(newMind, mob);
|
||||
|
||||
_roleSystem.MindAddRoles(newMind.Owner, role.MindRoles, newMind.Comp);
|
||||
|
||||
if (_roleSystem.MindHasRole<GhostRoleMarkerRoleComponent>(newMind!, out var markerRole))
|
||||
markerRole.Value.Comp2.Name = role.RoleName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -51,10 +51,13 @@ public sealed class ToggleableGhostRoleSystem : EntitySystem
|
||||
|
||||
var ghostRole = EnsureComp<GhostRoleComponent>(uid);
|
||||
EnsureComp<GhostTakeoverAvailableComponent>(uid);
|
||||
|
||||
//GhostRoleComponent inherits custom settings from the ToggleableGhostRoleComponent
|
||||
ghostRole.RoleName = Loc.GetString(component.RoleName);
|
||||
ghostRole.RoleDescription = Loc.GetString(component.RoleDescription);
|
||||
ghostRole.RoleRules = Loc.GetString(component.RoleRules);
|
||||
ghostRole.JobProto = component.JobProto;
|
||||
ghostRole.MindRoles = component.MindRoles;
|
||||
}
|
||||
|
||||
private void OnExamined(EntityUid uid, ToggleableGhostRoleComponent component, ExaminedEvent args)
|
||||
|
||||
51
Content.Server/Ghost/SpookySpeakerSystem.cs
Normal file
51
Content.Server/Ghost/SpookySpeakerSystem.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Ghost.Components;
|
||||
using Content.Shared.Random.Helpers;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Server.Ghost;
|
||||
|
||||
public sealed class SpookySpeakerSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<SpookySpeakerComponent, GhostBooEvent>(OnGhostBoo);
|
||||
}
|
||||
|
||||
private void OnGhostBoo(Entity<SpookySpeakerComponent> entity, ref GhostBooEvent args)
|
||||
{
|
||||
// Only activate sometimes, so groups don't all trigger together
|
||||
if (!_random.Prob(entity.Comp.SpeakChance))
|
||||
return;
|
||||
|
||||
var curTime = _timing.CurTime;
|
||||
// Enforce a delay between messages to prevent spam
|
||||
if (curTime < entity.Comp.NextSpeakTime)
|
||||
return;
|
||||
|
||||
if (!_proto.TryIndex(entity.Comp.MessageSet, out var messages))
|
||||
return;
|
||||
|
||||
// Grab a random localized message from the set
|
||||
var message = _random.Pick(messages);
|
||||
// Chatcode moment: messages starting with '.' are considered radio messages unless prefixed with '>'
|
||||
// So this is a stupid trick to make the "...Oooo"-style messages work.
|
||||
message = '>' + message;
|
||||
// Say the message
|
||||
_chat.TrySendInGameICMessage(entity, message, InGameICChatType.Speak, hideChat: true);
|
||||
|
||||
// Set the delay for the next message
|
||||
entity.Comp.NextSpeakTime = curTime + entity.Comp.Cooldown;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user