Merge branch 'master' of https://github.com/crystallpunk-14/crystall-punk-14
This commit is contained in:
@@ -344,6 +344,9 @@ resharper_keep_existing_attribute_arrangement = true
|
||||
resharper_wrap_chained_binary_patterns = chop_if_long
|
||||
resharper_wrap_chained_method_calls = chop_if_long
|
||||
resharper_csharp_trailing_comma_in_multiline_lists = true
|
||||
resharper_csharp_qualified_using_at_nested_scope = false
|
||||
resharper_csharp_prefer_qualified_reference = false
|
||||
resharper_csharp_allow_alias = false
|
||||
|
||||
[*.{csproj,xml,yml,yaml,dll.config,msbuildproj,targets,props}]
|
||||
indent_size = 2
|
||||
|
||||
5
.envrc
5
.envrc
@@ -1,4 +1,5 @@
|
||||
if ! has nix_direnv_version || ! nix_direnv_version 3.0.4; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.4/direnvrc" "sha256-DzlYZ33mWF/Gs8DDeyjr8mnVmQGx7ASYqA5WlxwvBG4="
|
||||
set -e
|
||||
if ! has nix_direnv_version || ! nix_direnv_version 3.0.6; then
|
||||
source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.6/direnvrc" "sha256-RYcUJaRMf8oF5LznDrlCXbkOQrywm0HDv1VjYGaJGdM="
|
||||
fi
|
||||
use flake
|
||||
|
||||
@@ -46,7 +46,7 @@ public class MapLoadBenchmark
|
||||
PoolManager.Shutdown();
|
||||
}
|
||||
|
||||
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog" };
|
||||
public static readonly string[] MapsSource = { "Empty", "Satlern", "Box", "Bagel", "Dev", "CentComm", "Core", "TestTeg", "Packed", "Omega", "Reach", "Meta", "Marathon", "MeteorArena", "Fland", "Oasis", "Cog", "Convex"};
|
||||
|
||||
[ParamsSource(nameof(MapsSource))]
|
||||
public string Map;
|
||||
|
||||
@@ -39,6 +39,6 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface
|
||||
if (message is not CargoBountyConsoleState state)
|
||||
return;
|
||||
|
||||
_menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
|
||||
_menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip);
|
||||
}
|
||||
}
|
||||
|
||||
22
Content.Client/Cargo/UI/BountyHistoryEntry.xaml
Normal file
22
Content.Client/Cargo/UI/BountyHistoryEntry.xaml
Normal file
@@ -0,0 +1,22 @@
|
||||
<BoxContainer xmlns="https://spacestation14.io"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Margin="10 10 10 0"
|
||||
HorizontalExpand="True">
|
||||
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
|
||||
<RichTextLabel Name="RewardLabel"/>
|
||||
<RichTextLabel Name="ManifestLabel"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Vertical" MinWidth="120" Margin="0 0 10 0">
|
||||
<RichTextLabel Name="TimestampLabel" HorizontalAlignment="Right" />
|
||||
<RichTextLabel Name="IdLabel" HorizontalAlignment="Right" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<customControls:HSeparator Margin="5 10 5 10"/>
|
||||
<RichTextLabel Name="NoticeLabel" />
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
49
Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
Normal file
49
Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Shared.Cargo;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Client.Cargo.UI;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class BountyHistoryEntry : BoxContainer
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototype = default!;
|
||||
|
||||
public BountyHistoryEntry(CargoBountyHistoryData bounty)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype))
|
||||
return;
|
||||
|
||||
var items = new List<string>();
|
||||
foreach (var entry in bountyPrototype.Entries)
|
||||
{
|
||||
items.Add(Loc.GetString("bounty-console-manifest-entry",
|
||||
("amount", entry.Amount),
|
||||
("item", Loc.GetString(entry.Name))));
|
||||
}
|
||||
|
||||
ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items))));
|
||||
RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward)));
|
||||
IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id)));
|
||||
|
||||
TimestampLabel.SetMarkup(bounty.Timestamp.ToString(@"hh\:mm\:ss"));
|
||||
|
||||
if (bounty.Result == CargoBountyHistoryData.BountyResult.Completed)
|
||||
{
|
||||
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-completed-label"));
|
||||
}
|
||||
else
|
||||
{
|
||||
NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-skipped-label",
|
||||
("id", bounty.ActorName ?? "")));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,15 +11,28 @@
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<ScrollContainer HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Name="BountyEntriesContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
</BoxContainer>
|
||||
</ScrollContainer>
|
||||
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True">
|
||||
<ScrollContainer HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<BoxContainer Name="BountyEntriesContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
<ScrollContainer HScrollEnabled="False"
|
||||
HorizontalExpand="True"
|
||||
VerticalExpand="True">
|
||||
<Label Name="NoHistoryLabel"
|
||||
Text="{Loc 'bounty-console-history-empty-label'}"
|
||||
Visible="False"
|
||||
Align="Center" />
|
||||
<BoxContainer Name="BountyHistoryContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True" />
|
||||
</ScrollContainer>
|
||||
</TabContainer>
|
||||
</PanelContainer>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
|
||||
@@ -15,9 +15,12 @@ public sealed partial class CargoBountyMenu : FancyWindow
|
||||
public CargoBountyMenu()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
MasterTabContainer.SetTabTitle(0, Loc.GetString("bounty-console-tab-available-label"));
|
||||
MasterTabContainer.SetTabTitle(1, Loc.GetString("bounty-console-tab-history-label"));
|
||||
}
|
||||
|
||||
public void UpdateEntries(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
|
||||
public void UpdateEntries(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
|
||||
{
|
||||
BountyEntriesContainer.Children.Clear();
|
||||
foreach (var b in bounties)
|
||||
@@ -32,5 +35,21 @@ public sealed partial class CargoBountyMenu : FancyWindow
|
||||
{
|
||||
MinHeight = 10
|
||||
});
|
||||
|
||||
BountyHistoryContainer.Children.Clear();
|
||||
if (history.Count == 0)
|
||||
{
|
||||
NoHistoryLabel.Visible = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
NoHistoryLabel.Visible = false;
|
||||
|
||||
// Show the history in reverse, so last entry is first in the list
|
||||
for (var i = history.Count - 1; i >= 0; i--)
|
||||
{
|
||||
BountyHistoryContainer.AddChild(new BountyHistoryEntry(history[i]));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ using JetBrains.Annotations;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Content.Client.Changelog
|
||||
@@ -15,8 +17,9 @@ namespace Content.Client.Changelog
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class ChangelogWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly ChangelogManager _changelog = default!;
|
||||
[Dependency] private readonly IClientAdminManager _adminManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public ChangelogWindow()
|
||||
{
|
||||
@@ -67,8 +70,22 @@ namespace Content.Client.Changelog
|
||||
Tabs.SetTabTitle(i++, Loc.GetString($"changelog-tab-title-{changelog.Name}"));
|
||||
}
|
||||
|
||||
var version = typeof(ChangelogWindow).Assembly.GetName().Version ?? new Version(1, 0);
|
||||
VersionLabel.Text = Loc.GetString("changelog-version-tag", ("version", version.ToString()));
|
||||
// Try to get the current version from the build.json file
|
||||
var version = _cfg.GetCVar(CVars.BuildVersion);
|
||||
var forkId = _cfg.GetCVar(CVars.BuildForkId);
|
||||
|
||||
var versionText = Loc.GetString("changelog-version-unknown");
|
||||
|
||||
// Make sure these aren't empty, like in a dev env
|
||||
if (!string.IsNullOrEmpty(version) && !string.IsNullOrEmpty(forkId))
|
||||
{
|
||||
versionText = Loc.GetString("changelog-version-tag",
|
||||
("fork", forkId),
|
||||
("version", version[..7])); // Only show the first 7 characters
|
||||
}
|
||||
|
||||
// if else statements are ugly, shut up
|
||||
VersionLabel.Text = versionText;
|
||||
|
||||
TabsUpdated();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
@@ -7,25 +7,25 @@
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- General -->
|
||||
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Vocal -->
|
||||
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Hands -->
|
||||
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -50,7 +50,6 @@ public sealed partial class EmotesMenu : RadialMenu
|
||||
|
||||
var button = new EmoteMenuButton
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = Loc.GetString(emote.Name),
|
||||
ProtoId = emote.ID,
|
||||
@@ -106,7 +105,7 @@ public sealed partial class EmotesMenu : RadialMenu
|
||||
}
|
||||
|
||||
|
||||
public sealed class EmoteMenuButton : RadialMenuTextureButton
|
||||
public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<EmotePrototype> ProtoId { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Content.Client.Clickable
|
||||
"/Textures/Logo",
|
||||
};
|
||||
|
||||
private const float Threshold = 0.25f;
|
||||
private const float Threshold = 0.1f;
|
||||
private const int ClickRadius = 2;
|
||||
|
||||
[Dependency] private readonly IResourceCache _resourceCache = default!;
|
||||
|
||||
@@ -22,7 +22,7 @@ public sealed class CrewManifestSection : BoxContainer
|
||||
AddChild(new Label()
|
||||
{
|
||||
StyleClasses = { "LabelBig" },
|
||||
Text = Loc.GetString($"department-{section.ID}")
|
||||
Text = Loc.GetString(section.Name)
|
||||
});
|
||||
|
||||
var gridContainer = new GridContainer()
|
||||
|
||||
@@ -39,6 +39,8 @@ public sealed class CriminalRecordsConsoleBoundUserInterface : BoundUserInterfac
|
||||
SendMessage(new CriminalRecordChangeStatus(status, null));
|
||||
_window.OnDialogConfirmed += (status, reason) =>
|
||||
SendMessage(new CriminalRecordChangeStatus(status, reason));
|
||||
_window.OnStatusFilterPressed += (statusFilter) =>
|
||||
SendMessage(new CriminalRecordSetStatusFilter(statusFilter));
|
||||
_window.OnHistoryUpdated += UpdateHistory;
|
||||
_window.OnHistoryClosed += () => _historyWindow?.Close();
|
||||
_window.OnClose += Close;
|
||||
|
||||
@@ -1,36 +1,142 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'criminal-records-console-window-title'}"
|
||||
MinSize="660 400">
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
Title="{Loc 'criminal-records-console-window-title'}"
|
||||
MinSize="695 440">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<!-- Record search bar
|
||||
TODO: make this into a control shared with general records -->
|
||||
<BoxContainer Margin="5 5 5 10" HorizontalExpand="true" VerticalAlignment="Center">
|
||||
<OptionButton Name="FilterType" MinWidth="200" Margin="0 0 10 0"/> <!-- Populated in constructor -->
|
||||
<LineEdit Name="FilterText" PlaceHolder="{Loc 'criminal-records-filter-placeholder'}" HorizontalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<!-- Record listing -->
|
||||
<BoxContainer Orientation="Vertical" Margin="5" MinWidth="250" MaxWidth="250">
|
||||
<Label Name="RecordListingTitle" Text="{Loc 'criminal-records-console-records-list-title'}" HorizontalExpand="True" Align="Center"/>
|
||||
<Label Name="NoRecords" Text="{Loc 'criminal-records-console-no-records'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="RecordListing"/> <!-- Populated when loading state -->
|
||||
</ScrollContainer>
|
||||
<BoxContainer Name="AllList"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="8">
|
||||
<!-- Record search bar -->
|
||||
<BoxContainer Margin="5 5 5 10"
|
||||
HorizontalExpand="true"
|
||||
VerticalAlignment="Center">
|
||||
<OptionButton Name="FilterType"
|
||||
MinWidth="250"
|
||||
Margin="0 0 10 0" />
|
||||
<!-- Populated in constructor -->
|
||||
<LineEdit Name="FilterText"
|
||||
PlaceHolder="{Loc 'criminal-records-filter-placeholder'}"
|
||||
HorizontalExpand="True" />
|
||||
</BoxContainer>
|
||||
<Label Name="RecordUnselected" Text="{Loc 'criminal-records-console-select-record-info'}" HorizontalExpand="True" Align="Center" FontColorOverride="DarkGray"/>
|
||||
<!-- Selected record info -->
|
||||
<BoxContainer Name="PersonContainer" Orientation="Vertical" Margin="5" Visible="False">
|
||||
<Label Name="PersonName" StyleClasses="LabelBig"/>
|
||||
<Label Name="PersonPrints"/>
|
||||
<Label Name="PersonDna"/>
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 5 0 5" />
|
||||
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
|
||||
<Label Name="StatusLabel" Text="{Loc 'criminal-records-console-status'}" FontColorOverride="DarkGray"/>
|
||||
<OptionButton Name="StatusOptionButton"/> <!-- Populated in constructor -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True">
|
||||
<!-- Record listing -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
Margin="10 10"
|
||||
MinWidth="250"
|
||||
MaxWidth="250">
|
||||
<Label Name="RecordListingTitle"
|
||||
Text="{Loc 'criminal-records-console-records-list-title'}"
|
||||
HorizontalExpand="True"
|
||||
Align="Center" />
|
||||
<Label Name="NoRecords"
|
||||
Text="{Loc 'criminal-records-console-no-records'}"
|
||||
HorizontalExpand="True"
|
||||
Align="Center"
|
||||
FontColorOverride="DarkGray" />
|
||||
<ScrollContainer VerticalExpand="True">
|
||||
<ItemList Name="RecordListing" />
|
||||
<!-- Populated when loading state -->
|
||||
</ScrollContainer>
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="WantedReason" Visible="False"/>
|
||||
<Button Name="HistoryButton" Text="{Loc 'criminal-records-console-crime-history'}"/>
|
||||
<Label Name="RecordUnselected"
|
||||
Text="{Loc 'criminal-records-console-select-record-info'}"
|
||||
HorizontalExpand="True"
|
||||
Align="Center"
|
||||
FontColorOverride="DarkGray" />
|
||||
<!-- Selected record info -->
|
||||
<BoxContainer Name="PersonContainer"
|
||||
Orientation="Vertical"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True"
|
||||
Margin="5"
|
||||
Visible="False">
|
||||
<Label Name="PersonName"
|
||||
Margin="0 0 0 5"
|
||||
StyleClasses="LabelBig" />
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'crew-monitoring-user-interface-job'}:"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Text=":"
|
||||
FontColorOverride="DarkGray" />
|
||||
<TextureRect Name="PersonJobIcon"
|
||||
TextureScale="2 2"
|
||||
Margin="6 0"
|
||||
VerticalAlignment="Center" />
|
||||
<Label Name="PersonJob" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'general-station-record-prints-filter'}"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Text=":"
|
||||
Margin="0 0 6 0"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Name="PersonPrints" />
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 0 0 5">
|
||||
<Label Text="{Loc 'general-station-record-dna-filter'}"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Text=":"
|
||||
Margin="0 0 6 0"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Name="PersonDna" />
|
||||
</BoxContainer>
|
||||
<PanelContainer StyleClasses="LowDivider"
|
||||
Margin="0 5 0 5" />
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 5 0 5">
|
||||
<Label Name="StatusLabel"
|
||||
Text="{Loc 'criminal-records-console-status'}"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Text=":"
|
||||
FontColorOverride="DarkGray" />
|
||||
<Label Name="PersonStatus"
|
||||
FontColorOverride="DarkGray" />
|
||||
<AnimatedTextureRect Name="PersonStatusTX"
|
||||
Margin="8 0" />
|
||||
<OptionButton Name="StatusOptionButton"
|
||||
MinWidth="130" />
|
||||
<!-- Populated in constructor -->
|
||||
</BoxContainer>
|
||||
<RichTextLabel Name="WantedReason"
|
||||
Visible="False"
|
||||
MaxWidth="425" />
|
||||
<Button Name="HistoryButton"
|
||||
Text="{Loc 'criminal-records-console-crime-history'}"
|
||||
Margin="0 5" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="0 0 0 5">
|
||||
<OptionButton
|
||||
Name="CrewListFilter"
|
||||
MinWidth="250"
|
||||
Margin="10 0 10 0" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<PanelContainer StyleClasses="LowDivider" />
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="10 2 5 0"
|
||||
VerticalAlignment="Bottom">
|
||||
<Label Text="{Loc 'criminal-records-console-flavor-left'}"
|
||||
StyleClasses="WindowFooterText" />
|
||||
<Label Text="{Loc 'criminal-records-console-flavor-right'}"
|
||||
StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 5 0" />
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
SetSize="19 19" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
|
||||
@@ -13,6 +13,9 @@ using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Utility;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Client.GameObjects;
|
||||
|
||||
namespace Content.Client.CriminalRecords;
|
||||
|
||||
@@ -24,6 +27,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
private readonly IPrototypeManager _proto;
|
||||
private readonly IRobustRandom _random;
|
||||
private readonly AccessReaderSystem _accessReader;
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
public readonly EntityUid Console;
|
||||
|
||||
@@ -33,10 +38,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
public Action<uint?>? OnKeySelected;
|
||||
public Action<StationRecordFilterType, string>? OnFiltersChanged;
|
||||
public Action<SecurityStatus>? OnStatusSelected;
|
||||
public Action<uint>? OnCheckStatus;
|
||||
public Action<CriminalRecord, bool, bool>? OnHistoryUpdated;
|
||||
public Action? OnHistoryClosed;
|
||||
public Action<SecurityStatus, string>? OnDialogConfirmed;
|
||||
|
||||
public Action<SecurityStatus>? OnStatusFilterPressed;
|
||||
private uint _maxLength;
|
||||
private bool _access;
|
||||
private uint? _selectedKey;
|
||||
@@ -46,6 +53,8 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
private StationRecordFilterType _currentFilterType;
|
||||
|
||||
private SecurityStatus _currentCrewListFilter;
|
||||
|
||||
public CriminalRecordsConsoleWindow(EntityUid console, uint maxLength, IPlayerManager playerManager, IPrototypeManager prototypeManager, IRobustRandom robustRandom, AccessReaderSystem accessReader)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
@@ -55,10 +64,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
_proto = prototypeManager;
|
||||
_random = robustRandom;
|
||||
_accessReader = accessReader;
|
||||
IoCManager.InjectDependencies(this);
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
_maxLength = maxLength;
|
||||
_currentFilterType = StationRecordFilterType.Name;
|
||||
|
||||
_currentCrewListFilter = SecurityStatus.None;
|
||||
|
||||
OpenCentered();
|
||||
|
||||
foreach (var item in Enum.GetValues<StationRecordFilterType>())
|
||||
@@ -71,6 +84,12 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
AddStatusSelect(status);
|
||||
}
|
||||
|
||||
//Populate status to filter crew list
|
||||
foreach (var item in Enum.GetValues<SecurityStatus>())
|
||||
{
|
||||
CrewListFilter.AddItem(GetCrewListFilterLocals(item), (int)item);
|
||||
}
|
||||
|
||||
OnClose += () => _reasonDialog?.Close();
|
||||
|
||||
RecordListing.OnItemSelected += args =>
|
||||
@@ -97,6 +116,20 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
}
|
||||
};
|
||||
|
||||
//Select Status to filter crew
|
||||
CrewListFilter.OnItemSelected += eventArgs =>
|
||||
{
|
||||
var type = (SecurityStatus)eventArgs.Id;
|
||||
|
||||
if (_currentCrewListFilter != type)
|
||||
{
|
||||
_currentCrewListFilter = type;
|
||||
|
||||
StatusFilterPressed(type);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
FilterText.OnTextEntered += args =>
|
||||
{
|
||||
FilterListingOfRecords(args.Text);
|
||||
@@ -104,16 +137,21 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
StatusOptionButton.OnItemSelected += args =>
|
||||
{
|
||||
SetStatus((SecurityStatus) args.Id);
|
||||
SetStatus((SecurityStatus)args.Id);
|
||||
};
|
||||
|
||||
HistoryButton.OnPressed += _ =>
|
||||
{
|
||||
if (_selectedRecord is {} record)
|
||||
if (_selectedRecord is { } record)
|
||||
OnHistoryUpdated?.Invoke(record, _access, true);
|
||||
};
|
||||
}
|
||||
|
||||
public void StatusFilterPressed(SecurityStatus statusSelected)
|
||||
{
|
||||
OnStatusFilterPressed?.Invoke(statusSelected);
|
||||
}
|
||||
|
||||
public void UpdateState(CriminalRecordsConsoleState state)
|
||||
{
|
||||
if (state.Filter != null)
|
||||
@@ -129,10 +167,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
}
|
||||
}
|
||||
|
||||
if (state.FilterStatus != _currentCrewListFilter)
|
||||
{
|
||||
_currentCrewListFilter = state.FilterStatus;
|
||||
}
|
||||
|
||||
_selectedKey = state.SelectedKey;
|
||||
|
||||
FilterType.SelectId((int)_currentFilterType);
|
||||
|
||||
CrewListFilter.SelectId((int)_currentCrewListFilter);
|
||||
NoRecords.Visible = state.RecordListing == null || state.RecordListing.Count == 0;
|
||||
PopulateRecordListing(state.RecordListing);
|
||||
|
||||
@@ -179,7 +221,7 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
// in parallel to synchronize the items in RecordListing with `entries`.
|
||||
int i = RecordListing.Count - 1;
|
||||
int j = entries.Count - 1;
|
||||
while(i >= 0 && j >= 0)
|
||||
while (i >= 0 && j >= 0)
|
||||
{
|
||||
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
|
||||
if (strcmp == 0)
|
||||
@@ -212,23 +254,44 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
|
||||
while (j >= 0)
|
||||
{
|
||||
RecordListing.Insert(0, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
|
||||
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
|
||||
j--;
|
||||
}
|
||||
}
|
||||
|
||||
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
|
||||
{
|
||||
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");
|
||||
var na = Loc.GetString("generic-not-available-shorthand");
|
||||
PersonName.Text = stationRecord.Name;
|
||||
PersonPrints.Text = Loc.GetString("general-station-record-console-record-fingerprint", ("fingerprint", stationRecord.Fingerprint ?? na));
|
||||
PersonDna.Text = Loc.GetString("general-station-record-console-record-dna", ("dna", stationRecord.DNA ?? na));
|
||||
PersonJob.Text = stationRecord.JobTitle ?? na;
|
||||
|
||||
StatusOptionButton.SelectId((int) criminalRecord.Status);
|
||||
if (criminalRecord.Reason is {} reason)
|
||||
// Job icon
|
||||
if (_proto.TryIndex<JobIconPrototype>(stationRecord.JobIcon, out var proto))
|
||||
{
|
||||
PersonJobIcon.Texture = _spriteSystem.Frame0(proto.Icon);
|
||||
}
|
||||
|
||||
PersonPrints.Text = stationRecord.Fingerprint ?? Loc.GetString("generic-not-available-shorthand");
|
||||
PersonDna.Text = stationRecord.DNA ?? Loc.GetString("generic-not-available-shorthand");
|
||||
|
||||
if (criminalRecord.Status != SecurityStatus.None)
|
||||
{
|
||||
specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/security_icons.rsi"), GetStatusIcon(criminalRecord.Status));
|
||||
}
|
||||
PersonStatusTX.SetFromSpriteSpecifier(specifier);
|
||||
PersonStatusTX.DisplayRect.TextureScale = new Vector2(3f, 3f);
|
||||
|
||||
StatusOptionButton.SelectId((int)criminalRecord.Status);
|
||||
if (criminalRecord.Reason is { } reason)
|
||||
{
|
||||
var message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-wanted-reason"));
|
||||
|
||||
if (criminalRecord.Status == SecurityStatus.Suspected)
|
||||
{
|
||||
message = FormattedMessage.FromMarkupOrThrow(Loc.GetString("criminal-records-console-suspected-reason"));
|
||||
}
|
||||
message.AddText($": {reason}");
|
||||
|
||||
WantedReason.SetMessage(message);
|
||||
WantedReason.Visible = true;
|
||||
}
|
||||
@@ -288,9 +351,37 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
|
||||
|
||||
_reasonDialog.OnClose += () => { _reasonDialog = null; };
|
||||
}
|
||||
|
||||
private string GetStatusIcon(SecurityStatus status)
|
||||
{
|
||||
return status switch
|
||||
{
|
||||
SecurityStatus.Paroled => "hud_paroled",
|
||||
SecurityStatus.Wanted => "hud_wanted",
|
||||
SecurityStatus.Detained => "hud_incarcerated",
|
||||
SecurityStatus.Discharged => "hud_discharged",
|
||||
SecurityStatus.Suspected => "hud_suspected",
|
||||
_ => "SecurityIconNone"
|
||||
};
|
||||
}
|
||||
private string GetTypeFilterLocals(StationRecordFilterType type)
|
||||
{
|
||||
return Loc.GetString($"criminal-records-{type.ToString().ToLower()}-filter");
|
||||
}
|
||||
|
||||
private string GetCrewListFilterLocals(SecurityStatus type)
|
||||
{
|
||||
string result;
|
||||
|
||||
// If "NONE" override to "show all"
|
||||
if (type == SecurityStatus.None)
|
||||
{
|
||||
result = Loc.GetString("criminal-records-console-show-all");
|
||||
}
|
||||
else
|
||||
{
|
||||
result = Loc.GetString($"criminal-records-status-{type.ToString().ToLower()}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,112 +21,131 @@ public sealed class DoorSystem : SharedDoorSystem
|
||||
protected override void OnComponentInit(Entity<DoorComponent> ent, ref ComponentInit args)
|
||||
{
|
||||
var comp = ent.Comp;
|
||||
comp.OpenSpriteStates = new(2);
|
||||
comp.ClosedSpriteStates = new(2);
|
||||
comp.OpenSpriteStates = new List<(DoorVisualLayers, string)>(2);
|
||||
comp.ClosedSpriteStates = new List<(DoorVisualLayers, string)>(2);
|
||||
|
||||
comp.OpenSpriteStates.Add((DoorVisualLayers.Base, comp.OpenSpriteState));
|
||||
comp.ClosedSpriteStates.Add((DoorVisualLayers.Base, comp.ClosedSpriteState));
|
||||
|
||||
comp.OpeningAnimation = new Animation()
|
||||
comp.OpeningAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.OpeningAnimationTime),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick()
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = DoorVisualLayers.Base,
|
||||
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f) }
|
||||
}
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(comp.OpeningSpriteState, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
comp.ClosingAnimation = new Animation()
|
||||
comp.ClosingAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.ClosingAnimationTime),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick()
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = DoorVisualLayers.Base,
|
||||
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f) }
|
||||
}
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(comp.ClosingSpriteState, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
comp.EmaggingAnimation = new Animation ()
|
||||
comp.EmaggingAnimation = new Animation
|
||||
{
|
||||
Length = TimeSpan.FromSeconds(comp.EmaggingAnimationTime),
|
||||
AnimationTracks =
|
||||
{
|
||||
new AnimationTrackSpriteFlick()
|
||||
new AnimationTrackSpriteFlick
|
||||
{
|
||||
LayerKey = DoorVisualLayers.BaseUnlit,
|
||||
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f) }
|
||||
}
|
||||
KeyFrames =
|
||||
{
|
||||
new AnimationTrackSpriteFlick.KeyFrame(comp.EmaggingSpriteState, 0f),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private void OnAppearanceChange(EntityUid uid, DoorComponent comp, ref AppearanceChangeEvent args)
|
||||
private void OnAppearanceChange(Entity<DoorComponent> entity, ref AppearanceChangeEvent args)
|
||||
{
|
||||
if (args.Sprite == null)
|
||||
return;
|
||||
|
||||
if(!AppearanceSystem.TryGetData<DoorState>(uid, DoorVisuals.State, out var state, args.Component))
|
||||
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
|
||||
state = DoorState.Closed;
|
||||
|
||||
if (AppearanceSystem.TryGetData<string>(uid, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
|
||||
{
|
||||
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
|
||||
}
|
||||
foreach (var layer in args.Sprite.AllLayers)
|
||||
{
|
||||
layer.Rsi = res?.RSI;
|
||||
}
|
||||
}
|
||||
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
|
||||
UpdateSpriteLayers(args.Sprite, baseRsi);
|
||||
|
||||
TryComp<AnimationPlayerComponent>(uid, out var animPlayer);
|
||||
if (_animationSystem.HasRunningAnimation(uid, animPlayer, DoorComponent.AnimationKey))
|
||||
_animationSystem.Stop(uid, animPlayer, DoorComponent.AnimationKey); // Halt all running anomations.
|
||||
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
|
||||
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
|
||||
|
||||
args.Sprite.DrawDepth = comp.ClosedDrawDepth;
|
||||
switch(state)
|
||||
UpdateAppearanceForDoorState(entity, args.Sprite, state);
|
||||
}
|
||||
|
||||
private void UpdateAppearanceForDoorState(Entity<DoorComponent> entity, SpriteComponent sprite, DoorState state)
|
||||
{
|
||||
sprite.DrawDepth = state is DoorState.Open ? entity.Comp.OpenDrawDepth : entity.Comp.ClosedDrawDepth;
|
||||
|
||||
switch (state)
|
||||
{
|
||||
case DoorState.Open:
|
||||
args.Sprite.DrawDepth = comp.OpenDrawDepth;
|
||||
foreach(var (layer, layerState) in comp.OpenSpriteStates)
|
||||
foreach (var (layer, layerState) in entity.Comp.OpenSpriteStates)
|
||||
{
|
||||
args.Sprite.LayerSetState(layer, layerState);
|
||||
sprite.LayerSetState(layer, layerState);
|
||||
}
|
||||
break;
|
||||
|
||||
return;
|
||||
case DoorState.Closed:
|
||||
foreach(var (layer, layerState) in comp.ClosedSpriteStates)
|
||||
foreach (var (layer, layerState) in entity.Comp.ClosedSpriteStates)
|
||||
{
|
||||
args.Sprite.LayerSetState(layer, layerState);
|
||||
sprite.LayerSetState(layer, layerState);
|
||||
}
|
||||
break;
|
||||
|
||||
return;
|
||||
case DoorState.Opening:
|
||||
if (animPlayer != null && comp.OpeningAnimationTime != 0.0)
|
||||
_animationSystem.Play((uid, animPlayer), (Animation)comp.OpeningAnimation, DoorComponent.AnimationKey);
|
||||
break;
|
||||
if (entity.Comp.OpeningAnimationTime == 0.0)
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.OpeningAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
return;
|
||||
case DoorState.Closing:
|
||||
if (animPlayer != null && comp.ClosingAnimationTime != 0.0 && comp.CurrentlyCrushing.Count == 0)
|
||||
_animationSystem.Play((uid, animPlayer), (Animation)comp.ClosingAnimation, DoorComponent.AnimationKey);
|
||||
break;
|
||||
if (entity.Comp.ClosingAnimationTime == 0.0 || entity.Comp.CurrentlyCrushing.Count != 0)
|
||||
return;
|
||||
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.ClosingAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
return;
|
||||
case DoorState.Denying:
|
||||
if (animPlayer != null)
|
||||
_animationSystem.Play((uid, animPlayer), (Animation)comp.DenyingAnimation, DoorComponent.AnimationKey);
|
||||
break;
|
||||
case DoorState.Welded:
|
||||
break;
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.DenyingAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
return;
|
||||
case DoorState.Emagging:
|
||||
if (animPlayer != null)
|
||||
_animationSystem.Play((uid, animPlayer), (Animation)comp.EmaggingAnimation, DoorComponent.AnimationKey);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException($"Invalid door visual state {state}");
|
||||
_animationSystem.Play(entity, (Animation)entity.Comp.EmaggingAnimation, DoorComponent.AnimationKey);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSpriteLayers(SpriteComponent sprite, string baseRsi)
|
||||
{
|
||||
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
|
||||
{
|
||||
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
|
||||
return;
|
||||
}
|
||||
|
||||
sprite.BaseRSI = res.RSI;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
|
||||
|
||||
var button = new GhostRoleRadioMenuButton()
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64, 64),
|
||||
ToolTip = Loc.GetString(ghostRoleProto.Name),
|
||||
ProtoId = ghostRoleProto.ID,
|
||||
@@ -100,7 +99,7 @@ public sealed partial class GhostRoleRadioMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButton
|
||||
public sealed class GhostRoleRadioMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<GhostRolePrototype> ProtoId { get; set; }
|
||||
}
|
||||
|
||||
@@ -2,45 +2,38 @@ using Content.Shared.Chat.TypingIndicator;
|
||||
using Content.Shared.Holopad;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
using System.Linq;
|
||||
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
|
||||
|
||||
namespace Content.Client.Holopad;
|
||||
|
||||
public sealed class HolopadSystem : SharedHolopadSystem
|
||||
{
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _playerManager = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<HolopadHologramComponent, ComponentInit>(OnComponentInit);
|
||||
SubscribeLocalEvent<HolopadHologramComponent, ComponentStartup>(OnComponentStartup);
|
||||
SubscribeLocalEvent<HolopadHologramComponent, BeforePostShaderRenderEvent>(OnShaderRender);
|
||||
SubscribeAllEvent<TypingChangedEvent>(OnTypingChanged);
|
||||
|
||||
SubscribeNetworkEvent<PlayerSpriteStateRequest>(OnPlayerSpriteStateRequest);
|
||||
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||
}
|
||||
|
||||
private void OnComponentInit(EntityUid uid, HolopadHologramComponent component, ComponentInit ev)
|
||||
private void OnComponentStartup(Entity<HolopadHologramComponent> entity, ref ComponentStartup ev)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var sprite))
|
||||
return;
|
||||
|
||||
UpdateHologramSprite(uid);
|
||||
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
|
||||
}
|
||||
|
||||
private void OnShaderRender(EntityUid uid, HolopadHologramComponent component, BeforePostShaderRenderEvent ev)
|
||||
private void OnShaderRender(Entity<HolopadHologramComponent> entity, ref BeforePostShaderRenderEvent ev)
|
||||
{
|
||||
if (ev.Sprite.PostShader == null)
|
||||
return;
|
||||
|
||||
ev.Sprite.PostShader.SetParameter("t", (float)_timing.CurTime.TotalSeconds * component.ScrollRate);
|
||||
UpdateHologramSprite(entity, entity.Comp.LinkedEntity);
|
||||
}
|
||||
|
||||
private void OnTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
|
||||
@@ -57,100 +50,66 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
RaiseNetworkEvent(netEv);
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateRequest(PlayerSpriteStateRequest ev)
|
||||
private void UpdateHologramSprite(EntityUid hologram, EntityUid? target)
|
||||
{
|
||||
var targetPlayer = GetEntity(ev.TargetPlayer);
|
||||
var player = _playerManager.LocalSession?.AttachedEntity;
|
||||
|
||||
// Ignore the request if received by a player who isn't the target
|
||||
if (targetPlayer != player)
|
||||
return;
|
||||
|
||||
if (!TryComp<SpriteComponent>(player, out var playerSprite))
|
||||
return;
|
||||
|
||||
var spriteLayerData = new List<PrototypeLayerData>();
|
||||
|
||||
if (playerSprite.Visible)
|
||||
{
|
||||
// Record the RSI paths, state names and shader paramaters of all visible layers
|
||||
for (int i = 0; i < playerSprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (!playerSprite.TryGetLayer(i, out var layer))
|
||||
continue;
|
||||
|
||||
if (!layer.Visible ||
|
||||
string.IsNullOrEmpty(layer.ActualRsi?.Path.ToString()) ||
|
||||
string.IsNullOrEmpty(layer.State.Name))
|
||||
continue;
|
||||
|
||||
var layerDatum = new PrototypeLayerData();
|
||||
layerDatum.RsiPath = layer.ActualRsi.Path.ToString();
|
||||
layerDatum.State = layer.State.Name;
|
||||
|
||||
if (layer.CopyToShaderParameters != null)
|
||||
{
|
||||
var key = (string)layer.CopyToShaderParameters.LayerKey;
|
||||
|
||||
if (playerSprite.LayerMapTryGet(key, out var otherLayerIdx) &&
|
||||
playerSprite.TryGetLayer(otherLayerIdx, out var otherLayer) &&
|
||||
otherLayer.Visible)
|
||||
{
|
||||
layerDatum.MapKeys = new() { key };
|
||||
|
||||
layerDatum.CopyToShaderParameters = new PrototypeCopyToShaderParameters()
|
||||
{
|
||||
LayerKey = key,
|
||||
ParameterTexture = layer.CopyToShaderParameters.ParameterTexture,
|
||||
ParameterUV = layer.CopyToShaderParameters.ParameterUV
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
spriteLayerData.Add(layerDatum);
|
||||
}
|
||||
}
|
||||
|
||||
// Return the recorded data to the server
|
||||
var evResponse = new PlayerSpriteStateMessage(ev.TargetPlayer, spriteLayerData.ToArray());
|
||||
RaiseNetworkEvent(evResponse);
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev)
|
||||
{
|
||||
UpdateHologramSprite(GetEntity(ev.SpriteEntity), ev.SpriteLayerData);
|
||||
}
|
||||
|
||||
private void UpdateHologramSprite(EntityUid uid, PrototypeLayerData[]? layerData = null)
|
||||
{
|
||||
if (!TryComp<SpriteComponent>(uid, out var hologramSprite))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadHologramComponent>(uid, out var holopadhologram))
|
||||
// Get required components
|
||||
if (!TryComp<SpriteComponent>(hologram, out var hologramSprite) ||
|
||||
!TryComp<HolopadHologramComponent>(hologram, out var holopadhologram))
|
||||
return;
|
||||
|
||||
// Remove all sprite layers
|
||||
for (int i = hologramSprite.AllLayers.Count() - 1; i >= 0; i--)
|
||||
hologramSprite.RemoveLayer(i);
|
||||
|
||||
if (layerData == null || layerData.Length == 0)
|
||||
if (TryComp<SpriteComponent>(target, out var targetSprite))
|
||||
{
|
||||
layerData = new PrototypeLayerData[1];
|
||||
layerData[0] = new PrototypeLayerData()
|
||||
// Use the target's holographic avatar (if available)
|
||||
if (TryComp<HolographicAvatarComponent>(target, out var targetAvatar) &&
|
||||
targetAvatar.LayerData != null)
|
||||
{
|
||||
RsiPath = holopadhologram.RsiPath,
|
||||
State = holopadhologram.RsiState
|
||||
};
|
||||
for (int i = 0; i < targetAvatar.LayerData.Length; i++)
|
||||
{
|
||||
var layer = targetAvatar.LayerData[i];
|
||||
hologramSprite.AddLayer(targetAvatar.LayerData[i], i);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise copy the target's current physical appearance
|
||||
else
|
||||
{
|
||||
hologramSprite.CopyFrom(targetSprite);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < layerData.Length; i++)
|
||||
// There is no target, display a default sprite instead (if available)
|
||||
else
|
||||
{
|
||||
var layer = layerData[i];
|
||||
layer.Shader = "unshaded";
|
||||
if (string.IsNullOrEmpty(holopadhologram.RsiPath) || string.IsNullOrEmpty(holopadhologram.RsiState))
|
||||
return;
|
||||
|
||||
hologramSprite.AddLayer(layerData[i], i);
|
||||
var layer = new PrototypeLayerData();
|
||||
layer.RsiPath = holopadhologram.RsiPath;
|
||||
layer.State = holopadhologram.RsiState;
|
||||
|
||||
hologramSprite.AddLayer(layer);
|
||||
}
|
||||
|
||||
UpdateHologramShader(uid, hologramSprite, holopadhologram);
|
||||
// Override specific values
|
||||
hologramSprite.Color = Color.White;
|
||||
hologramSprite.Offset = holopadhologram.Offset;
|
||||
hologramSprite.DrawDepth = (int)DrawDepth.Mobs;
|
||||
hologramSprite.NoRotation = true;
|
||||
hologramSprite.DirectionOverride = Direction.South;
|
||||
hologramSprite.EnableDirectionOverride = true;
|
||||
|
||||
// Remove shading from all layers (except displacement maps)
|
||||
for (int i = 0; i < hologramSprite.AllLayers.Count(); i++)
|
||||
{
|
||||
if (hologramSprite.TryGetLayer(i, out var layer) && layer.ShaderPrototype != "DisplacedStencilDraw")
|
||||
hologramSprite.LayerSetShader(i, "unshaded");
|
||||
}
|
||||
|
||||
UpdateHologramShader(hologram, hologramSprite, holopadhologram);
|
||||
}
|
||||
|
||||
private void UpdateHologramShader(EntityUid uid, SpriteComponent sprite, HolopadHologramComponent holopadHologram)
|
||||
|
||||
@@ -170,7 +170,7 @@ namespace Content.Client.LateJoin
|
||||
|
||||
foreach (var department in departments)
|
||||
{
|
||||
var departmentName = Loc.GetString($"department-{department.ID}");
|
||||
var departmentName = Loc.GetString(department.Name);
|
||||
_jobCategories[id] = new Dictionary<string, BoxContainer>();
|
||||
var stationAvailable = _gameTicker.JobsAvailable[id];
|
||||
var jobsAvailable = new List<JobPrototype>();
|
||||
|
||||
@@ -840,7 +840,7 @@ namespace Content.Client.Lobby.UI
|
||||
|
||||
foreach (var department in departments)
|
||||
{
|
||||
var departmentName = Loc.GetString($"department-{department.ID}");
|
||||
var departmentName = Loc.GetString(department.Name);
|
||||
|
||||
if (!_jobCategories.TryGetValue(department.ID, out var category))
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
{
|
||||
[Dependency] private readonly IEntityManager _entManager = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
private readonly SharedTransformSystem _transformSystem;
|
||||
private readonly SpriteSystem _spriteSystem;
|
||||
|
||||
private NetEntity? _trackedEntity;
|
||||
@@ -36,10 +37,10 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
RobustXamlLoader.Load(this);
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
_transformSystem = _entManager.System<SharedTransformSystem>();
|
||||
_spriteSystem = _entManager.System<SpriteSystem>();
|
||||
|
||||
NavMap.TrackedEntitySelectedAction += SetTrackedEntityFromNavMap;
|
||||
|
||||
}
|
||||
|
||||
public void Set(string stationName, EntityUid? mapUid)
|
||||
@@ -290,7 +291,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
{
|
||||
NavMap.TrackedEntities.TryAdd(sensor.SuitSensorUid,
|
||||
new NavMapBlip
|
||||
(coordinates.Value,
|
||||
(CoordinatesToLocal(coordinates.Value),
|
||||
_blipTexture,
|
||||
(_trackedEntity == null || sensor.SuitSensorUid == _trackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
|
||||
sensor.SuitSensorUid == _trackedEntity));
|
||||
@@ -356,7 +357,7 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
if (NavMap.TrackedEntities.TryGetValue(castSensor.SuitSensorUid, out var data))
|
||||
{
|
||||
data = new NavMapBlip
|
||||
(data.Coordinates,
|
||||
(CoordinatesToLocal(data.Coordinates),
|
||||
data.Texture,
|
||||
(currTrackedEntity == null || castSensor.SuitSensorUid == currTrackedEntity) ? Color.LimeGreen : Color.LimeGreen * Color.DimGray,
|
||||
castSensor.SuitSensorUid == currTrackedEntity);
|
||||
@@ -421,6 +422,26 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the input coordinates to an EntityCoordinates which are in
|
||||
/// reference to the grid that the map is displaying. This is a stylistic
|
||||
/// choice; this window deliberately limits the rate that blips update,
|
||||
/// but if the blip is attached to another grid which is moving, that
|
||||
/// blip will move smoothly, unlike the others. By converting the
|
||||
/// coordinates, we are back in control of the blip movement.
|
||||
/// </summary>
|
||||
private EntityCoordinates CoordinatesToLocal(EntityCoordinates refCoords)
|
||||
{
|
||||
if (NavMap.MapUid != null)
|
||||
{
|
||||
return _transformSystem.WithEntityId(refCoords, (EntityUid)NavMap.MapUid);
|
||||
}
|
||||
else
|
||||
{
|
||||
return refCoords;
|
||||
}
|
||||
}
|
||||
|
||||
private void ClearOutDatedData()
|
||||
{
|
||||
SensorsTable.RemoveAllChildren();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Movement.Systems;
|
||||
using Content.Shared.Movement.Components;
|
||||
|
||||
namespace Content.Client.Movement.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
|
||||
{
|
||||
/// <summary>
|
||||
/// The location the offset will attempt to pan towards; based on the cursor's position in the game window.
|
||||
/// </summary>
|
||||
public Vector2 TargetPosition = Vector2.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// The current positional offset being applied. Used to enable gradual panning.
|
||||
/// </summary>
|
||||
public Vector2 CurrentPosition = Vector2.Zero;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.GameObjects;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Movement.Systems;
|
||||
@@ -52,4 +53,14 @@ public sealed class ContentEyeSystem : SharedContentEyeSystem
|
||||
{
|
||||
RaisePredictiveEvent(new RequestEyeEvent(drawFov, drawLight));
|
||||
}
|
||||
|
||||
public override void FrameUpdate(float frameTime)
|
||||
{
|
||||
base.FrameUpdate(frameTime);
|
||||
var eyeEntities = AllEntityQuery<ContentEyeComponent, EyeComponent>();
|
||||
while (eyeEntities.MoveNext(out var entity, out ContentEyeComponent? contentComponent, out EyeComponent? eyeComponent))
|
||||
{
|
||||
UpdateEyeOffset((entity, eyeComponent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
Normal file
91
Content.Client/Movement/Systems/EyeCursorOffsetSystem.cs
Normal file
@@ -0,0 +1,91 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Movement.Components;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Inventory;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Input;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Client.Player;
|
||||
|
||||
namespace Content.Client.Movement.Systems;
|
||||
|
||||
public partial class EyeCursorOffsetSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IEyeManager _eyeManager = default!;
|
||||
[Dependency] private readonly IInputManager _inputManager = default!;
|
||||
[Dependency] private readonly IPlayerManager _player = default!;
|
||||
[Dependency] private readonly SharedTransformSystem _transform = default!;
|
||||
[Dependency] private readonly SharedContentEyeSystem _contentEye = default!;
|
||||
[Dependency] private readonly IMapManager _mapManager = default!;
|
||||
[Dependency] private readonly IClyde _clyde = default!;
|
||||
|
||||
// This value is here to make sure the user doesn't have to move their mouse
|
||||
// all the way out to the edge of the screen to get the full offset.
|
||||
static private float _edgeOffset = 0.9f;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<EyeCursorOffsetComponent, GetEyeOffsetEvent>(OnGetEyeOffsetEvent);
|
||||
}
|
||||
|
||||
private void OnGetEyeOffsetEvent(EntityUid uid, EyeCursorOffsetComponent component, ref GetEyeOffsetEvent args)
|
||||
{
|
||||
var offset = OffsetAfterMouse(uid, component);
|
||||
if (offset == null)
|
||||
return;
|
||||
|
||||
args.Offset += offset.Value;
|
||||
}
|
||||
|
||||
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
|
||||
{
|
||||
var localPlayer = _player.LocalPlayer?.ControlledEntity;
|
||||
var mousePos = _inputManager.MouseScreenPosition;
|
||||
var screenSize = _clyde.MainWindow.Size;
|
||||
var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;
|
||||
|
||||
var mouseNormalizedPos = new Vector2(-(mousePos.X - screenSize.X / 2) / minValue, (mousePos.Y - screenSize.Y / 2) / minValue); // X needs to be inverted here for some reason, otherwise it ends up flipped.
|
||||
|
||||
if (localPlayer == null)
|
||||
return null;
|
||||
|
||||
var playerPos = _transform.GetWorldPosition(localPlayer.Value);
|
||||
|
||||
if (component == null)
|
||||
{
|
||||
component = EnsureComp<EyeCursorOffsetComponent>(uid);
|
||||
}
|
||||
|
||||
// Doesn't move the offset if the mouse has left the game window!
|
||||
if (mousePos.Window != WindowId.Invalid)
|
||||
{
|
||||
// The offset must account for the in-world rotation.
|
||||
var eyeRotation = _eyeManager.CurrentEye.Rotation;
|
||||
var mouseActualRelativePos = Vector2.Transform(mouseNormalizedPos, System.Numerics.Quaternion.CreateFromAxisAngle(-System.Numerics.Vector3.UnitZ, (float)(eyeRotation.Opposite().Theta))); // I don't know, it just works.
|
||||
|
||||
// Caps the offset into a circle around the player.
|
||||
mouseActualRelativePos *= component.MaxOffset;
|
||||
if (mouseActualRelativePos.Length() > component.MaxOffset)
|
||||
{
|
||||
mouseActualRelativePos = mouseActualRelativePos.Normalized() * component.MaxOffset;
|
||||
}
|
||||
|
||||
component.TargetPosition = mouseActualRelativePos;
|
||||
|
||||
//Makes the view not jump immediately when moving the cursor fast.
|
||||
if (component.CurrentPosition != component.TargetPosition)
|
||||
{
|
||||
Vector2 vectorOffset = component.TargetPosition - component.CurrentPosition;
|
||||
if (vectorOffset.Length() > component.OffsetSpeed)
|
||||
{
|
||||
vectorOffset = vectorOffset.Normalized() * component.OffsetSpeed;
|
||||
}
|
||||
component.CurrentPosition += vectorOffset;
|
||||
}
|
||||
}
|
||||
return component.CurrentPosition;
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
<CheckBox Name="RestartSoundsCheckBox" Text="{Loc 'ui-options-restart-sounds'}" />
|
||||
<CheckBox Name="EventMusicCheckBox" Text="{Loc 'ui-options-event-music'}" />
|
||||
<CheckBox Name="AdminSoundsCheckBox" Text="{Loc 'ui-options-admin-sounds'}" />
|
||||
<CheckBox Name="BwoinkSoundCheckBox" Text="{Loc 'ui-options-bwoink-sound'}" />
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<ui:OptionsTabControlRow Name="Control" Access="Public" />
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Client.Administration.Managers;
|
||||
using Content.Client.Audio;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Client.Audio;
|
||||
@@ -12,8 +13,9 @@ namespace Content.Client.Options.UI.Tabs;
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class AudioTab : Control
|
||||
{
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
[Dependency] private readonly IAudioManager _audio = default!;
|
||||
[Dependency] private readonly IClientAdminManager _admin = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
public AudioTab()
|
||||
{
|
||||
@@ -61,10 +63,30 @@ public sealed partial class AudioTab : Control
|
||||
Control.AddOptionCheckBox(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
|
||||
Control.AddOptionCheckBox(CCVars.BwoinkSoundEnabled, BwoinkSoundCheckBox);
|
||||
|
||||
Control.Initialize();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
{
|
||||
base.EnteredTree();
|
||||
_admin.AdminStatusUpdated += UpdateAdminButtonsVisibility;
|
||||
UpdateAdminButtonsVisibility();
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
{
|
||||
base.ExitedTree();
|
||||
_admin.AdminStatusUpdated -= UpdateAdminButtonsVisibility;
|
||||
}
|
||||
|
||||
|
||||
private void UpdateAdminButtonsVisibility()
|
||||
{
|
||||
BwoinkSoundCheckBox.Visible = _admin.IsActive();
|
||||
}
|
||||
|
||||
private void OnMasterVolumeSliderChanged(float value)
|
||||
{
|
||||
// TODO: I was thinking of giving OptionsTabControlRow a flag to "set CVar immediately", but I'm deferring that
|
||||
|
||||
@@ -56,35 +56,35 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
|
||||
|
||||
protected virtual void DeactivateInternal() { }
|
||||
|
||||
private void OnStartup(EntityUid uid, T component, ComponentStartup args)
|
||||
private void OnStartup(Entity<T> ent, ref ComponentStartup args)
|
||||
{
|
||||
RefreshOverlay(uid);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnRemove(EntityUid uid, T component, ComponentRemove args)
|
||||
private void OnRemove(Entity<T> ent, ref ComponentRemove args)
|
||||
{
|
||||
RefreshOverlay(uid);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnPlayerAttached(LocalPlayerAttachedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Entity);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnPlayerDetached(LocalPlayerDetachedEvent args)
|
||||
{
|
||||
if (_player.LocalSession?.AttachedEntity == null)
|
||||
if (_player.LocalSession?.AttachedEntity is null)
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
private void OnCompEquip(EntityUid uid, T component, GotEquippedEvent args)
|
||||
private void OnCompEquip(Entity<T> ent, ref GotEquippedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Equipee);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnCompUnequip(EntityUid uid, T component, GotUnequippedEvent args)
|
||||
private void OnCompUnequip(Entity<T> ent, ref GotUnequippedEvent args)
|
||||
{
|
||||
RefreshOverlay(args.Equipee);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnRoundRestart(RoundRestartCleanupEvent args)
|
||||
@@ -92,24 +92,24 @@ public abstract class EquipmentHudSystem<T> : EntitySystem where T : IComponent
|
||||
Deactivate();
|
||||
}
|
||||
|
||||
protected virtual void OnRefreshEquipmentHud(EntityUid uid, T component, InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
|
||||
protected virtual void OnRefreshEquipmentHud(Entity<T> ent, ref InventoryRelayedEvent<RefreshEquipmentHudEvent<T>> args)
|
||||
{
|
||||
OnRefreshComponentHud(uid, component, args.Args);
|
||||
OnRefreshComponentHud(ent, ref args.Args);
|
||||
}
|
||||
|
||||
protected virtual void OnRefreshComponentHud(EntityUid uid, T component, RefreshEquipmentHudEvent<T> args)
|
||||
protected virtual void OnRefreshComponentHud(Entity<T> ent, ref RefreshEquipmentHudEvent<T> args)
|
||||
{
|
||||
args.Active = true;
|
||||
args.Components.Add(component);
|
||||
args.Components.Add(ent.Comp);
|
||||
}
|
||||
|
||||
protected void RefreshOverlay(EntityUid uid)
|
||||
protected void RefreshOverlay()
|
||||
{
|
||||
if (uid != _player.LocalSession?.AttachedEntity)
|
||||
if (_player.LocalSession?.AttachedEntity is not { } entity)
|
||||
return;
|
||||
|
||||
var ev = new RefreshEquipmentHudEvent<T>(TargetSlots);
|
||||
RaiseLocalEvent(uid, ev);
|
||||
RaiseLocalEvent(entity, ref ev);
|
||||
|
||||
if (ev.Active)
|
||||
Update(ev);
|
||||
|
||||
@@ -28,7 +28,7 @@ public sealed class ShowHealthBarsSystem : EquipmentHudSystem<ShowHealthBarsComp
|
||||
|
||||
private void OnHandleState(Entity<ShowHealthBarsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
RefreshOverlay(ent);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
protected override void UpdateInternal(RefreshEquipmentHudEvent<ShowHealthBarsComponent> component)
|
||||
|
||||
@@ -47,7 +47,7 @@ public sealed class ShowHealthIconsSystem : EquipmentHudSystem<ShowHealthIconsCo
|
||||
|
||||
private void OnHandleState(Entity<ShowHealthIconsComponent> ent, ref AfterAutoHandleStateEvent args)
|
||||
{
|
||||
RefreshOverlay(ent);
|
||||
RefreshOverlay();
|
||||
}
|
||||
|
||||
private void OnGetStatusIconsEvent(Entity<DamageableComponent> entity, ref GetStatusIconsEvent args)
|
||||
|
||||
@@ -15,6 +15,16 @@ public sealed class ShowMindShieldIconsSystem : EquipmentHudSystem<ShowMindShiel
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<MindShieldComponent, GetStatusIconsEvent>(OnGetStatusIconsEvent);
|
||||
SubscribeLocalEvent<FakeMindShieldComponent, GetStatusIconsEvent>(OnGetStatusIconsEventFake);
|
||||
}
|
||||
// TODO: Probably need to get this OFF of client since this can be read by bad actors rather easily
|
||||
// ...imagine cheating in a game about silly paper dolls
|
||||
private void OnGetStatusIconsEventFake(EntityUid uid, FakeMindShieldComponent component, ref GetStatusIconsEvent ev)
|
||||
{
|
||||
if(!IsActive)
|
||||
return;
|
||||
if (component.IsEnabled && _prototype.TryIndex(component.MindShieldStatusIcon, out var fakeStatusIconPrototype))
|
||||
ev.StatusIcons.Add(fakeStatusIconPrototype);
|
||||
}
|
||||
|
||||
private void OnGetStatusIconsEvent(EntityUid uid, MindShieldComponent component, ref GetStatusIconsEvent ev)
|
||||
|
||||
@@ -1,50 +1,129 @@
|
||||
<controls:FancyWindow xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.ParticleAccelerator.UI"
|
||||
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
|
||||
Title="{Loc 'particle-accelerator-control-menu-device-version-label'}"
|
||||
MinSize="420 320"
|
||||
SetSize="420 320">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" Margin="0 10 0 0">
|
||||
<BoxContainer Orientation="Horizontal" VerticalExpand="True">
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True" Margin="10 0 10 5">
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="StatusLabel" HorizontalExpand="True"/>
|
||||
<RichTextLabel Name="StatusStateLabel"/>
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
xmlns:ui="clr-namespace:Content.Client.ParticleAccelerator.UI"
|
||||
Title="{Loc 'particle-accelerator-control-menu-device-version-label'}"
|
||||
MinSize="320 120">
|
||||
|
||||
<!-- Main Container -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalExpand="True">
|
||||
|
||||
<!-- Sub-Main container -->
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
VerticalExpand="True"
|
||||
HorizontalExpand="True">
|
||||
|
||||
<!-- Info part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
HorizontalExpand="True"
|
||||
Margin="8">
|
||||
|
||||
<!-- Info -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="4">
|
||||
|
||||
<!-- Status -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="StatusLabel" HorizontalExpand="True"/>
|
||||
<Control MinWidth="8"/>
|
||||
<RichTextLabel Name="StatusStateLabel"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Power -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="PowerLabel"
|
||||
HorizontalExpand="True"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Control MinWidth="8"/>
|
||||
|
||||
<Button Name="OffButton"
|
||||
ToggleMode="False"
|
||||
Text="{Loc 'particle-accelerator-control-menu-off-button'}"
|
||||
StyleClasses="OpenRight"/>
|
||||
|
||||
<Button Name="OnButton"
|
||||
ToggleMode="False"
|
||||
Text="{Loc 'particle-accelerator-control-menu-on-button'}"
|
||||
StyleClasses="OpenLeft"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Strenght -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="StrengthLabel"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<Control MinWidth="8"/>
|
||||
|
||||
<SpinBox Name="StateSpinBox" Value="0"/>
|
||||
</BoxContainer>
|
||||
|
||||
<!-- Power -->
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="DrawLabel" HorizontalExpand="True"/>
|
||||
<Control MinWidth="8"/>
|
||||
<RichTextLabel Name="DrawValueLabel"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="PowerLabel" Margin="0 0 20 0" HorizontalExpand="True" VerticalAlignment="Center"/>
|
||||
<Button Name="OffButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-off-button'}" StyleClasses="OpenRight"/>
|
||||
<Button Name="OnButton" ToggleMode="False" Text="{Loc 'particle-accelerator-control-menu-on-button'}" StyleClasses="OpenLeft"/>
|
||||
|
||||
<Control MinHeight="8" VerticalExpand="True"/> <!-- Filler -->
|
||||
|
||||
<!-- Alarm -->
|
||||
<BoxContainer Name="AlarmControl"
|
||||
Orientation="Vertical"
|
||||
VerticalAlignment="Center"
|
||||
Visible="False">
|
||||
|
||||
<controls:StripeBack Margin="-8 0">
|
||||
<BoxContainer Orientation="Vertical">
|
||||
<RichTextLabel Name="BigAlarmLabel"
|
||||
HorizontalAlignment="Center"/>
|
||||
|
||||
<RichTextLabel Name="BigAlarmLabelTwo"
|
||||
HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
</controls:StripeBack>
|
||||
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}"
|
||||
HorizontalAlignment="Center"
|
||||
StyleClasses="LabelSubText"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="StrengthLabel" Margin="0 0 20 0" HorizontalExpand="True" HorizontalAlignment="Left" VerticalAlignment="Center"/>
|
||||
<SpinBox Name="StateSpinBox" Value="0"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<BoxContainer Orientation="Horizontal">
|
||||
<RichTextLabel Name="DrawLabel" HorizontalExpand="True"/>
|
||||
<RichTextLabel Name="DrawValueLabel"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="10" VerticalExpand="True"/>
|
||||
<BoxContainer Name="AlarmControl" Orientation="Vertical" VerticalAlignment="Center" Visible="False">
|
||||
<RichTextLabel Name="BigAlarmLabel" HorizontalAlignment="Center"/>
|
||||
<RichTextLabel Name="BigAlarmLabelTwo" HorizontalAlignment="Center"/>
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-service-manual-reference'}" HorizontalAlignment="Center" StyleClasses="LabelSubText"/>
|
||||
</BoxContainer>
|
||||
<Control MinHeight="10" VerticalExpand="True"/>
|
||||
</BoxContainer>
|
||||
<customControls:VSeparator Margin="0 0 0 10"/>
|
||||
<BoxContainer Orientation="Vertical" Margin="20 0 20 0" VerticalAlignment="Center">
|
||||
<PanelContainer Name="BackPanel" HorizontalAlignment="Center">
|
||||
|
||||
<PanelContainer StyleClasses="LowDivider" Margin="0 -8" HorizontalAlignment="Right"/>
|
||||
|
||||
<!-- PA Visual part -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Center"
|
||||
Margin="8">
|
||||
|
||||
<PanelContainer Name="BackPanel"
|
||||
HorizontalAlignment="Center">
|
||||
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxTexture Modulate="#202023" PatchMarginBottom="10" PatchMarginLeft="10" PatchMarginRight="10" PatchMarginTop="10"/>
|
||||
<gfx:StyleBoxTexture Modulate="#202023"
|
||||
PatchMarginBottom="8"
|
||||
PatchMarginLeft="8"
|
||||
PatchMarginRight="8"
|
||||
PatchMarginTop="8"/>
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Orientation="Vertical" HorizontalExpand="True" HorizontalAlignment="Center" VerticalExpand="True">
|
||||
<GridContainer Columns="3" VSeparationOverride="0" HSeparationOverride="0" HorizontalAlignment="Center">
|
||||
|
||||
<BoxContainer Orientation="Vertical"
|
||||
SeparationOverride="6"
|
||||
VerticalExpand="True"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalExpand="True"
|
||||
HorizontalAlignment="Center">
|
||||
|
||||
<!-- PA Visualisation -->
|
||||
<GridContainer Columns="3"
|
||||
VSeparationOverride="0"
|
||||
HSeparationOverride="0"
|
||||
HorizontalAlignment="Center">
|
||||
|
||||
<Control/>
|
||||
<ui:PASegmentControl Name="EndCapTexture" BaseState="end_cap"/>
|
||||
<Control/>
|
||||
@@ -58,17 +137,47 @@
|
||||
<ui:PASegmentControl Name="EmitterForeTexture" BaseState="emitter_fore"/>
|
||||
<ui:PASegmentControl Name="EmitterPortTexture" BaseState="emitter_port"/>
|
||||
</GridContainer>
|
||||
<Control MinHeight="5"/>
|
||||
<Button Name="ScanButton" Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}" HorizontalAlignment="Center"/>
|
||||
|
||||
<Button Name="ScanButton"
|
||||
Text="{Loc 'particle-accelerator-control-menu-scan-parts-button'}"
|
||||
HorizontalAlignment="Center"/>
|
||||
</BoxContainer>
|
||||
</PanelContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="4 4 0 4"/>
|
||||
</controls:StripeBack>
|
||||
<BoxContainer Orientation="Horizontal" Margin="12 0 0 0">
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-foo-bar-baz'}" StyleClasses="LabelSubText"/>
|
||||
|
||||
<!-- Footer -->
|
||||
<BoxContainer Orientation="Vertical"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<controls:StripeBack>
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-check-containment-field-warning'}"
|
||||
HorizontalAlignment="Center"
|
||||
StyleClasses="LabelSubText"
|
||||
Margin="0 4"/>
|
||||
</controls:StripeBack>
|
||||
|
||||
<BoxContainer Orientation="Horizontal"
|
||||
Margin="12 0 6 2"
|
||||
VerticalAlignment="Bottom">
|
||||
|
||||
<!-- Footer title -->
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-left'}"
|
||||
StyleClasses="WindowFooterText" />
|
||||
|
||||
<!-- Version -->
|
||||
<Label Text="{Loc 'particle-accelerator-control-menu-flavor-right'}"
|
||||
StyleClasses="WindowFooterText"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalExpand="True"
|
||||
Margin="0 0 4 0" />
|
||||
|
||||
<TextureRect StyleClasses="NTLogoDark"
|
||||
Stretch="KeepAspectCentered"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
SetSize="19 19"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
@@ -11,37 +11,37 @@
|
||||
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
|
||||
|
||||
<!-- Entry layer (shows main categories) -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
<ui:RadialMenuTextureButton StyleClasses="RadialMenuButton" SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
|
||||
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
|
||||
</ui:RadialMenuTextureButton>
|
||||
</ui:RadialMenuTextureButtonWithSector>
|
||||
</ui:RadialContainer>
|
||||
|
||||
<!-- Walls and flooring -->
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Windows and grilles -->
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Airlocks -->
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Computer and machine frames -->
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
<!-- Lighting -->
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" Radius="64"/>
|
||||
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -74,7 +74,6 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
|
||||
var button = new RCDMenuButton()
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = tooltip,
|
||||
ProtoId = protoId,
|
||||
@@ -99,9 +98,7 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
// is visible in the main radial container (as these all start with Visible = false)
|
||||
foreach (var child in main.Children)
|
||||
{
|
||||
var castChild = child as RadialMenuTextureButton;
|
||||
|
||||
if (castChild is not RadialMenuTextureButton)
|
||||
if (child is not RadialMenuTextureButton castChild)
|
||||
continue;
|
||||
|
||||
if (castChild.TargetLayer == proto.Category)
|
||||
@@ -169,12 +166,7 @@ public sealed partial class RCDMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButton
|
||||
public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public ProtoId<RCDPrototype> ProtoId { get; set; }
|
||||
|
||||
public RCDMenuButton()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ public sealed class RadiationSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IOverlayManager _overlayMan = default!;
|
||||
|
||||
public List<RadiationRay>? Rays;
|
||||
public List<DebugRadiationRay>? Rays;
|
||||
public Dictionary<NetEntity, Dictionary<Vector2i, float>>? ResistanceGrids;
|
||||
|
||||
public override void Initialize()
|
||||
|
||||
@@ -81,13 +81,19 @@ public sealed partial class NavScreen : BoxContainer
|
||||
// Get the positive reduced angle.
|
||||
var displayRot = -worldRot.Reduced();
|
||||
|
||||
GridPosition.Text = $"{worldPos.X:0.0}, {worldPos.Y:0.0}";
|
||||
GridOrientation.Text = $"{displayRot.Degrees:0.0}";
|
||||
GridPosition.Text = Loc.GetString("shuttle-console-position-value",
|
||||
("X", $"{worldPos.X:0.0}"),
|
||||
("Y", $"{worldPos.Y:0.0}"));
|
||||
GridOrientation.Text = Loc.GetString("shuttle-console-orientation-value",
|
||||
("angle", $"{displayRot.Degrees:0.0}"));
|
||||
|
||||
var gridVelocity = gridBody.LinearVelocity;
|
||||
gridVelocity = displayRot.RotateVec(gridVelocity);
|
||||
// Get linear velocity relative to the console entity
|
||||
GridLinearVelocity.Text = $"{gridVelocity.X + 10f * float.Epsilon:0.0}, {gridVelocity.Y + 10f * float.Epsilon:0.0}";
|
||||
GridAngularVelocity.Text = $"{-gridBody.AngularVelocity + 10f * float.Epsilon:0.0}";
|
||||
GridLinearVelocity.Text = Loc.GetString("shuttle-console-linear-velocity-value",
|
||||
("X", $"{gridVelocity.X + 10f * float.Epsilon:0.0}"),
|
||||
("Y", $"{gridVelocity.Y + 10f * float.Epsilon:0.0}"));
|
||||
GridAngularVelocity.Text = Loc.GetString("shuttle-console-angular-velocity-value",
|
||||
("angularVelocity", $"{-MathHelper.RadiansToDegrees(gridBody.AngularVelocity) + 10f * float.Epsilon:0.0}"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
<ui:RadialMenu xmlns="https://spacestation14.io"
|
||||
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
BackButtonStyleClass="RadialMenuBackButton"
|
||||
CloseButtonStyleClass="RadialMenuCloseButton"
|
||||
@@ -7,7 +7,7 @@
|
||||
MinSize="450 450">
|
||||
|
||||
<!-- Main -->
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" Radius="64" ReserveSpaceForHiddenChildren="False">
|
||||
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
|
||||
</ui:RadialContainer>
|
||||
|
||||
</ui:RadialMenu>
|
||||
|
||||
@@ -54,7 +54,6 @@ public sealed partial class StationAiMenu : RadialMenu
|
||||
// TODO: This radial boilerplate is quite annoying
|
||||
var button = new StationAiMenuButton(action.Event)
|
||||
{
|
||||
StyleClasses = { "RadialMenuButton" },
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
|
||||
};
|
||||
@@ -121,7 +120,7 @@ public sealed partial class StationAiMenu : RadialMenu
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButton
|
||||
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector
|
||||
{
|
||||
public BaseStationAiAction Action = action;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
@@ -8,6 +7,11 @@ namespace Content.Client.UserInterface.Controls;
|
||||
[Virtual]
|
||||
public class RadialContainer : LayoutContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Increment of radius per child element to be rendered.
|
||||
/// </summary>
|
||||
private const float RadiusIncrement = 5f;
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the anglular range, in radians, in which child elements will be placed.
|
||||
/// The first value denotes the angle at which the first element is to be placed, and
|
||||
@@ -49,10 +53,30 @@ public class RadialContainer : LayoutContainer
|
||||
public RAlignment RadialAlignment { get; set; } = RAlignment.Clockwise;
|
||||
|
||||
/// <summary>
|
||||
/// Determines how far from the radial container's center that its child elements will be placed
|
||||
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
|
||||
/// To correctly display dynamic amount of elements control actually resizes depending on amount of child buttons,
|
||||
/// but uses this property as base value for final radius calculation.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public float Radius { get; set; } = 100f;
|
||||
public float InitialRadius { get; set; } = 100f;
|
||||
|
||||
/// <summary>
|
||||
/// Radial menu radius determines how far from the radial container's center its child elements will be placed.
|
||||
/// This is dynamically calculated (based on child button count) radius, result of <see cref="InitialRadius"/> and
|
||||
/// <see cref="RadiusIncrement"/> multiplied by currently visible child button count.
|
||||
/// </summary>
|
||||
[ViewVariables(VVAccess.ReadOnly)]
|
||||
public float CalculatedRadius { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines radial menu button sectors inner radius, is a multiplier of <see cref="InitialRadius"/>.
|
||||
/// </summary>
|
||||
public float InnerRadiusMultiplier { get; set; } = 0.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Determines radial menu button sectors outer radius, is a multiplier of <see cref="InitialRadius"/>.
|
||||
/// </summary>
|
||||
public float OuterRadiusMultiplier { get; set; } = 1.5f;
|
||||
|
||||
/// <summary>
|
||||
/// Sets whether the container should reserve a space on the layout for child which are not currently visible
|
||||
@@ -67,37 +91,74 @@ public class RadialContainer : LayoutContainer
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
|
||||
const float baseRadius = 100f;
|
||||
const float radiusIncrement = 5f;
|
||||
|
||||
var children = ReserveSpaceForHiddenChildren ? Children : Children.Where(x => x.Visible);
|
||||
var children = ReserveSpaceForHiddenChildren
|
||||
? Children
|
||||
: Children.Where(x => x.Visible);
|
||||
|
||||
var childCount = children.Count();
|
||||
|
||||
// Add padding from the center at higher child counts so they don't overlap.
|
||||
Radius = baseRadius + (childCount * radiusIncrement);
|
||||
|
||||
// Add padding from the center at higher child counts so they don't overlap.
|
||||
CalculatedRadius = InitialRadius + (childCount * RadiusIncrement);
|
||||
|
||||
var isAntiClockwise = RadialAlignment == RAlignment.AntiClockwise;
|
||||
|
||||
// Determine the size of the arc, accounting for clockwise and anti-clockwise arrangements
|
||||
var arc = AngularRange.Y - AngularRange.X;
|
||||
arc = (arc < 0) ? MathF.Tau + arc : arc;
|
||||
arc = (RadialAlignment == RAlignment.AntiClockwise) ? MathF.Tau - arc : arc;
|
||||
arc = arc < 0
|
||||
? MathF.Tau + arc
|
||||
: arc;
|
||||
arc = isAntiClockwise
|
||||
? MathF.Tau - arc
|
||||
: arc;
|
||||
|
||||
// Account for both circular arrangements and arc-based arrangements
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f) ? 0 : 1;
|
||||
var childMod = MathHelper.CloseTo(arc, MathF.Tau, 0.01f)
|
||||
? 0
|
||||
: 1;
|
||||
|
||||
// Determine the separation between child elements
|
||||
var sepAngle = arc / (childCount - childMod);
|
||||
sepAngle *= (RadialAlignment == RAlignment.AntiClockwise) ? -1f : 1f;
|
||||
sepAngle *= isAntiClockwise
|
||||
? -1f
|
||||
: 1f;
|
||||
|
||||
var controlCenter = finalSize * 0.5f;
|
||||
|
||||
// Adjust the positions of all the child elements
|
||||
foreach (var (i, child) in children.Select((x, i) => (i, x)))
|
||||
var query = children.Select((x, index) => (index, x));
|
||||
foreach (var (childIndex, child) in query)
|
||||
{
|
||||
var position = new Vector2(Radius * MathF.Sin(AngularRange.X + sepAngle * i) + Width / 2f - child.Width / 2f, -Radius * MathF.Cos(AngularRange.X + sepAngle * i) + Height / 2f - child.Height / 2f);
|
||||
const float angleOffset = MathF.PI * 0.5f;
|
||||
|
||||
var targetAngleOfChild = AngularRange.X + sepAngle * (childIndex + 0.5f) + angleOffset;
|
||||
|
||||
// flooring values for snapping float values to physical grid -
|
||||
// it prevents gaps and overlapping between different button segments
|
||||
var position = new Vector2(
|
||||
MathF.Floor(CalculatedRadius * MathF.Cos(targetAngleOfChild)),
|
||||
MathF.Floor(-CalculatedRadius * MathF.Sin(targetAngleOfChild))
|
||||
) + controlCenter - child.DesiredSize * 0.5f + Position;
|
||||
|
||||
SetPosition(child, position);
|
||||
|
||||
// radial menu buttons with sector need to also know in which sector and around which point
|
||||
// they should be rendered, how much space sector should should take etc.
|
||||
if (child is IRadialMenuItemWithSector tb)
|
||||
{
|
||||
tb.AngleSectorFrom = sepAngle * childIndex;
|
||||
tb.AngleSectorTo = sepAngle * (childIndex + 1);
|
||||
tb.AngleOffset = angleOffset;
|
||||
tb.InnerRadius = CalculatedRadius * InnerRadiusMultiplier;
|
||||
tb.OuterRadius = CalculatedRadius * OuterRadiusMultiplier;
|
||||
tb.ParentCenter = controlCenter;
|
||||
}
|
||||
}
|
||||
|
||||
return base.ArrangeOverride(finalSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -109,4 +170,5 @@ public class RadialContainer : LayoutContainer
|
||||
Clockwise,
|
||||
AntiClockwise,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.CustomControls;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Content.Shared.Input;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Shared.Input;
|
||||
|
||||
namespace Content.Client.UserInterface.Controls;
|
||||
|
||||
@@ -12,11 +15,16 @@ public class RadialMenu : BaseWindow
|
||||
/// <summary>
|
||||
/// Contextual button used to traverse through previous layers of the radial menu
|
||||
/// </summary>
|
||||
public TextureButton? ContextualButton { get; set; }
|
||||
public RadialMenuContextualCentralTextureButton ContextualButton { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Button that represents outer area of menu (closes menu on outside clicks).
|
||||
/// </summary>
|
||||
public RadialMenuOuterAreaButton MenuOuterAreaButton { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Set a style class to be applied to the contextual button when it is set to move the user back through previous layers of the radial menu
|
||||
/// </summary>
|
||||
/// </summary>
|
||||
public string? BackButtonStyleClass
|
||||
{
|
||||
get
|
||||
@@ -52,7 +60,7 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
private List<Control> _path = new();
|
||||
private readonly List<Control> _path = new();
|
||||
private string? _backButtonStyleClass;
|
||||
private string? _closeButtonStyleClass;
|
||||
|
||||
@@ -60,8 +68,8 @@ public class RadialMenu : BaseWindow
|
||||
/// A free floating menu which enables the quick display of one or more radial containers
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
|
||||
/// the menu), along with a contextual button at the menu center, which will either return the user
|
||||
/// Only one radial container is visible at a time (each container forming a separate 'layer' within
|
||||
/// the menu), along with a contextual button at the menu center, which will either return the user
|
||||
/// to the previous layer or close the menu if there are no previous layers left to traverse.
|
||||
/// To create a functional radial menu, simply parent one or more named radial containers to it,
|
||||
/// and populate the radial containers with RadialMenuButtons. Setting the TargetLayer field of these
|
||||
@@ -78,23 +86,56 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
|
||||
// Auto generate a contextual button for moving back through visited layers
|
||||
ContextualButton = new TextureButton()
|
||||
ContextualButton = new RadialMenuContextualCentralTextureButton
|
||||
{
|
||||
HorizontalAlignment = HAlignment.Center,
|
||||
VerticalAlignment = VAlignment.Center,
|
||||
SetSize = new Vector2(64f, 64f),
|
||||
};
|
||||
MenuOuterAreaButton = new RadialMenuOuterAreaButton();
|
||||
|
||||
ContextualButton.OnButtonUp += _ => ReturnToPreviousLayer();
|
||||
MenuOuterAreaButton.OnButtonUp += _ => Close();
|
||||
AddChild(ContextualButton);
|
||||
AddChild(MenuOuterAreaButton);
|
||||
|
||||
// Hide any further add children, unless its promoted to the active layer
|
||||
OnChildAdded += child => child.Visible = (GetCurrentActiveLayer() == child);
|
||||
OnChildAdded += child =>
|
||||
{
|
||||
child.Visible = GetCurrentActiveLayer() == child;
|
||||
SetupContextualButtonData(child);
|
||||
};
|
||||
}
|
||||
|
||||
private void SetupContextualButtonData(Control child)
|
||||
{
|
||||
if (child is RadialContainer { Visible: true } container)
|
||||
{
|
||||
var parentCenter = MinSize * 0.5f;
|
||||
ContextualButton.ParentCenter = parentCenter;
|
||||
MenuOuterAreaButton.ParentCenter = parentCenter;
|
||||
ContextualButton.InnerRadius = container.CalculatedRadius * container.InnerRadiusMultiplier;
|
||||
MenuOuterAreaButton.OuterRadius = container.CalculatedRadius * container.OuterRadiusMultiplier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override Vector2 ArrangeOverride(Vector2 finalSize)
|
||||
{
|
||||
var result = base.ArrangeOverride(finalSize);
|
||||
|
||||
var currentLayer = GetCurrentActiveLayer();
|
||||
if (currentLayer != null)
|
||||
{
|
||||
SetupContextualButtonData(currentLayer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Control? GetCurrentActiveLayer()
|
||||
{
|
||||
var children = Children.Where(x => x != ContextualButton);
|
||||
var children = Children.Where(x => x != ContextualButton && x != MenuOuterAreaButton);
|
||||
|
||||
if (!children.Any())
|
||||
return null;
|
||||
@@ -116,7 +157,7 @@ public class RadialMenu : BaseWindow
|
||||
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child == ContextualButton)
|
||||
if (child == ContextualButton || child == MenuOuterAreaButton)
|
||||
continue;
|
||||
|
||||
// Hide layers which are not of interest
|
||||
@@ -129,6 +170,7 @@ public class RadialMenu : BaseWindow
|
||||
else
|
||||
{
|
||||
child.Visible = true;
|
||||
SetupContextualButtonData(child);
|
||||
result = true;
|
||||
}
|
||||
}
|
||||
@@ -158,7 +200,7 @@ public class RadialMenu : BaseWindow
|
||||
// Hide all children except the contextual button
|
||||
foreach (var child in Children)
|
||||
{
|
||||
if (child != ContextualButton)
|
||||
if (child != ContextualButton && child != MenuOuterAreaButton)
|
||||
child.Visible = false;
|
||||
}
|
||||
|
||||
@@ -172,49 +214,86 @@ public class RadialMenu : BaseWindow
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base class for radial menu buttons. Excludes all actions except clicks and alt-clicks
|
||||
/// from interactions.
|
||||
/// </summary>
|
||||
[Virtual]
|
||||
public class RadialMenuButton : Button
|
||||
public class RadialMenuTextureButtonBase : TextureButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will transition to the named layer
|
||||
/// </summary>
|
||||
public string? TargetLayer { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuButton()
|
||||
/// <inheritdoc />
|
||||
protected RadialMenuTextureButtonBase()
|
||||
{
|
||||
OnButtonUp += OnClicked;
|
||||
EnableAllKeybinds = true;
|
||||
}
|
||||
|
||||
private void OnClicked(ButtonEventArgs args)
|
||||
/// <inheritdoc />
|
||||
protected override void KeyBindUp(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (TargetLayer == null || TargetLayer == string.Empty)
|
||||
return;
|
||||
|
||||
var parent = FindParentMultiLayerContainer(this);
|
||||
|
||||
if (parent == null)
|
||||
return;
|
||||
|
||||
parent.TryToMoveToNewLayer(TargetLayer);
|
||||
if (args.Function == EngineKeyFunctions.UIClick
|
||||
|| args.Function == ContentKeyFunctions.AltActivateItemInWorld)
|
||||
base.KeyBindUp(args);
|
||||
}
|
||||
}
|
||||
|
||||
private RadialMenu? FindParentMultiLayerContainer(Control control)
|
||||
/// <summary>
|
||||
/// Special button for closing radial menu or going back between radial menu levels.
|
||||
/// Is looking like just <see cref="TextureButton "/> but considers whole space around
|
||||
/// itself (til radial menu buttons) as itself in case of clicking. But this 'effect'
|
||||
/// works only if control have parent, and ActiveContainer property is set.
|
||||
/// Also considers all space outside of radial menu buttons as itself for clicking.
|
||||
/// </summary>
|
||||
public sealed class RadialMenuContextualCentralTextureButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
public float InnerRadius { get; set; }
|
||||
|
||||
public Vector2? ParentCenter { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
if (ParentCenter == null)
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
return null;
|
||||
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
|
||||
|
||||
var innerRadiusSquared = InnerRadius * InnerRadius;
|
||||
|
||||
// comparing to squared values is faster then making sqrt
|
||||
return distSquared < innerRadiusSquared;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Menu button for outer area of radial menu (covers everything 'outside').
|
||||
/// </summary>
|
||||
public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
public float OuterRadius { get; set; }
|
||||
|
||||
public Vector2? ParentCenter { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
if (ParentCenter == null)
|
||||
{
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
var distSquared = (point + Position - ParentCenter.Value).LengthSquared();
|
||||
|
||||
var outerRadiusSquared = OuterRadius * OuterRadius;
|
||||
|
||||
// comparing to squared values is faster, then making sqrt
|
||||
return distSquared > outerRadiusSquared;
|
||||
}
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButton : TextureButton
|
||||
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Upon clicking this button the radial menu will be moved to the named layer
|
||||
@@ -226,6 +305,7 @@ public class RadialMenuTextureButton : TextureButton
|
||||
/// </summary>
|
||||
public RadialMenuTextureButton()
|
||||
{
|
||||
EnableAllKeybinds = true;
|
||||
OnButtonUp += OnClicked;
|
||||
}
|
||||
|
||||
@@ -246,10 +326,329 @@ public class RadialMenuTextureButton : TextureButton
|
||||
{
|
||||
foreach (var ancestor in control.GetSelfAndLogicalAncestors())
|
||||
{
|
||||
if (ancestor is RadialMenu)
|
||||
return ancestor as RadialMenu;
|
||||
if (ancestor is RadialMenu menu)
|
||||
return menu;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public interface IRadialMenuItemWithSector
|
||||
{
|
||||
/// <summary>
|
||||
/// Angle in radian where button sector should start.
|
||||
/// </summary>
|
||||
public float AngleSectorFrom { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Angle in radian where button sector should end.
|
||||
/// </summary>
|
||||
public float AngleSectorTo { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outer radius for drawing segment and pointer detection.
|
||||
/// </summary>
|
||||
public float OuterRadius { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Outer radius for drawing segment and pointer detection.
|
||||
/// </summary>
|
||||
public float InnerRadius { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Offset in radian by which menu button should be rotated.
|
||||
/// </summary>
|
||||
public float AngleOffset { set; }
|
||||
|
||||
/// <summary>
|
||||
/// Coordinates of center in parent component - button container.
|
||||
/// </summary>
|
||||
public Vector2 ParentCenter { set; }
|
||||
}
|
||||
|
||||
[Virtual]
|
||||
public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadialMenuItemWithSector
|
||||
{
|
||||
private Vector2[]? _sectorPointsForDrawing;
|
||||
|
||||
private float _angleSectorFrom;
|
||||
private float _angleSectorTo;
|
||||
private float _outerRadius;
|
||||
private float _innerRadius;
|
||||
private float _angleOffset;
|
||||
|
||||
private bool _isWholeCircle;
|
||||
private Vector2? _parentCenter;
|
||||
|
||||
private Color _backgroundColorSrgb = Color.ToSrgb(new Color(70, 73, 102, 128));
|
||||
private Color _hoverBackgroundColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
|
||||
private Color _borderColorSrgb = Color.ToSrgb(new Color(173, 216, 230, 70));
|
||||
private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render border of segment. Is false by default.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// By default color of border is same as color of background. Use <see cref="BorderColor"/>
|
||||
/// and <see cref="HoverBorderColor"/> to change it.
|
||||
/// </remarks>
|
||||
public bool DrawBorder { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render background of all sector. Is true by default.
|
||||
/// </summary>
|
||||
public bool DrawBackground { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Marker, that control should render separator lines.
|
||||
/// Separator lines are used to visually separate sector of radial menu items.
|
||||
/// Is true by default
|
||||
/// </summary>
|
||||
public bool DrawSeparators { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color BackgroundColor
|
||||
{
|
||||
get => Color.FromSrgb(_backgroundColorSrgb);
|
||||
set => _backgroundColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of background in hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color HoverBackgroundColor
|
||||
{
|
||||
get => Color.FromSrgb(_hoverBackgroundColorSrgb);
|
||||
set => _hoverBackgroundColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of button border. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color BorderColor
|
||||
{
|
||||
get => Color.FromSrgb(_borderColorSrgb);
|
||||
set => _borderColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of button border when button is hovered. Accepts RGB color, works with sRGB for DrawPrimitive internally.
|
||||
/// </summary>
|
||||
public Color HoverBorderColor
|
||||
{
|
||||
get => Color.FromSrgb(_hoverBorderColorSrgb);
|
||||
set => _hoverBorderColorSrgb = Color.ToSrgb(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Color of separator lines.
|
||||
/// Separator lines are used to visually separate sector of radial menu items.
|
||||
/// </summary>
|
||||
public Color SeparatorColor { get; set; } = new Color(128, 128, 128, 128);
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.AngleSectorFrom
|
||||
{
|
||||
set
|
||||
{
|
||||
_angleSectorFrom = value;
|
||||
_isWholeCircle = IsWholeCircle(value, _angleSectorTo);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.AngleSectorTo
|
||||
{
|
||||
set
|
||||
{
|
||||
_angleSectorTo = value;
|
||||
_isWholeCircle = IsWholeCircle(_angleSectorFrom, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.OuterRadius { set => _outerRadius = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
float IRadialMenuItemWithSector.InnerRadius { set => _innerRadius = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public float AngleOffset { set => _angleOffset = value; }
|
||||
|
||||
/// <inheritdoc />
|
||||
Vector2 IRadialMenuItemWithSector.ParentCenter { set => _parentCenter = value; }
|
||||
|
||||
/// <summary>
|
||||
/// A simple texture button that can move the user to a different layer within a radial menu
|
||||
/// </summary>
|
||||
public RadialMenuTextureButtonWithSector()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Draw(DrawingHandleScreen handle)
|
||||
{
|
||||
base.Draw(handle);
|
||||
|
||||
if (_parentCenter == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// draw sector where space that button occupies actually is
|
||||
var containerCenter = (_parentCenter.Value - Position) * UIScale;
|
||||
|
||||
var angleFrom = _angleSectorFrom + _angleOffset;
|
||||
var angleTo = _angleSectorTo + _angleOffset;
|
||||
if (DrawBackground)
|
||||
{
|
||||
var segmentColor = DrawMode == DrawModeEnum.Hover
|
||||
? _hoverBackgroundColorSrgb
|
||||
: _backgroundColorSrgb;
|
||||
|
||||
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, segmentColor);
|
||||
}
|
||||
|
||||
if (DrawBorder)
|
||||
{
|
||||
var borderColor = DrawMode == DrawModeEnum.Hover
|
||||
? _hoverBorderColorSrgb
|
||||
: _borderColorSrgb;
|
||||
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false);
|
||||
}
|
||||
|
||||
if (!_isWholeCircle && DrawSeparators)
|
||||
{
|
||||
DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool HasPoint(Vector2 point)
|
||||
{
|
||||
if (_parentCenter == null)
|
||||
{
|
||||
return base.HasPoint(point);
|
||||
}
|
||||
|
||||
var outerRadiusSquared = _outerRadius * _outerRadius;
|
||||
var innerRadiusSquared = _innerRadius * _innerRadius;
|
||||
|
||||
var distSquared = (point + Position - _parentCenter.Value).LengthSquared();
|
||||
var isInRadius = distSquared < outerRadiusSquared && distSquared > innerRadiusSquared;
|
||||
if (!isInRadius)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// difference from the center of the parent to the `point`
|
||||
var pointFromParent = point + Position - _parentCenter.Value;
|
||||
|
||||
// Flip Y to get from ui coordinates to natural coordinates
|
||||
var angle = MathF.Atan2(-pointFromParent.Y, pointFromParent.X) - _angleOffset;
|
||||
if (angle < 0)
|
||||
{
|
||||
// atan2 range is -pi->pi, while angle sectors are
|
||||
// 0->2pi, so remap the result into that range
|
||||
angle = MathF.PI * 2 + angle;
|
||||
}
|
||||
|
||||
var isInAngle = angle >= _angleSectorFrom && angle < _angleSectorTo;
|
||||
return isInAngle;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Draw segment between two concentrated circles from and to certain angles.
|
||||
/// </summary>
|
||||
/// <param name="drawingHandleScreen">Drawing handle, to which rendering should be delegated.</param>
|
||||
/// <param name="center">Point where circle center should be.</param>
|
||||
/// <param name="radiusInner">Radius of internal circle.</param>
|
||||
/// <param name="radiusOuter">Radius of external circle.</param>
|
||||
/// <param name="angleSectorFrom">Angle in radian, from which sector should start.</param>
|
||||
/// <param name="angleSectorTo">Angle in radian, from which sector should start.</param>
|
||||
/// <param name="color">Color for drawing.</param>
|
||||
/// <param name="filled">Should figure be filled, or have only border.</param>
|
||||
private void DrawAnnulusSector(
|
||||
DrawingHandleScreen drawingHandleScreen,
|
||||
Vector2 center,
|
||||
float radiusInner,
|
||||
float radiusOuter,
|
||||
float angleSectorFrom,
|
||||
float angleSectorTo,
|
||||
Color color,
|
||||
bool filled = true
|
||||
)
|
||||
{
|
||||
const float minimalSegmentSize = MathF.Tau / 128f;
|
||||
|
||||
var requestedSegmentSize = angleSectorTo - angleSectorFrom;
|
||||
var segmentCount = (int)(requestedSegmentSize / minimalSegmentSize) + 1;
|
||||
var anglePerSegment = requestedSegmentSize / (segmentCount - 1);
|
||||
|
||||
var bufferSize = segmentCount * 2;
|
||||
if (_sectorPointsForDrawing == null || _sectorPointsForDrawing.Length != bufferSize)
|
||||
{
|
||||
_sectorPointsForDrawing ??= new Vector2[bufferSize];
|
||||
}
|
||||
|
||||
for (var i = 0; i < segmentCount; i++)
|
||||
{
|
||||
var angle = angleSectorFrom + anglePerSegment * i;
|
||||
|
||||
// Flip Y to get from ui coordinates to natural coordinates
|
||||
var unitPos = new Vector2(MathF.Cos(angle), -MathF.Sin(angle));
|
||||
var outerPoint = center + unitPos * radiusOuter;
|
||||
var innerPoint = center + unitPos * radiusInner;
|
||||
if (filled)
|
||||
{
|
||||
// to make filled sector we need to create strip from triangles
|
||||
_sectorPointsForDrawing[i * 2] = outerPoint;
|
||||
_sectorPointsForDrawing[i * 2 + 1] = innerPoint;
|
||||
}
|
||||
else
|
||||
{
|
||||
// to make border of sector we need points ordered as sequences on radius
|
||||
_sectorPointsForDrawing[i] = outerPoint;
|
||||
_sectorPointsForDrawing[bufferSize - 1 - i] = innerPoint;
|
||||
}
|
||||
}
|
||||
|
||||
var type = filled
|
||||
? DrawPrimitiveTopology.TriangleStrip
|
||||
: DrawPrimitiveTopology.LineStrip;
|
||||
drawingHandleScreen.DrawPrimitives(type, _sectorPointsForDrawing, color);
|
||||
}
|
||||
|
||||
private static void DrawSeparatorLines(
|
||||
DrawingHandleScreen drawingHandleScreen,
|
||||
Vector2 center,
|
||||
float radiusInner,
|
||||
float radiusOuter,
|
||||
float angleSectorFrom,
|
||||
float angleSectorTo,
|
||||
Color color
|
||||
)
|
||||
{
|
||||
var fromPoint = new Angle(-angleSectorFrom).RotateVec(Vector2.UnitX);
|
||||
drawingHandleScreen.DrawLine(
|
||||
center + fromPoint * radiusOuter,
|
||||
center + fromPoint * radiusInner,
|
||||
color
|
||||
);
|
||||
|
||||
var toPoint = new Angle(-angleSectorTo).RotateVec(Vector2.UnitX);
|
||||
drawingHandleScreen.DrawLine(
|
||||
center + toPoint * radiusOuter,
|
||||
center + toPoint * radiusInner,
|
||||
color
|
||||
);
|
||||
}
|
||||
|
||||
private static bool IsWholeCircle(float angleSectorFrom, float angleSectorTo)
|
||||
{
|
||||
return new Angle(angleSectorFrom).EqualsApprox(new Angle(angleSectorTo));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
public IAHelpUIHandler? UIHelper;
|
||||
private bool _discordRelayActive;
|
||||
private bool _hasUnreadAHelp;
|
||||
private bool _bwoinkSoundEnabled;
|
||||
private string? _aHelpSound;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -56,6 +57,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
|
||||
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
|
||||
_config.OnValueChanged(CCVars.AHelpSound, v => _aHelpSound = v, true);
|
||||
_config.OnValueChanged(CCVars.BwoinkSoundEnabled, v => _bwoinkSoundEnabled = v, true);
|
||||
}
|
||||
|
||||
public void UnloadButton()
|
||||
@@ -135,7 +137,7 @@ public sealed class AHelpUIController: UIController, IOnSystemChanged<BwoinkSyst
|
||||
}
|
||||
if (message.PlaySound && localPlayer.UserId != message.TrueSender)
|
||||
{
|
||||
if (_aHelpSound != null)
|
||||
if (_aHelpSound != null && (_bwoinkSoundEnabled || !_adminManager.IsActive()))
|
||||
_audio.PlayGlobal(_aHelpSound, Filter.Local(), false);
|
||||
_clyde.RequestWindowAttention();
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
using Content.Shared.Mobs.Components;
|
||||
using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Traits.Assorted;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.Player;
|
||||
@@ -94,7 +95,11 @@ public sealed class DamageOverlayUiController : UIController
|
||||
{
|
||||
case MobState.Alive:
|
||||
{
|
||||
if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
|
||||
if (EntityManager.HasComponent<PainNumbnessComponent>(entity))
|
||||
{
|
||||
_overlay.BruteLevel = 0;
|
||||
}
|
||||
else if (damageable.DamagePerGroup.TryGetValue("Brute", out var bruteDamage))
|
||||
{
|
||||
_overlay.BruteLevel = FixedPoint2.Min(1f, bruteDamage / critThreshold).Float();
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@ public sealed partial class MeleeWeaponSystem
|
||||
const float length = 0.15f;
|
||||
var startOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance / 5f));
|
||||
var endOffset = sprite.Rotation.RotateVec(new Vector2(0f, -distance));
|
||||
sprite.Rotation += spriteRotation;
|
||||
|
||||
return new Animation()
|
||||
{
|
||||
|
||||
@@ -170,7 +170,7 @@ public sealed partial class MeleeWeaponSystem : SharedMeleeWeaponSystem
|
||||
var targetCoordinates = xform.Coordinates;
|
||||
var targetLocalAngle = xform.LocalRotation;
|
||||
|
||||
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range);
|
||||
return Interaction.InRangeUnobstructed(user, target, targetCoordinates, targetLocalAngle, range, overlapCheck: false);
|
||||
}
|
||||
|
||||
protected override void DoDamageEffect(List<EntityUid> targets, EntityUid? user, TransformComponent targetXform)
|
||||
|
||||
49
Content.Client/Wieldable/WieldableSystem.cs
Normal file
49
Content.Client/Wieldable/WieldableSystem.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using System.Numerics;
|
||||
using Content.Client.Movement.Components;
|
||||
using Content.Client.Movement.Systems;
|
||||
using Content.Shared.Camera;
|
||||
using Content.Shared.Hands;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Wieldable;
|
||||
using Content.Shared.Wieldable.Components;
|
||||
using Robust.Client.Timing;
|
||||
|
||||
namespace Content.Client.Wieldable;
|
||||
|
||||
public sealed class WieldableSystem : SharedWieldableSystem
|
||||
{
|
||||
[Dependency] private readonly EyeCursorOffsetSystem _eyeOffset = default!;
|
||||
[Dependency] private readonly IClientGameTiming _gameTiming = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, ItemUnwieldedEvent>(OnEyeOffsetUnwielded);
|
||||
SubscribeLocalEvent<CursorOffsetRequiresWieldComponent, HeldRelayedEvent<GetEyeOffsetRelayedEvent>>(OnGetEyeOffset);
|
||||
}
|
||||
|
||||
public void OnEyeOffsetUnwielded(Entity<CursorOffsetRequiresWieldComponent> entity, ref ItemUnwieldedEvent args)
|
||||
{
|
||||
if (!TryComp(entity.Owner, out EyeCursorOffsetComponent? cursorOffsetComp))
|
||||
return;
|
||||
|
||||
if (_gameTiming.IsFirstTimePredicted)
|
||||
cursorOffsetComp.CurrentPosition = Vector2.Zero;
|
||||
}
|
||||
|
||||
public void OnGetEyeOffset(Entity<CursorOffsetRequiresWieldComponent> entity, ref HeldRelayedEvent<GetEyeOffsetRelayedEvent> args)
|
||||
{
|
||||
if (!TryComp(entity.Owner, out WieldableComponent? wieldableComp))
|
||||
return;
|
||||
|
||||
if (!wieldableComp.Wielded)
|
||||
return;
|
||||
|
||||
var offset = _eyeOffset.OffsetAfterMouse(entity.Owner, null);
|
||||
if (offset == null)
|
||||
return;
|
||||
|
||||
args.Args.Offset += offset.Value;
|
||||
}
|
||||
}
|
||||
41
Content.IntegrationTests/Tests/ContrabandTest.cs
Normal file
41
Content.IntegrationTests/Tests/ContrabandTest.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Content.Shared.Contraband;
|
||||
using Robust.Shared.GameObjects;
|
||||
using Robust.Shared.Prototypes;
|
||||
|
||||
namespace Content.IntegrationTests.Tests;
|
||||
|
||||
[TestFixture]
|
||||
public sealed class ContrabandTest
|
||||
{
|
||||
[Test]
|
||||
public async Task EntityShowDepartmentsAndJobs()
|
||||
{
|
||||
await using var pair = await PoolManager.GetServerClient();
|
||||
var client = pair.Client;
|
||||
var protoMan = client.ResolveDependency<IPrototypeManager>();
|
||||
var componentFactory = client.ResolveDependency<IComponentFactory>();
|
||||
|
||||
await client.WaitAssertion(() =>
|
||||
{
|
||||
foreach (var proto in protoMan.EnumeratePrototypes<EntityPrototype>())
|
||||
{
|
||||
if (proto.Abstract || pair.IsTestPrototype(proto))
|
||||
continue;
|
||||
|
||||
if (!proto.TryGetComponent<ContrabandComponent>(out var contraband, componentFactory))
|
||||
continue;
|
||||
|
||||
Assert.That(protoMan.TryIndex(contraband.Severity, out var severity, false),
|
||||
@$"{proto.ID} has a ContrabandComponent with a unknown severity.");
|
||||
|
||||
if (!severity.ShowDepartmentsAndJobs)
|
||||
continue;
|
||||
|
||||
Assert.That(contraband.AllowedDepartments.Count + contraband.AllowedJobs.Count, Is.Not.EqualTo(0),
|
||||
@$"{proto.ID} has a ContrabandComponent with ShowDepartmentsAndJobs but no allowed departments or jobs.");
|
||||
}
|
||||
});
|
||||
|
||||
await pair.CleanReturnAsync();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using Content.Server.Wires;
|
||||
using Content.Shared.Access;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Wires;
|
||||
|
||||
namespace Content.Server.Access;
|
||||
@@ -31,11 +30,9 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
|
||||
|
||||
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
|
||||
{
|
||||
if (!EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
|
||||
{
|
||||
comp.Enabled = true;
|
||||
EntityManager.Dirty(wire.Owner, comp);
|
||||
}
|
||||
comp.Enabled = true;
|
||||
EntityManager.Dirty(wire.Owner, comp);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -58,7 +55,7 @@ public sealed partial class AccessWireAction : ComponentWireAction<AccessReaderC
|
||||
{
|
||||
if (!wire.IsCut)
|
||||
{
|
||||
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access) && !EntityManager.HasComponent<EmaggedComponent>(wire.Owner))
|
||||
if (EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var access))
|
||||
{
|
||||
access.Enabled = true;
|
||||
EntityManager.Dirty(wire.Owner, access);
|
||||
|
||||
@@ -245,6 +245,10 @@ public sealed class AccessOverriderSystem : SharedAccessOverriderSystem
|
||||
$"{ToPrettyString(player):player} has modified {ToPrettyString(accessReaderEnt.Value):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]");
|
||||
|
||||
accessReaderEnt.Value.Comp.AccessLists = ConvertAccessListToHashSet(newAccessList);
|
||||
|
||||
var ev = new OnAccessOverriderAccessUpdatedEvent(player);
|
||||
RaiseLocalEvent(component.TargetAccessReaderId, ref ev);
|
||||
|
||||
Dirty(accessReaderEnt.Value);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Server.Database;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
@@ -30,36 +28,15 @@ public sealed partial class BanManager
|
||||
private TimeSpan _banNotificationRateLimitStart;
|
||||
private int _banNotificationRateLimitCount;
|
||||
|
||||
private void OnDatabaseNotification(DatabaseNotification notification)
|
||||
private bool OnDatabaseNotificationEarlyFilter()
|
||||
{
|
||||
if (notification.Channel != BanNotificationChannel)
|
||||
return;
|
||||
|
||||
if (notification.Payload == null)
|
||||
{
|
||||
_sawmill.Error("Got ban notification with null payload!");
|
||||
return;
|
||||
}
|
||||
|
||||
BanNotificationData data;
|
||||
try
|
||||
{
|
||||
data = JsonSerializer.Deserialize<BanNotificationData>(notification.Payload)
|
||||
?? throw new JsonException("Content is null");
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
_sawmill.Error($"Got invalid JSON in ban notification: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CheckBanRateLimit())
|
||||
{
|
||||
_sawmill.Verbose("Not processing ban notification due to rate limit");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
_taskManager.RunOnMainThread(() => ProcessBanNotification(data));
|
||||
return true;
|
||||
}
|
||||
|
||||
private async void ProcessBanNotification(BanNotificationData data)
|
||||
|
||||
@@ -53,7 +53,12 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
{
|
||||
_netManager.RegisterNetMessage<MsgRoleBans>();
|
||||
|
||||
_db.SubscribeToNotifications(OnDatabaseNotification);
|
||||
_db.SubscribeToJsonNotification<BanNotificationData>(
|
||||
_taskManager,
|
||||
_sawmill,
|
||||
BanNotificationChannel,
|
||||
ProcessBanNotification,
|
||||
OnDatabaseNotificationEarlyFilter);
|
||||
|
||||
_userDbData.AddOnLoadPlayer(CachePlayerData);
|
||||
_userDbData.AddOnPlayerDisconnect(ClearPlayerData);
|
||||
@@ -160,6 +165,8 @@ public sealed partial class BanManager : IBanManager, IPostInjectInit
|
||||
null);
|
||||
|
||||
await _db.AddServerBanAsync(banDef);
|
||||
if (_cfg.GetCVar(CCVars.ServerBanResetLastReadRules) && target != null)
|
||||
await _db.SetLastReadRules(target.Value, null); // Reset their last read rules. They probably need a refresher!
|
||||
var adminName = banningAdmin == null
|
||||
? Loc.GetString("system-user")
|
||||
: (await _db.GetPlayerRecordByUserId(banningAdmin.Value))?.LastSeenUserName ?? Loc.GetString("system-user");
|
||||
|
||||
114
Content.Server/Administration/Managers/MultiServerKickManager.cs
Normal file
114
Content.Server/Administration/Managers/MultiServerKickManager.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Content.Server.Database;
|
||||
using Content.Shared.CCVar;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Asynchronous;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
|
||||
namespace Content.Server.Administration.Managers;
|
||||
|
||||
/// <summary>
|
||||
/// Handles kicking people that connect to multiple servers on the same DB at once.
|
||||
/// </summary>
|
||||
/// <seealso cref="CCVars.AdminAllowMultiServerPlay"/>
|
||||
public sealed class MultiServerKickManager
|
||||
{
|
||||
public const string NotificationChannel = "multi_server_kick";
|
||||
|
||||
[Dependency] private readonly IPlayerManager _playerManager = null!;
|
||||
[Dependency] private readonly IServerDbManager _dbManager = null!;
|
||||
[Dependency] private readonly ILogManager _logManager = null!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = null!;
|
||||
[Dependency] private readonly IAdminManager _adminManager = null!;
|
||||
[Dependency] private readonly ITaskManager _taskManager = null!;
|
||||
[Dependency] private readonly IServerNetManager _netManager = null!;
|
||||
[Dependency] private readonly ILocalizationManager _loc = null!;
|
||||
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = null!;
|
||||
|
||||
private ISawmill _sawmill = null!;
|
||||
private bool _allowed;
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
_sawmill = _logManager.GetSawmill("multi_server_kick");
|
||||
|
||||
_playerManager.PlayerStatusChanged += OnPlayerStatusChanged;
|
||||
_cfg.OnValueChanged(CCVars.AdminAllowMultiServerPlay, b => _allowed = b, true);
|
||||
|
||||
_dbManager.SubscribeToJsonNotification<NotificationData>(
|
||||
_taskManager,
|
||||
_sawmill,
|
||||
NotificationChannel,
|
||||
OnNotification,
|
||||
OnNotificationEarlyFilter
|
||||
);
|
||||
}
|
||||
|
||||
// ReSharper disable once AsyncVoidMethod
|
||||
private async void OnPlayerStatusChanged(object? sender, SessionStatusEventArgs e)
|
||||
{
|
||||
if (_allowed)
|
||||
return;
|
||||
|
||||
if (e.NewStatus != SessionStatus.InGame)
|
||||
return;
|
||||
|
||||
// Send notification to other servers so they can kick this player that just connected.
|
||||
try
|
||||
{
|
||||
await _dbManager.SendNotification(new DatabaseNotification
|
||||
{
|
||||
Channel = NotificationChannel,
|
||||
Payload = JsonSerializer.Serialize(new NotificationData
|
||||
{
|
||||
PlayerId = e.Session.UserId,
|
||||
ServerId = (await _serverDbEntry.ServerEntity).Id,
|
||||
}),
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_sawmill.Error($"Failed to send notification for multi server kick: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool OnNotificationEarlyFilter()
|
||||
{
|
||||
if (_allowed)
|
||||
{
|
||||
_sawmill.Verbose("Received notification for player join, but multi server play is allowed on this server. Ignoring");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// ReSharper disable once AsyncVoidMethod
|
||||
private async void OnNotification(NotificationData notification)
|
||||
{
|
||||
if (!_playerManager.TryGetSessionById(new NetUserId(notification.PlayerId), out var player))
|
||||
return;
|
||||
|
||||
if (notification.ServerId == (await _serverDbEntry.ServerEntity).Id)
|
||||
return;
|
||||
|
||||
if (_adminManager.IsAdmin(player, includeDeAdmin: true))
|
||||
return;
|
||||
|
||||
_sawmill.Info($"Kicking {player} for connecting to another server. Multi-server play is not allowed.");
|
||||
_netManager.DisconnectChannel(player.Channel, _loc.GetString("multi-server-kick-reason"));
|
||||
}
|
||||
|
||||
private sealed class NotificationData
|
||||
{
|
||||
[JsonPropertyName("player_id")]
|
||||
public Guid PlayerId { get; set; }
|
||||
|
||||
[JsonPropertyName("server_id")]
|
||||
public int ServerId { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -116,8 +116,11 @@ public sealed class TechAnomalySystem : EntitySystem
|
||||
|
||||
if (_random.Prob(tech.Comp.EmagSupercritProbability))
|
||||
{
|
||||
_emag.DoEmagEffect(tech, source);
|
||||
_emag.DoEmagEffect(tech, sink);
|
||||
var sourceEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
|
||||
RaiseLocalEvent(source, ref sourceEv);
|
||||
|
||||
var sinkEv = new GotEmaggedEvent(tech, EmagType.Access | EmagType.Interaction);
|
||||
RaiseLocalEvent(sink, ref sinkEv);
|
||||
}
|
||||
|
||||
CreateNewLink(tech, source, sink);
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class FireAlarmSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly AtmosDeviceNetworkSystem _atmosDevNet = default!;
|
||||
[Dependency] private readonly AtmosAlarmableSystem _atmosAlarmable = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _access = default!;
|
||||
[Dependency] private readonly IConfigurationManager _configManager = default!;
|
||||
@@ -77,11 +78,18 @@ public sealed class FireAlarmSystem : EntitySystem
|
||||
|
||||
private void OnEmagged(EntityUid uid, FireAlarmComponent component, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
|
||||
{
|
||||
// Remove the atmos alarmable component permanently from this device.
|
||||
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
|
||||
RemCompDeferred<AtmosAlarmableComponent>(uid);
|
||||
}
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (!TryComp<AtmosAlarmableComponent>(uid, out var alarmable))
|
||||
return;
|
||||
|
||||
// Remove the atmos alarmable component permanently from this device.
|
||||
_atmosAlarmable.ForceAlert(uid, AtmosAlarmType.Emagged, alarmable);
|
||||
RemCompDeferred<AtmosAlarmableComponent>(uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Binary.Components
|
||||
{
|
||||
@@ -38,6 +39,7 @@ namespace Content.Server.Atmos.Piping.Binary.Components
|
||||
public float LowerThreshold { get; set; } = 0.01f;
|
||||
|
||||
[DataField("higherThreshold")]
|
||||
[GuidebookData]
|
||||
public float HigherThreshold { get; set; } = DefaultHigherThreshold;
|
||||
public static readonly float DefaultHigherThreshold = 2 * Atmospherics.MaxOutputPressure;
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Trinary.Components
|
||||
{
|
||||
@@ -27,6 +28,7 @@ namespace Content.Server.Atmos.Piping.Trinary.Components
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("threshold")]
|
||||
[GuidebookData]
|
||||
public float Threshold { get; set; } = Atmospherics.OneAtmosphere;
|
||||
|
||||
[DataField("maxTransferRate")]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Containers.ItemSlots;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Audio;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
@@ -61,5 +62,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
|
||||
[DataField("accessDeniedSound")]
|
||||
public SoundSpecifier AccessDeniedSound = new SoundPathSpecifier("/Audio/Machines/custom_deny.ogg");
|
||||
|
||||
#region GuidebookData
|
||||
|
||||
[GuidebookData]
|
||||
public float Volume => Air.Volume;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Server.Atmos.Piping.Binary.Components;
|
||||
using Content.Server.Atmos.Piping.Unary.EntitySystems;
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
{
|
||||
@@ -29,6 +30,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
public float MaxTransferRate = Atmospherics.MaxTransferRate;
|
||||
|
||||
[DataField("maxPressure")]
|
||||
[GuidebookData]
|
||||
public float MaxPressure { get; set; } = GasVolumePumpComponent.DefaultHigherThreshold;
|
||||
|
||||
[DataField("inlet")]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
{
|
||||
@@ -13,6 +14,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// thermomachine to heat or cool air.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float HeatCapacity = 5000;
|
||||
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
@@ -21,6 +23,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// <summary>
|
||||
/// Tolerance for temperature setpoint hysteresis.
|
||||
/// </summary>
|
||||
[GuidebookData]
|
||||
[DataField, ViewVariables(VVAccess.ReadOnly)]
|
||||
public float TemperatureTolerance = 2f;
|
||||
|
||||
@@ -44,6 +47,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// Ignored if heater.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float MinTemperature = 73.15f;
|
||||
|
||||
/// <summary>
|
||||
@@ -51,6 +55,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// Ignored if freezer.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float MaxTemperature = 593.15f;
|
||||
|
||||
/// <summary>
|
||||
@@ -63,6 +68,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// An percentage of the energy change that is leaked into the surrounding environment rather than the inlet pipe.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float EnergyLeakPercentage;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping.Unary.Components;
|
||||
using Content.Shared.DeviceLinking;
|
||||
using Content.Shared.Guidebook;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
|
||||
|
||||
namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
@@ -35,6 +36,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// In releasing mode, do not pump when environment pressure is below this limit.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[GuidebookData]
|
||||
public float UnderPressureLockoutThreshold = 80; // this must be tuned in conjunction with atmos.mmos_spacing_speed
|
||||
|
||||
/// <summary>
|
||||
@@ -58,7 +60,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
[DataField]
|
||||
public bool IsPressureLockoutManuallyDisabled = false;
|
||||
/// <summary>
|
||||
/// The time when the manual pressure lockout will be reenabled.
|
||||
/// The time when the manual pressure lockout will be reenabled.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[AutoPausedField]
|
||||
@@ -101,6 +103,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
/// Max pressure of the target gas (NOT relative to source).
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[GuidebookData]
|
||||
public float MaxPressure = Atmospherics.MaxOutputPressure;
|
||||
|
||||
/// <summary>
|
||||
@@ -172,5 +175,12 @@ namespace Content.Server.Atmos.Piping.Unary.Components
|
||||
InternalPressureBound = data.InternalPressureBound;
|
||||
PressureLockoutOverride = data.PressureLockoutOverride;
|
||||
}
|
||||
|
||||
#region GuidebookData
|
||||
|
||||
[GuidebookData]
|
||||
public float DefaultExternalBound => Atmospherics.OneAtmosphere;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +109,15 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
|
||||
var item = canister.GasTankSlot.Item;
|
||||
_slots.TryEjectToHands(uid, canister.GasTankSlot, args.Actor);
|
||||
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
|
||||
|
||||
if (canister.ReleaseValve)
|
||||
{
|
||||
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.High, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister} while the valve was open, releasing [{GetContainedGasesString((uid, canister))}] to atmosphere");
|
||||
}
|
||||
else
|
||||
{
|
||||
_adminLogger.Add(LogType.CanisterTankEjected, LogImpact.Medium, $"Player {ToPrettyString(args.Actor):player} ejected tank {ToPrettyString(item):tank} from {ToPrettyString(uid):canister}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCanisterChangeReleasePressure(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleasePressureMessage args)
|
||||
@@ -124,24 +132,24 @@ public sealed class GasCanisterSystem : EntitySystem
|
||||
|
||||
private void OnCanisterChangeReleaseValve(EntityUid uid, GasCanisterComponent canister, GasCanisterChangeReleaseValveMessage args)
|
||||
{
|
||||
var impact = LogImpact.High;
|
||||
// filling a jetpack with plasma is less important than filling a room with it
|
||||
impact = canister.GasTankSlot.HasItem ? LogImpact.Medium : LogImpact.High;
|
||||
var hasItem = canister.GasTankSlot.HasItem;
|
||||
var impact = hasItem ? LogImpact.Medium : LogImpact.High;
|
||||
|
||||
var containedGasDict = new Dictionary<Gas, float>();
|
||||
var containedGasArray = Enum.GetValues(typeof(Gas));
|
||||
|
||||
for (int i = 0; i < containedGasArray.Length; i++)
|
||||
{
|
||||
containedGasDict.Add((Gas)i, canister.Air[i]);
|
||||
}
|
||||
|
||||
_adminLogger.Add(LogType.CanisterValve, impact, $"{ToPrettyString(args.Actor):player} set the valve on {ToPrettyString(uid):canister} to {args.Valve:valveState} while it contained [{string.Join(", ", containedGasDict)}]");
|
||||
_adminLogger.Add(
|
||||
LogType.CanisterValve,
|
||||
impact,
|
||||
$"{ToPrettyString(args.Actor):player} {(args.Valve ? "opened" : "closed")} the valve on {ToPrettyString(uid):canister} to {(hasItem ? "inserted tank" : "environment")} while it contained [{GetContainedGasesString((uid, canister))}]");
|
||||
|
||||
canister.ReleaseValve = args.Valve;
|
||||
DirtyUI(uid, canister);
|
||||
}
|
||||
|
||||
private static string GetContainedGasesString(Entity<GasCanisterComponent> canister)
|
||||
{
|
||||
return string.Join(", ", canister.Comp.Air);
|
||||
}
|
||||
|
||||
private void OnCanisterUpdated(EntityUid uid, GasCanisterComponent canister, ref AtmosDeviceUpdateEvent args)
|
||||
{
|
||||
_atmos.React(canister.Air, canister);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Portable
|
||||
{
|
||||
@@ -45,5 +46,12 @@ namespace Content.Server.Atmos.Portable
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public float TransferRate = 800;
|
||||
|
||||
#region GuidebookData
|
||||
|
||||
[GuidebookData]
|
||||
public float Volume => Air.Volume;
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using Content.Shared.Atmos;
|
||||
using Content.Shared.Atmos.Piping.Portable.Components;
|
||||
using Content.Shared.Atmos.Visuals;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Atmos.Portable;
|
||||
|
||||
@@ -23,12 +24,14 @@ public sealed partial class SpaceHeaterComponent : Component
|
||||
/// Maximum target temperature the device can be set to
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float MaxTemperature = Atmospherics.T20C + 20;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal target temperature the device can be set to
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float MinTemperature = Atmospherics.T0C - 10;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ namespace Content.Server.Bed
|
||||
{
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly ActionsSystem _actionsSystem = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly SleepingSystem _sleepingSystem = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
|
||||
@@ -114,7 +115,12 @@ namespace Content.Server.Bed
|
||||
|
||||
private void OnEmagged(EntityUid uid, StasisBedComponent component, ref GotEmaggedEvent args)
|
||||
{
|
||||
args.Repeatable = true;
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
// Reset any metabolisms first so they receive the multiplier correctly
|
||||
UpdateMetabolisms(uid, component, false);
|
||||
component.Multiplier = 1 / component.Multiplier;
|
||||
|
||||
@@ -12,15 +12,22 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
|
||||
/// <summary>
|
||||
/// Maximum amount of bounties a station can have.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public int MaxBounties = 6;
|
||||
|
||||
/// <summary>
|
||||
/// A list of all the bounties currently active for a station.
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField]
|
||||
public List<CargoBountyData> Bounties = new();
|
||||
|
||||
/// <summary>
|
||||
/// A list of all the bounties that have been completed or
|
||||
/// skipped for a station.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public List<CargoBountyHistoryData> History = new();
|
||||
|
||||
/// <summary>
|
||||
/// Used to determine unique order IDs
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,7 @@ using Content.Shared.Cargo;
|
||||
using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.NameIdentifier;
|
||||
using Content.Shared.Paper;
|
||||
using Content.Shared.Stacks;
|
||||
@@ -16,6 +17,7 @@ using JetBrains.Annotations;
|
||||
using Robust.Server.Containers;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Content.Server.Cargo.Systems;
|
||||
@@ -25,6 +27,7 @@ public sealed partial class CargoSystem
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
|
||||
[Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
|
||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||
|
||||
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
|
||||
private const string BountyNameIdentifierGroup = "Bounty";
|
||||
@@ -54,7 +57,7 @@ public sealed partial class CargoSystem
|
||||
return;
|
||||
|
||||
var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime;
|
||||
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, untilNextSkip));
|
||||
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip));
|
||||
}
|
||||
|
||||
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
|
||||
@@ -95,13 +98,13 @@ public sealed partial class CargoSystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryRemoveBounty(station, bounty.Value))
|
||||
if (!TryRemoveBounty(station, bounty.Value, true, args.Actor))
|
||||
return;
|
||||
|
||||
FillBountyDatabase(station);
|
||||
db.NextSkipTime = _timing.CurTime + db.SkipDelay;
|
||||
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
|
||||
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
|
||||
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
|
||||
_audio.PlayPvs(component.SkipSound, uid);
|
||||
}
|
||||
|
||||
@@ -179,7 +182,7 @@ public sealed partial class CargoSystem
|
||||
continue;
|
||||
}
|
||||
|
||||
TryRemoveBounty(station, bounty.Value);
|
||||
TryRemoveBounty(station, bounty.Value, false);
|
||||
FillBountyDatabase(station);
|
||||
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
|
||||
}
|
||||
@@ -434,24 +437,44 @@ public sealed partial class CargoSystem
|
||||
}
|
||||
|
||||
[PublicAPI]
|
||||
public bool TryRemoveBounty(EntityUid uid, string dataId, StationCargoBountyDatabaseComponent? component = null)
|
||||
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
|
||||
string dataId,
|
||||
bool skipped,
|
||||
EntityUid? actor = null)
|
||||
{
|
||||
if (!TryGetBountyFromId(uid, dataId, out var data, component))
|
||||
if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp))
|
||||
return false;
|
||||
|
||||
return TryRemoveBounty(uid, data.Value, component);
|
||||
return TryRemoveBounty(ent, data.Value, skipped, actor);
|
||||
}
|
||||
|
||||
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
|
||||
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
|
||||
CargoBountyData data,
|
||||
bool skipped,
|
||||
EntityUid? actor = null)
|
||||
{
|
||||
if (!Resolve(uid, ref component))
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return false;
|
||||
|
||||
for (var i = 0; i < component.Bounties.Count; i++)
|
||||
for (var i = 0; i < ent.Comp.Bounties.Count; i++)
|
||||
{
|
||||
if (component.Bounties[i].Id == data.Id)
|
||||
if (ent.Comp.Bounties[i].Id == data.Id)
|
||||
{
|
||||
component.Bounties.RemoveAt(i);
|
||||
string? actorName = null;
|
||||
if (actor != null)
|
||||
{
|
||||
var getIdentityEvent = new TryGetIdentityShortInfoEvent(ent.Owner, actor.Value);
|
||||
RaiseLocalEvent(getIdentityEvent);
|
||||
actorName = getIdentityEvent.Title;
|
||||
}
|
||||
|
||||
ent.Comp.History.Add(new CargoBountyHistoryData(data,
|
||||
skipped
|
||||
? CargoBountyHistoryData.BountyResult.Skipped
|
||||
: CargoBountyHistoryData.BountyResult.Completed,
|
||||
_gameTiming.CurTime,
|
||||
actorName));
|
||||
ent.Comp.Bounties.RemoveAt(i);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -492,7 +515,7 @@ public sealed partial class CargoSystem
|
||||
}
|
||||
|
||||
var untilNextSkip = db.NextSkipTime - _timing.CurTime;
|
||||
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip));
|
||||
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ using Content.Shared.Cargo.Components;
|
||||
using Content.Shared.Cargo.Events;
|
||||
using Content.Shared.Cargo.Prototypes;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Interaction;
|
||||
using Content.Shared.Paper;
|
||||
@@ -21,6 +21,7 @@ namespace Content.Server.Cargo.Systems
|
||||
public sealed partial class CargoSystem
|
||||
{
|
||||
[Dependency] private readonly SharedTransformSystem _transformSystem = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
/// <summary>
|
||||
/// How much time to wait (in seconds) before increasing bank accounts balance.
|
||||
@@ -41,6 +42,7 @@ namespace Content.Server.Cargo.Systems
|
||||
SubscribeLocalEvent<CargoOrderConsoleComponent, ComponentInit>(OnInit);
|
||||
SubscribeLocalEvent<CargoOrderConsoleComponent, InteractUsingEvent>(OnInteractUsing);
|
||||
SubscribeLocalEvent<CargoOrderConsoleComponent, BankBalanceUpdatedEvent>(OnOrderBalanceUpdated);
|
||||
SubscribeLocalEvent<CargoOrderConsoleComponent, GotEmaggedEvent>(OnEmagged);
|
||||
Reset();
|
||||
}
|
||||
|
||||
@@ -75,6 +77,17 @@ namespace Content.Server.Cargo.Systems
|
||||
_timer = 0;
|
||||
}
|
||||
|
||||
private void OnEmagged(Entity<CargoOrderConsoleComponent> ent, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(ent, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
private void UpdateConsole(float frameTime)
|
||||
{
|
||||
_timer += frameTime;
|
||||
@@ -85,9 +98,11 @@ namespace Content.Server.Cargo.Systems
|
||||
{
|
||||
_timer -= Delay;
|
||||
|
||||
foreach (var account in EntityQuery<StationBankAccountComponent>())
|
||||
var stationQuery = EntityQueryEnumerator<StationBankAccountComponent>();
|
||||
while (stationQuery.MoveNext(out var uid, out var bank))
|
||||
{
|
||||
account.Balance += account.IncreasePerSecond * Delay;
|
||||
var balanceToAdd = bank.IncreasePerSecond * Delay;
|
||||
UpdateBankAccount(uid, bank, balanceToAdd);
|
||||
}
|
||||
|
||||
var query = EntityQueryEnumerator<CargoOrderConsoleComponent>();
|
||||
@@ -192,7 +207,7 @@ namespace Content.Server.Cargo.Systems
|
||||
order.Approved = true;
|
||||
_audio.PlayPvs(component.ConfirmSound, uid);
|
||||
|
||||
if (!HasComp<EmaggedComponent>(uid))
|
||||
if (!_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(uid, player);
|
||||
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
|
||||
@@ -213,7 +228,7 @@ namespace Content.Server.Cargo.Systems
|
||||
$"{ToPrettyString(player):user} approved order [orderId:{order.OrderId}, quantity:{order.OrderQuantity}, product:{order.ProductId}, requester:{order.Requester}, reason:{order.Reason}] with balance at {bank.Balance}");
|
||||
|
||||
orderDatabase.Orders.Remove(order);
|
||||
DeductFunds(bank, cost);
|
||||
UpdateBankAccount(station.Value, bank, -cost);
|
||||
UpdateOrders(station.Value);
|
||||
}
|
||||
|
||||
@@ -536,11 +551,6 @@ namespace Content.Server.Cargo.Systems
|
||||
|
||||
}
|
||||
|
||||
private void DeductFunds(StationBankAccountComponent component, int amount)
|
||||
{
|
||||
component.Balance = Math.Max(0, component.Balance - amount);
|
||||
}
|
||||
|
||||
#region Station
|
||||
|
||||
private bool TryGetOrderDatabase([NotNullWhen(true)] EntityUid? stationUid, [MaybeNullWhen(false)] out StationCargoOrderDatabaseComponent dbComp)
|
||||
|
||||
@@ -60,6 +60,7 @@ namespace Content.Server.Cloning
|
||||
[Dependency] private readonly SharedMindSystem _mindSystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly SharedJobSystem _jobs = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
public readonly Dictionary<MindComponent, EntityUid> ClonesWaitingForMind = new();
|
||||
public const float EasyModeCloningCost = 0.7f;
|
||||
@@ -276,10 +277,15 @@ namespace Content.Server.Cloning
|
||||
/// </summary>
|
||||
private void OnEmagged(EntityUid uid, CloningPodComponent clonePod, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (!this.IsPowered(uid, EntityManager))
|
||||
return;
|
||||
|
||||
_audio.PlayPvs(clonePod.SparkSound, uid);
|
||||
_popupSystem.PopupEntity(Loc.GetString("cloning-pod-component-upgrade-emag-requirement"), uid);
|
||||
args.Handled = true;
|
||||
}
|
||||
@@ -309,7 +315,7 @@ namespace Content.Server.Cloning
|
||||
var indices = _transformSystem.GetGridTilePositionOrDefault((uid, transform));
|
||||
var tileMix = _atmosphereSystem.GetTileMixture(transform.GridUid, null, indices, true);
|
||||
|
||||
if (HasComp<EmaggedComponent>(uid))
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_audio.PlayPvs(clonePod.ScreamSound, uid);
|
||||
Spawn(clonePod.MobSpawnId, transform.Coordinates);
|
||||
@@ -327,7 +333,7 @@ namespace Content.Server.Cloning
|
||||
}
|
||||
_puddleSystem.TrySpillAt(uid, bloodSolution, out _);
|
||||
|
||||
if (!HasComp<EmaggedComponent>(uid))
|
||||
if (!_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
{
|
||||
_material.SpawnMultipleFromMaterial(_robustRandom.Next(1, (int) (clonePod.UsedBiomass / 2.5)), clonePod.RequiredMaterial, Transform(uid).Coordinates);
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ using Content.Shared.Chat;
|
||||
using Content.Shared.Communications;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.DeviceNetwork;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Popups;
|
||||
using Robust.Server.GameObjects;
|
||||
@@ -177,7 +176,7 @@ namespace Content.Server.Communications
|
||||
|
||||
private bool CanUse(EntityUid user, EntityUid console)
|
||||
{
|
||||
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent) && !HasComp<EmaggedComponent>(console))
|
||||
if (TryComp<AccessReaderComponent>(console, out var accessReaderComponent))
|
||||
{
|
||||
return _accessReaderSystem.IsAllowed(user, console, accessReaderComponent);
|
||||
}
|
||||
|
||||
@@ -89,6 +89,8 @@ public sealed class FixRotationsCommand : IConsoleCommand
|
||||
valid |= tagSystem.HasTag(child, "ForceFixRotations");
|
||||
// override
|
||||
valid &= !tagSystem.HasTag(child, "ForceNoFixRotations");
|
||||
// remove diagonal entities as well
|
||||
valid &= !tagSystem.HasTag(child, "Diagonal");
|
||||
|
||||
if (!valid)
|
||||
continue;
|
||||
|
||||
@@ -13,6 +13,8 @@ using Robust.Server.GameObjects;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Content.Shared.IdentityManagement;
|
||||
using Content.Shared.Security.Components;
|
||||
using System.Linq;
|
||||
using Content.Shared.Roles.Jobs;
|
||||
|
||||
namespace Content.Server.CriminalRecords.Systems;
|
||||
|
||||
@@ -42,6 +44,7 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
||||
subs.Event<CriminalRecordChangeStatus>(OnChangeStatus);
|
||||
subs.Event<CriminalRecordAddHistory>(OnAddHistory);
|
||||
subs.Event<CriminalRecordDeleteHistory>(OnDeleteHistory);
|
||||
subs.Event<CriminalRecordSetStatusFilter>(OnStatusFilterPressed);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -57,6 +60,11 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
||||
ent.Comp.ActiveKey = msg.SelectedKey;
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
private void OnStatusFilterPressed(Entity<CriminalRecordsConsoleComponent> ent, ref CriminalRecordSetStatusFilter msg)
|
||||
{
|
||||
ent.Comp.FilterStatus = msg.FilterStatus;
|
||||
UpdateUserInterface(ent);
|
||||
}
|
||||
|
||||
private void OnFiltersChanged(Entity<CriminalRecordsConsoleComponent> ent, ref SetStationRecordFilter msg)
|
||||
{
|
||||
@@ -112,13 +120,26 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
||||
}
|
||||
|
||||
// will probably never fail given the checks above
|
||||
name = _records.RecordName(key.Value);
|
||||
officer = Loc.GetString("criminal-records-console-unknown-officer");
|
||||
var jobName = "Unknown";
|
||||
|
||||
_records.TryGetRecord<GeneralStationRecord>(key.Value, out var entry);
|
||||
if (entry != null)
|
||||
jobName = entry.JobTitle;
|
||||
|
||||
var tryGetIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(null, mob.Value);
|
||||
RaiseLocalEvent(tryGetIdentityShortInfoEvent);
|
||||
if (tryGetIdentityShortInfoEvent.Title != null)
|
||||
officer = tryGetIdentityShortInfoEvent.Title;
|
||||
|
||||
_criminalRecords.TryChangeStatus(key.Value, msg.Status, msg.Reason, officer);
|
||||
|
||||
(string, object)[] args;
|
||||
if (reason != null)
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason) };
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer), ("reason", reason), ("job", jobName) };
|
||||
else
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer) };
|
||||
args = new (string, object)[] { ("name", name), ("officer", officer), ("job", jobName) };
|
||||
|
||||
// figure out which radio message to send depending on transition
|
||||
var statusString = (oldStatus, msg.Status) switch
|
||||
@@ -193,8 +214,18 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
||||
return;
|
||||
}
|
||||
|
||||
// get the listing of records to display
|
||||
var listing = _records.BuildListing((owningStation.Value, stationRecords), console.Filter);
|
||||
|
||||
// filter the listing by the selected criminal record status
|
||||
//if NONE, dont filter by status, just show all crew
|
||||
if (console.FilterStatus != SecurityStatus.None)
|
||||
{
|
||||
listing = listing
|
||||
.Where(x => _records.TryGetRecord<CriminalRecord>(new StationRecordKey(x.Key, owningStation.Value), out var record) && record.Status == console.FilterStatus)
|
||||
.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
|
||||
var state = new CriminalRecordsConsoleState(listing, console.Filter);
|
||||
if (console.ActiveKey is { } id)
|
||||
{
|
||||
@@ -205,6 +236,9 @@ public sealed class CriminalRecordsConsoleSystem : SharedCriminalRecordsConsoleS
|
||||
state.SelectedKey = id;
|
||||
}
|
||||
|
||||
// Set the Current Tab aka the filter status type for the records list
|
||||
state.FilterStatus = console.FilterStatus;
|
||||
|
||||
_ui.SetUiState(uid, CriminalRecordsConsoleKey.Key, state);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Shared.Bed.Sleep;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Damage.Events;
|
||||
using Content.Shared.Damage.ForceSay;
|
||||
using Content.Shared.FixedPoint;
|
||||
using Content.Shared.Mobs;
|
||||
@@ -47,7 +48,7 @@ public sealed class DamageForceSaySystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true, string? suffixOverride = null)
|
||||
private void TryForceSay(EntityUid uid, DamageForceSayComponent component, bool useSuffix=true)
|
||||
{
|
||||
if (!TryComp<ActorComponent>(uid, out var actor))
|
||||
return;
|
||||
@@ -57,7 +58,13 @@ public sealed class DamageForceSaySystem : EntitySystem
|
||||
_timing.CurTime < component.NextAllowedTime)
|
||||
return;
|
||||
|
||||
var suffix = Loc.GetString(suffixOverride ?? component.ForceSayStringPrefix + _random.Next(1, component.ForceSayStringCount));
|
||||
var ev = new BeforeForceSayEvent(component.ForceSayStringDataset);
|
||||
RaiseLocalEvent(uid, ev);
|
||||
|
||||
if (!_prototype.TryIndex(ev.Prefix, out var prefixList))
|
||||
return;
|
||||
|
||||
var suffix = Loc.GetString(_random.Pick(prefixList.Values));
|
||||
|
||||
// set cooldown & raise event
|
||||
component.NextAllowedTime = _timing.CurTime + component.Cooldown;
|
||||
@@ -80,7 +87,7 @@ public sealed class DamageForceSaySystem : EntitySystem
|
||||
if (!args.FellAsleep)
|
||||
return;
|
||||
|
||||
TryForceSay(uid, component, true, "damage-force-say-sleep");
|
||||
TryForceSay(uid, component);
|
||||
AllowNextSpeech(uid);
|
||||
}
|
||||
|
||||
|
||||
@@ -1112,7 +1112,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
.SingleOrDefaultAsync());
|
||||
}
|
||||
|
||||
public async Task SetLastReadRules(NetUserId player, DateTimeOffset date)
|
||||
public async Task SetLastReadRules(NetUserId player, DateTimeOffset? date)
|
||||
{
|
||||
await using var db = await GetDb();
|
||||
|
||||
@@ -1122,7 +1122,7 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
return;
|
||||
}
|
||||
|
||||
dbPlayer.LastReadRules = date.UtcDateTime;
|
||||
dbPlayer.LastReadRules = date?.UtcDateTime;
|
||||
await db.DbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
@@ -1801,6 +1801,8 @@ INSERT INTO player_round (players_id, rounds_id) VALUES ({players[player]}, {id}
|
||||
|
||||
#endregion
|
||||
|
||||
public abstract Task SendNotification(DatabaseNotification notification);
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -282,7 +282,7 @@ namespace Content.Server.Database
|
||||
#region Rules
|
||||
|
||||
Task<DateTimeOffset?> GetLastReadRules(NetUserId player);
|
||||
Task SetLastReadRules(NetUserId player, DateTimeOffset time);
|
||||
Task SetLastReadRules(NetUserId player, DateTimeOffset? time);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -350,6 +350,15 @@ namespace Content.Server.Database
|
||||
/// <param name="notification">The notification to trigger</param>
|
||||
void InjectTestNotification(DatabaseNotification notification);
|
||||
|
||||
/// <summary>
|
||||
/// Send a notification to all other servers connected to the same database.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The local server will receive the sent notification itself again.
|
||||
/// </remarks>
|
||||
/// <param name="notification">The notification to send.</param>
|
||||
Task SendNotification(DatabaseNotification notification);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -821,7 +830,7 @@ namespace Content.Server.Database
|
||||
return RunDbCommand(() => _db.GetLastReadRules(player));
|
||||
}
|
||||
|
||||
public Task SetLastReadRules(NetUserId player, DateTimeOffset time)
|
||||
public Task SetLastReadRules(NetUserId player, DateTimeOffset? time)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.SetLastReadRules(player, time));
|
||||
@@ -1045,6 +1054,12 @@ namespace Content.Server.Database
|
||||
HandleDatabaseNotification(notification);
|
||||
}
|
||||
|
||||
public Task SendNotification(DatabaseNotification notification)
|
||||
{
|
||||
DbWriteOpsMetric.Inc();
|
||||
return RunDbCommand(() => _db.SendNotification(notification));
|
||||
}
|
||||
|
||||
private async void HandleDatabaseNotification(DatabaseNotification notification)
|
||||
{
|
||||
lock (_notificationHandlers)
|
||||
|
||||
76
Content.Server/Database/ServerDbManagerExt.cs
Normal file
76
Content.Server/Database/ServerDbManagerExt.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using System.Text.Json;
|
||||
using Robust.Shared.Asynchronous;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
|
||||
public static class ServerDbManagerExt
|
||||
{
|
||||
/// <summary>
|
||||
/// Subscribe to a database notification on a specific channel, formatted as JSON.
|
||||
/// </summary>
|
||||
/// <param name="dbManager">The database manager to subscribe on.</param>
|
||||
/// <param name="taskManager">The task manager used to run the main callback on the main thread.</param>
|
||||
/// <param name="sawmill">Sawmill to log any errors to.</param>
|
||||
/// <param name="channel">
|
||||
/// The notification channel to listen on. Only notifications on this channel will be handled.
|
||||
/// </param>
|
||||
/// <param name="action">
|
||||
/// The action to run on the notification data.
|
||||
/// This runs on the main thread.
|
||||
/// </param>
|
||||
/// <param name="earlyFilter">
|
||||
/// An early filter callback that runs before the JSON message is deserialized.
|
||||
/// Return false to not handle the notification.
|
||||
/// This does not run on the main thread.
|
||||
/// </param>
|
||||
/// <param name="filter">
|
||||
/// A filter callback that runs after the JSON message is deserialized.
|
||||
/// Return false to not handle the notification.
|
||||
/// This does not run on the main thread.
|
||||
/// </param>
|
||||
/// <typeparam name="TData">The type of JSON data to deserialize.</typeparam>
|
||||
public static void SubscribeToJsonNotification<TData>(
|
||||
this IServerDbManager dbManager,
|
||||
ITaskManager taskManager,
|
||||
ISawmill sawmill,
|
||||
string channel,
|
||||
Action<TData> action,
|
||||
Func<bool>? earlyFilter = null,
|
||||
Func<TData, bool>? filter = null)
|
||||
{
|
||||
dbManager.SubscribeToNotifications(notification =>
|
||||
{
|
||||
if (notification.Channel != channel)
|
||||
return;
|
||||
|
||||
if (notification.Payload == null)
|
||||
{
|
||||
sawmill.Error($"Got {channel} notification with null payload!");
|
||||
return;
|
||||
}
|
||||
|
||||
if (earlyFilter != null && !earlyFilter())
|
||||
return;
|
||||
|
||||
TData data;
|
||||
try
|
||||
{
|
||||
data = JsonSerializer.Deserialize<TData>(notification.Payload)
|
||||
?? throw new JsonException("Content is null");
|
||||
}
|
||||
catch (JsonException e)
|
||||
{
|
||||
sawmill.Error($"Got invalid JSON in {channel} notification: {e}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (filter != null && !filter(data))
|
||||
return;
|
||||
|
||||
taskManager.RunOnMainThread(() =>
|
||||
{
|
||||
action(data);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.Administration.Managers;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Npgsql;
|
||||
|
||||
namespace Content.Server.Database;
|
||||
@@ -17,6 +18,7 @@ public sealed partial class ServerDbPostgres
|
||||
private static readonly string[] NotificationChannels =
|
||||
[
|
||||
BanManager.BanNotificationChannel,
|
||||
MultiServerKickManager.NotificationChannel,
|
||||
];
|
||||
|
||||
private static readonly TimeSpan ReconnectWaitIncrease = TimeSpan.FromSeconds(10);
|
||||
@@ -111,6 +113,14 @@ public sealed partial class ServerDbPostgres
|
||||
});
|
||||
}
|
||||
|
||||
public override async Task SendNotification(DatabaseNotification notification)
|
||||
{
|
||||
await using var db = await GetDbImpl();
|
||||
|
||||
await db.PgDbContext.Database.ExecuteSqlAsync(
|
||||
$"SELECT pg_notify({notification.Channel}, {notification.Payload})");
|
||||
}
|
||||
|
||||
public override void Shutdown()
|
||||
{
|
||||
_notificationTokenSource.Cancel();
|
||||
|
||||
@@ -537,6 +537,12 @@ namespace Content.Server.Database
|
||||
return await base.AddAdminMessage(message);
|
||||
}
|
||||
|
||||
public override Task SendNotification(DatabaseNotification notification)
|
||||
{
|
||||
// Notifications not implemented on SQLite.
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override DateTime NormalizeDatabaseTime(DateTime time)
|
||||
{
|
||||
DebugTools.Assert(time.Kind == DateTimeKind.Unspecified);
|
||||
|
||||
@@ -15,7 +15,6 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
|
||||
/// The type of explosion. Determines damage types and tile break chance scaling.
|
||||
/// </summary>
|
||||
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
|
||||
[JsonIgnore]
|
||||
public string ExplosionType = default!;
|
||||
|
||||
/// <summary>
|
||||
@@ -23,14 +22,12 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
|
||||
/// chance.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[JsonIgnore]
|
||||
public float MaxIntensity = 5;
|
||||
|
||||
/// <summary>
|
||||
/// How quickly intensity drops off as you move away from the epicenter
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[JsonIgnore]
|
||||
public float IntensitySlope = 1;
|
||||
|
||||
/// <summary>
|
||||
@@ -41,15 +38,20 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
|
||||
/// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles.
|
||||
/// </remarks>
|
||||
[DataField]
|
||||
[JsonIgnore]
|
||||
public float MaxTotalIntensity = 100;
|
||||
|
||||
/// <summary>
|
||||
/// The intensity of the explosion per unit reaction.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[JsonIgnore]
|
||||
public float IntensityPerUnit = 1;
|
||||
|
||||
/// <summary>
|
||||
/// Factor used to scale the explosion intensity when calculating tile break chances. Allows for stronger
|
||||
/// explosives that don't space tiles, without having to create a new explosion-type prototype.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public float TileBreakScale = 1f;
|
||||
|
||||
public override bool ShouldLog => true;
|
||||
|
||||
@@ -72,6 +74,7 @@ public sealed partial class ExplosionReactionEffect : EntityEffect
|
||||
ExplosionType,
|
||||
intensity,
|
||||
IntensitySlope,
|
||||
MaxIntensity);
|
||||
MaxIntensity,
|
||||
TileBreakScale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ namespace Content.Server.Entry
|
||||
IoCManager.Resolve<IEntitySystemManager>().GetEntitySystem<GameTicker>().PostInitialize();
|
||||
IoCManager.Resolve<IBanManager>().Initialize();
|
||||
IoCManager.Resolve<IConnectionManager>().PostInit();
|
||||
IoCManager.Resolve<MultiServerKickManager>().Initialize();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ public sealed class FaxSystem : EntitySystem
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaData = default!;
|
||||
[Dependency] private readonly FaxecuteSystem _faxecute = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
|
||||
private const string PaperSlotId = "Paper";
|
||||
|
||||
@@ -227,7 +228,7 @@ public sealed class FaxSystem : EntitySystem
|
||||
return;
|
||||
}
|
||||
|
||||
if (component.KnownFaxes.ContainsValue(newName) && !HasComp<EmaggedComponent>(uid)) // Allow existing names if emagged for fun
|
||||
if (component.KnownFaxes.ContainsValue(newName) && !_emag.CheckFlag(uid, EmagType.Interaction)) // Allow existing names if emagged for fun
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("fax-machine-popup-name-exist"), uid);
|
||||
return;
|
||||
@@ -246,7 +247,12 @@ public sealed class FaxSystem : EntitySystem
|
||||
|
||||
private void OnEmagged(EntityUid uid, FaxMachineComponent component, ref GotEmaggedEvent args)
|
||||
{
|
||||
_audioSystem.PlayPvs(component.EmagSound, uid);
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
@@ -260,7 +266,7 @@ public sealed class FaxSystem : EntitySystem
|
||||
switch (command)
|
||||
{
|
||||
case FaxConstants.FaxPingCommand:
|
||||
var isForSyndie = HasComp<EmaggedComponent>(uid) &&
|
||||
var isForSyndie = _emag.CheckFlag(uid, EmagType.Interaction) &&
|
||||
args.Data.ContainsKey(FaxConstants.FaxSyndicateData);
|
||||
if (!isForSyndie && !component.ResponsePings)
|
||||
return;
|
||||
@@ -405,7 +411,7 @@ public sealed class FaxSystem : EntitySystem
|
||||
{ DeviceNetworkConstants.Command, FaxConstants.FaxPingCommand }
|
||||
};
|
||||
|
||||
if (HasComp<EmaggedComponent>(uid))
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
payload.Add(FaxConstants.FaxSyndicateData, true);
|
||||
|
||||
_deviceNetworkSystem.QueuePacket(uid, null, payload);
|
||||
|
||||
@@ -15,6 +15,7 @@ using Content.Shared.Telephone;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Server.GameObjects;
|
||||
using Robust.Server.GameStates;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Timing;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -35,22 +36,15 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
[Dependency] private readonly ChatSystem _chatSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly PvsOverrideSystem _pvs = default!;
|
||||
|
||||
private float _updateTimer = 1.0f;
|
||||
|
||||
private const float UpdateTime = 1.0f;
|
||||
private const float MinTimeBetweenSyncRequests = 0.5f;
|
||||
private TimeSpan _minTimeSpanBetweenSyncRequests;
|
||||
|
||||
private HashSet<EntityUid> _pendingRequestsForSpriteState = new();
|
||||
private HashSet<EntityUid> _recentlyUpdatedHolograms = new();
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
_minTimeSpanBetweenSyncRequests = TimeSpan.FromSeconds(MinTimeBetweenSyncRequests);
|
||||
|
||||
// Holopad UI and bound user interface messages
|
||||
SubscribeLocalEvent<HolopadComponent, BeforeActivatableUIOpenEvent>(OnUIOpen);
|
||||
SubscribeLocalEvent<HolopadComponent, HolopadStartNewCallMessage>(OnHolopadStartNewCall);
|
||||
@@ -68,7 +62,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
|
||||
// Networked events
|
||||
SubscribeNetworkEvent<HolopadUserTypingChangedEvent>(OnTypingChanged);
|
||||
SubscribeNetworkEvent<PlayerSpriteStateMessage>(OnPlayerSpriteStateMessage);
|
||||
|
||||
// Component start/shutdown events
|
||||
SubscribeLocalEvent<HolopadComponent, ComponentInit>(OnHolopadInit);
|
||||
@@ -268,16 +261,11 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
if (source.Comp.Hologram == null)
|
||||
GenerateHologram(source);
|
||||
|
||||
// Receiver holopad holograms have to be generated now instead of waiting for their own event
|
||||
// to fire because holographic avatars get synced immediately
|
||||
if (TryComp<HolopadComponent>(args.Receiver, out var receivingHolopad) && receivingHolopad.Hologram == null)
|
||||
GenerateHologram((args.Receiver, receivingHolopad));
|
||||
|
||||
if (source.Comp.User != null)
|
||||
{
|
||||
// Re-link the user to refresh the sprite data
|
||||
LinkHolopadToUser(source, source.Comp.User.Value);
|
||||
}
|
||||
// Re-link the user to refresh the sprite data
|
||||
LinkHolopadToUser(source, source.Comp.User);
|
||||
}
|
||||
|
||||
private void OnHoloCallEnded(Entity<HolopadComponent> entity, ref TelephoneCallEndedEvent args)
|
||||
@@ -323,22 +311,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlayerSpriteStateMessage(PlayerSpriteStateMessage ev, EntitySessionEventArgs args)
|
||||
{
|
||||
var uid = args.SenderSession.AttachedEntity;
|
||||
|
||||
if (!Exists(uid))
|
||||
return;
|
||||
|
||||
if (!_pendingRequestsForSpriteState.Remove(uid.Value))
|
||||
return;
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(uid, out var holopadUser))
|
||||
return;
|
||||
|
||||
SyncHolopadUserWithLinkedHolograms((uid.Value, holopadUser), ev.SpriteLayerData);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region: Component start/shutdown events
|
||||
@@ -485,8 +457,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_recentlyUpdatedHolograms.Clear();
|
||||
}
|
||||
|
||||
public void UpdateUIState(Entity<HolopadComponent> entity, TelephoneComponent? telephone = null)
|
||||
@@ -555,10 +525,16 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
QueueDel(hologram);
|
||||
}
|
||||
|
||||
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid user)
|
||||
private void LinkHolopadToUser(Entity<HolopadComponent> entity, EntityUid? user)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
UnlinkHolopadFromUser(entity, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<HolopadUserComponent>(user, out var holopadUser))
|
||||
holopadUser = AddComp<HolopadUserComponent>(user);
|
||||
holopadUser = AddComp<HolopadUserComponent>(user.Value);
|
||||
|
||||
if (user != entity.Comp.User?.Owner)
|
||||
{
|
||||
@@ -567,51 +543,44 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
|
||||
// Assigns the new user in their place
|
||||
holopadUser.LinkedHolopads.Add(entity);
|
||||
entity.Comp.User = (user, holopadUser);
|
||||
entity.Comp.User = (user.Value, holopadUser);
|
||||
}
|
||||
|
||||
if (TryComp<HolographicAvatarComponent>(user, out var avatar))
|
||||
{
|
||||
SyncHolopadUserWithLinkedHolograms((user, holopadUser), avatar.LayerData);
|
||||
return;
|
||||
}
|
||||
|
||||
// We have no apriori sprite data for the hologram, request
|
||||
// the current appearance of the user from the client
|
||||
RequestHolopadUserSpriteUpdate((user, holopadUser));
|
||||
// Add the new user to PVS and sync their appearance with any
|
||||
// holopads connected to the one they are using
|
||||
_pvs.AddGlobalOverride(user.Value);
|
||||
SyncHolopadHologramAppearanceWithTarget(entity, entity.Comp.User);
|
||||
}
|
||||
|
||||
private void UnlinkHolopadFromUser(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
|
||||
{
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
entity.Comp.User = null;
|
||||
SyncHolopadHologramAppearanceWithTarget(entity, null);
|
||||
|
||||
foreach (var linkedHolopad in GetLinkedHolopads(entity))
|
||||
{
|
||||
if (linkedHolopad.Comp.Hologram != null)
|
||||
{
|
||||
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
|
||||
|
||||
// Send message with no sprite data to the client
|
||||
// This will set the holgram sprite to a generic icon
|
||||
var ev = new PlayerSpriteStateMessage(GetNetEntity(linkedHolopad.Comp.Hologram.Value));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
|
||||
if (!HasComp<HolopadUserComponent>(user))
|
||||
if (user == null)
|
||||
return;
|
||||
|
||||
user.Value.Comp.LinkedHolopads.Remove(entity);
|
||||
|
||||
if (!user.Value.Comp.LinkedHolopads.Any())
|
||||
if (!user.Value.Comp.LinkedHolopads.Any() &&
|
||||
user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
|
||||
{
|
||||
_pendingRequestsForSpriteState.Remove(user.Value);
|
||||
_pvs.RemoveGlobalOverride(user.Value);
|
||||
RemComp<HolopadUserComponent>(user.Value);
|
||||
}
|
||||
}
|
||||
private void SyncHolopadHologramAppearanceWithTarget(Entity<HolopadComponent> entity, Entity<HolopadUserComponent>? user)
|
||||
{
|
||||
foreach (var linkedHolopad in GetLinkedHolopads(entity))
|
||||
{
|
||||
if (linkedHolopad.Comp.Hologram == null)
|
||||
continue;
|
||||
|
||||
if (user.Value.Comp.LifeStage < ComponentLifeStage.Stopping)
|
||||
RemComp<HolopadUserComponent>(user.Value);
|
||||
if (user == null)
|
||||
_appearanceSystem.SetData(linkedHolopad.Comp.Hologram.Value.Owner, TypingIndicatorVisuals.IsTyping, false);
|
||||
|
||||
linkedHolopad.Comp.Hologram.Value.Comp.LinkedEntity = user;
|
||||
Dirty(linkedHolopad.Comp.Hologram.Value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,31 +615,6 @@ public sealed class HolopadSystem : SharedHolopadSystem
|
||||
Dirty(entity);
|
||||
}
|
||||
|
||||
private void RequestHolopadUserSpriteUpdate(Entity<HolopadUserComponent> user)
|
||||
{
|
||||
if (!_pendingRequestsForSpriteState.Add(user))
|
||||
return;
|
||||
|
||||
var ev = new PlayerSpriteStateRequest(GetNetEntity(user));
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
|
||||
private void SyncHolopadUserWithLinkedHolograms(Entity<HolopadUserComponent> entity, PrototypeLayerData[]? spriteLayerData)
|
||||
{
|
||||
foreach (var linkedHolopad in entity.Comp.LinkedHolopads)
|
||||
{
|
||||
foreach (var receivingHolopad in GetLinkedHolopads(linkedHolopad))
|
||||
{
|
||||
if (receivingHolopad.Comp.Hologram == null || !_recentlyUpdatedHolograms.Add(receivingHolopad.Comp.Hologram.Value))
|
||||
continue;
|
||||
|
||||
var netHologram = GetNetEntity(receivingHolopad.Comp.Hologram.Value);
|
||||
var ev = new PlayerSpriteStateMessage(netHologram, spriteLayerData);
|
||||
RaiseNetworkEvent(ev);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ActivateProjector(Entity<HolopadComponent> entity, EntityUid user)
|
||||
{
|
||||
if (!TryComp<TelephoneComponent>(entity, out var receiverTelephone))
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Shared.Implants.Components;
|
||||
using Content.Server.Body.Components;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Implants;
|
||||
@@ -9,6 +10,7 @@ public sealed partial class ImplanterSystem
|
||||
{
|
||||
SubscribeLocalEvent<ImplantedComponent, ComponentInit>(OnImplantedInit);
|
||||
SubscribeLocalEvent<ImplantedComponent, ComponentShutdown>(OnShutdown);
|
||||
SubscribeLocalEvent<ImplantedComponent, BeingGibbedEvent>(OnGibbed);
|
||||
}
|
||||
|
||||
private void OnImplantedInit(EntityUid uid, ImplantedComponent component, ComponentInit args)
|
||||
@@ -22,4 +24,10 @@ public sealed partial class ImplanterSystem
|
||||
//If the entity is deleted, get rid of the implants
|
||||
_container.CleanContainer(component.ImplantContainer);
|
||||
}
|
||||
|
||||
private void OnGibbed(Entity<ImplantedComponent> ent, ref BeingGibbedEvent args)
|
||||
{
|
||||
//If the entity is gibbed, get rid of the implants
|
||||
_container.CleanContainer(ent.Comp.ImplantContainer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,17 +58,6 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we are trying to implant a implant which is already implanted
|
||||
if (implant.HasValue && !component.AllowMultipleImplants && CheckSameImplant(target, implant.Value))
|
||||
{
|
||||
var name = Identity.Name(target, EntityManager, args.User);
|
||||
var msg = Loc.GetString("implanter-component-implant-already", ("implant", implant), ("target", name));
|
||||
_popup.PopupEntity(msg, target, args.User);
|
||||
args.Handled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
//Implant self instantly, otherwise try to inject the target.
|
||||
if (args.User == target)
|
||||
Implant(target, target, uid, component);
|
||||
@@ -79,14 +68,7 @@ public sealed partial class ImplanterSystem : SharedImplanterSystem
|
||||
args.Handled = true;
|
||||
}
|
||||
|
||||
public bool CheckSameImplant(EntityUid target, EntityUid implant)
|
||||
{
|
||||
if (!TryComp<ImplantedComponent>(target, out var implanted))
|
||||
return false;
|
||||
|
||||
var implantPrototype = Prototype(implant);
|
||||
return implanted.ImplantContainer.ContainedEntities.Any(entity => Prototype(entity) == implantPrototype);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to implant someone else.
|
||||
|
||||
76
Content.Server/Implants/RadioImplantSystem.cs
Normal file
76
Content.Server/Implants/RadioImplantSystem.cs
Normal file
@@ -0,0 +1,76 @@
|
||||
using Content.Server.Radio.Components;
|
||||
using Content.Shared.Implants;
|
||||
using Content.Shared.Implants.Components;
|
||||
using Robust.Shared.Containers;
|
||||
|
||||
namespace Content.Server.Implants;
|
||||
|
||||
public sealed class RadioImplantSystem : EntitySystem
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
base.Initialize();
|
||||
|
||||
SubscribeLocalEvent<RadioImplantComponent, ImplantImplantedEvent>(OnImplantImplanted);
|
||||
SubscribeLocalEvent<RadioImplantComponent, EntGotRemovedFromContainerMessage>(OnRemove);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// If implanted with a radio implant, installs the necessary intrinsic radio components
|
||||
/// </summary>
|
||||
private void OnImplantImplanted(Entity<RadioImplantComponent> ent, ref ImplantImplantedEvent args)
|
||||
{
|
||||
if (args.Implanted == null)
|
||||
return;
|
||||
|
||||
var activeRadio = EnsureComp<ActiveRadioComponent>(args.Implanted.Value);
|
||||
foreach (var channel in ent.Comp.RadioChannels)
|
||||
{
|
||||
if (activeRadio.Channels.Add(channel))
|
||||
ent.Comp.ActiveAddedChannels.Add(channel);
|
||||
}
|
||||
|
||||
EnsureComp<IntrinsicRadioReceiverComponent>(args.Implanted.Value);
|
||||
|
||||
var intrinsicRadioTransmitter = EnsureComp<IntrinsicRadioTransmitterComponent>(args.Implanted.Value);
|
||||
foreach (var channel in ent.Comp.RadioChannels)
|
||||
{
|
||||
if (intrinsicRadioTransmitter.Channels.Add(channel))
|
||||
ent.Comp.TransmitterAddedChannels.Add(channel);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes intrinsic radio components once the Radio Implant is removed
|
||||
/// </summary>
|
||||
private void OnRemove(Entity<RadioImplantComponent> ent, ref EntGotRemovedFromContainerMessage args)
|
||||
{
|
||||
if (TryComp<ActiveRadioComponent>(args.Container.Owner, out var activeRadioComponent))
|
||||
{
|
||||
foreach (var channel in ent.Comp.ActiveAddedChannels)
|
||||
{
|
||||
activeRadioComponent.Channels.Remove(channel);
|
||||
}
|
||||
ent.Comp.ActiveAddedChannels.Clear();
|
||||
|
||||
if (activeRadioComponent.Channels.Count == 0)
|
||||
{
|
||||
RemCompDeferred<ActiveRadioComponent>(args.Container.Owner);
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryComp<IntrinsicRadioTransmitterComponent>(args.Container.Owner, out var radioTransmitterComponent))
|
||||
return;
|
||||
|
||||
foreach (var channel in ent.Comp.TransmitterAddedChannels)
|
||||
{
|
||||
radioTransmitterComponent.Channels.Remove(channel);
|
||||
}
|
||||
ent.Comp.TransmitterAddedChannels.Clear();
|
||||
|
||||
if (radioTransmitterComponent.Channels.Count == 0 || activeRadioComponent?.Channels.Count == 0)
|
||||
{
|
||||
RemCompDeferred<IntrinsicRadioTransmitterComponent>(args.Container.Owner);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ public sealed class RulesManager
|
||||
{
|
||||
PopupTime = _cfg.GetCVar(CCVars.RulesWaitTime),
|
||||
CoreRules = _cfg.GetCVar(CCVars.RulesFile),
|
||||
ShouldShowRules = !isLocalhost && !hasCooldown
|
||||
ShouldShowRules = !isLocalhost && !hasCooldown,
|
||||
};
|
||||
_netManager.ServerSendMessage(showRulesMessage, e.Channel);
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ namespace Content.Server.IoC
|
||||
IoCManager.Register<MappingManager>();
|
||||
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
|
||||
IoCManager.Register<ConnectionManager>();
|
||||
IoCManager.Register<MultiServerKickManager>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ using Content.Shared.Chemistry.Reagent;
|
||||
using Content.Shared.UserInterface;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Emag.Components;
|
||||
using Content.Shared.Emag.Systems;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Lathe;
|
||||
using Content.Shared.Materials;
|
||||
@@ -42,6 +43,7 @@ namespace Content.Server.Lathe
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
[Dependency] private readonly ContainerSystem _container = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly UserInterfaceSystem _uiSys = default!;
|
||||
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
|
||||
[Dependency] private readonly PopupSystem _popup = default!;
|
||||
@@ -292,7 +294,7 @@ namespace Content.Server.Lathe
|
||||
{
|
||||
if (uid != args.Lathe || !TryComp<TechnologyDatabaseComponent>(uid, out var technologyDatabase))
|
||||
return;
|
||||
if (!args.getUnavailable && !HasComp<EmaggedComponent>(uid))
|
||||
if (!args.getUnavailable && !_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
foreach (var recipe in component.EmagDynamicRecipes)
|
||||
{
|
||||
|
||||
@@ -72,7 +72,10 @@ public sealed class HealingSystem : EntitySystem
|
||||
_bloodstreamSystem.TryModifyBleedAmount(entity.Owner, healing.BloodlossModifier);
|
||||
if (isBleeding != bloodstream.BleedAmount > 0)
|
||||
{
|
||||
_popupSystem.PopupEntity(Loc.GetString("medical-item-stop-bleeding"), entity, args.User);
|
||||
var popup = (args.User == entity.Owner)
|
||||
? Loc.GetString("medical-item-stop-bleeding-self")
|
||||
: Loc.GetString("medical-item-stop-bleeding", ("target", Identity.Entity(entity.Owner, EntityManager)));
|
||||
_popupSystem.PopupEntity(popup, entity, args.User);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ using Content.Shared.Mobs.Systems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Containers;
|
||||
using Robust.Shared.Map;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
@@ -42,6 +43,7 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
|
||||
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
|
||||
[Dependency] private readonly IPrototypeManager _proto = default!;
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
@@ -369,7 +371,7 @@ public sealed class SuitSensorSystem : EntitySystem
|
||||
userJobIcon = card.Comp.JobIcon;
|
||||
|
||||
foreach (var department in card.Comp.JobDepartments)
|
||||
userJobDepartments.Add(Loc.GetString($"department-{department}"));
|
||||
userJobDepartments.Add(Loc.GetString(_proto.Index(department).Name));
|
||||
}
|
||||
|
||||
// get health mob state
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class MiningSystem : EntitySystem
|
||||
return;
|
||||
|
||||
var coords = Transform(uid).Coordinates;
|
||||
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield);
|
||||
var toSpawn = _random.Next(proto.MinOreYield, proto.MaxOreYield+1);
|
||||
for (var i = 0; i < toSpawn; i++)
|
||||
{
|
||||
Spawn(proto.OreEntity, coords.Offset(_random.NextVector2(0.2f)));
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Numerics;
|
||||
using Content.Shared.Movement.Components;
|
||||
using Content.Shared.Movement.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Server.Movement.Components;
|
||||
|
||||
[RegisterComponent]
|
||||
public sealed partial class EyeCursorOffsetComponent : SharedEyeCursorOffsetComponent
|
||||
{
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Shared.Chemistry.EntitySystems;
|
||||
using Content.Shared.Damage;
|
||||
using Content.Shared.Emag.Components;
|
||||
@@ -55,34 +55,11 @@ public sealed partial class MedibotInjectOperator : HTNOperator
|
||||
if (!_entMan.TryGetComponent<MedibotComponent>(owner, out var botComp))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
|
||||
if (!_entMan.TryGetComponent<DamageableComponent>(target, out var damage))
|
||||
if (!_medibot.CheckInjectable((owner, botComp), target) || !_medibot.TryInject((owner, botComp), target))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
if (!_solutionContainer.TryGetInjectableSolution(target, out var injectable, out _))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
if (!_interaction.InRangeUnobstructed(owner, target))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
var total = damage.TotalDamage;
|
||||
|
||||
// always inject healthy patients when emagged
|
||||
if (total == 0 && !_entMan.HasComponent<EmaggedComponent>(owner))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
if (!_entMan.TryGetComponent<MobStateComponent>(target, out var mobState))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
var state = mobState.CurrentState;
|
||||
if (!_medibot.TryGetTreatment(botComp, mobState.CurrentState, out var treatment) || !treatment.IsValid(total))
|
||||
return HTNOperatorStatus.Failed;
|
||||
|
||||
_entMan.EnsureComponent<NPCRecentlyInjectedComponent>(target);
|
||||
_solutionContainer.TryAddReagent(injectable.Value, treatment.Reagent, treatment.Quantity, out _);
|
||||
_popup.PopupEntity(Loc.GetString("hypospray-component-feel-prick-message"), target, target);
|
||||
_audio.PlayPvs(botComp.InjectSound, target);
|
||||
_chat.TrySendInGameICMessage(owner, Loc.GetString("medibot-finish-inject"), InGameICChatType.Speak, hideChat: true, hideLog: true);
|
||||
|
||||
return HTNOperatorStatus.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.NPC.Components;
|
||||
using Content.Server.NPC.Pathfinding;
|
||||
using Content.Shared.Chemistry.Components.SolutionManager;
|
||||
using Content.Shared.Damage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Content.Server.NPC.Components;
|
||||
using Content.Shared.NPC.Components;
|
||||
|
||||
namespace Content.Server.NPC.Systems;
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ namespace Content.Server.Nuke
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
public int Timer = 300;
|
||||
|
||||
/// <summary>
|
||||
/// If the nuke is disarmed, this sets the minimum amount of time the timer can have.
|
||||
/// The remaining time will reset to this value if it is below it.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public int MinimumTime = 180;
|
||||
|
||||
/// <summary>
|
||||
/// How long until the bomb can arm again after deactivation.
|
||||
/// Used to prevent announcements spam.
|
||||
|
||||
@@ -522,6 +522,9 @@ public sealed class NukeSystem : EntitySystem
|
||||
_sound.PlayGlobalOnStation(uid, _audio.GetSound(component.DisarmSound));
|
||||
_sound.StopStationEventMusic(uid, StationEventMusicType.Nuke);
|
||||
|
||||
// reset nuke remaining time to either itself or the minimum time, whichever is higher
|
||||
component.RemainingTime = Math.Max(component.RemainingTime, component.MinimumTime);
|
||||
|
||||
// disable sound and reset it
|
||||
component.PlayedAlertSound = false;
|
||||
component.AlertAudioStream = _audio.Stop(component.AlertAudioStream);
|
||||
|
||||
@@ -21,6 +21,7 @@ namespace Content.Server.Nutrition.EntitySystems;
|
||||
public sealed class FatExtractorSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly HungerSystem _hunger = default!;
|
||||
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
|
||||
[Dependency] private readonly SharedAudioSystem _audio = default!;
|
||||
@@ -36,8 +37,13 @@ public sealed class FatExtractorSystem : EntitySystem
|
||||
|
||||
private void OnGotEmagged(EntityUid uid, FatExtractorComponent component, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
args.Repeatable = false;
|
||||
}
|
||||
|
||||
private void OnClosed(EntityUid uid, FatExtractorComponent component, ref StorageAfterCloseEvent args)
|
||||
@@ -103,7 +109,7 @@ public sealed class FatExtractorSystem : EntitySystem
|
||||
if (_hunger.GetHunger(hunger) < component.NutritionPerSecond)
|
||||
return false;
|
||||
|
||||
if (hunger.CurrentThreshold < component.MinHungerThreshold && !HasComp<EmaggedComponent>(uid))
|
||||
if (hunger.CurrentThreshold < component.MinHungerThreshold && !_emag.CheckFlag(uid, EmagType.Interaction))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
{
|
||||
[Dependency] private readonly DoAfterSystem _doAfterSystem = default!;
|
||||
[Dependency] private readonly DamageableSystem _damageableSystem = default!;
|
||||
[Dependency] private readonly EmagSystem _emag = default!;
|
||||
[Dependency] private readonly FoodSystem _foodSystem = default!;
|
||||
[Dependency] private readonly ExplosionSystem _explosionSystem = default!;
|
||||
[Dependency] private readonly PopupSystem _popupSystem = default!;
|
||||
@@ -63,7 +64,7 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
forced = false;
|
||||
}
|
||||
|
||||
if (entity.Comp.ExplodeOnUse || HasComp<EmaggedComponent>(entity.Owner))
|
||||
if (entity.Comp.ExplodeOnUse || _emag.CheckFlag(entity, EmagType.Interaction))
|
||||
{
|
||||
_explosionSystem.QueueExplosion(entity.Owner, "Default", entity.Comp.ExplosionIntensity, 0.5f, 3, canCreateVacuum: false);
|
||||
EntityManager.DeleteEntity(entity);
|
||||
@@ -161,8 +162,15 @@ namespace Content.Server.Nutrition.EntitySystems
|
||||
args.Args.Target.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEmagged(Entity<VapeComponent> entity, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!_emag.CompareFlag(args.Type, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
if (_emag.CheckFlag(entity, EmagType.Interaction))
|
||||
return;
|
||||
|
||||
args.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Power.EntitySystems;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
@@ -16,6 +17,7 @@ namespace Content.Server.Power.Components
|
||||
/// Maximum charge of the battery in joules (ie. watt seconds)
|
||||
/// </summary>
|
||||
[DataField]
|
||||
[GuidebookData]
|
||||
public float MaxCharge;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Content.Server.Power.Pow3r;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
@@ -24,6 +25,7 @@ namespace Content.Server.Power.Components
|
||||
|
||||
[DataField("maxSupply")]
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[GuidebookData]
|
||||
public float MaxSupply
|
||||
{
|
||||
get => NetworkBattery.MaxSupply;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Content.Server.Power.NodeGroups;
|
||||
using Content.Server.Power.Pow3r;
|
||||
using Content.Shared.Guidebook;
|
||||
|
||||
namespace Content.Server.Power.Components
|
||||
{
|
||||
@@ -8,6 +9,7 @@ namespace Content.Server.Power.Components
|
||||
{
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
[DataField("supplyRate")]
|
||||
[GuidebookData]
|
||||
public float MaxSupply { get => NetworkSupply.MaxSupply; set => NetworkSupply.MaxSupply = value; }
|
||||
|
||||
[ViewVariables(VVAccess.ReadWrite)]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user