This commit is contained in:
Ed
2025-02-06 14:07:05 +03:00
629 changed files with 312896 additions and 51155 deletions

View File

@@ -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
View File

@@ -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

View File

@@ -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;

View File

@@ -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);
}
}

View 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>

View 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 ?? "")));
}
}
}

View File

@@ -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">

View File

@@ -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]));
}
}
}
}

View File

@@ -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();
}

View File

@@ -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>

View File

@@ -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; }
}

View File

@@ -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!;

View File

@@ -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()

View File

@@ -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;

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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)

View File

@@ -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>();

View File

@@ -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))
{

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View 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;
}
}

View File

@@ -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" />

View File

@@ -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

View File

@@ -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);

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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()
{
}
}

View File

@@ -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()

View File

@@ -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}"));
}
}

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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,
}
}

View File

@@ -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));
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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()
{

View File

@@ -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)

View 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;
}
}

View 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();
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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)

View File

@@ -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");

View 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; }
}
}

View File

@@ -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);

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -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")]

View File

@@ -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
}
}

View File

@@ -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")]

View File

@@ -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>

View File

@@ -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
}
}

View File

@@ -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);

View File

@@ -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
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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)

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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)

View 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);
});
});
}
}

View File

@@ -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();

View File

@@ -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);

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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);

View File

@@ -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))

View File

@@ -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);
}
}

View File

@@ -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.

View 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);
}
}
}

View File

@@ -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);
}

View File

@@ -79,6 +79,7 @@ namespace Content.Server.IoC
IoCManager.Register<MappingManager>();
IoCManager.Register<IWatchlistWebhookManager, WatchlistWebhookManager>();
IoCManager.Register<ConnectionManager>();
IoCManager.Register<MultiServerKickManager>();
}
}
}

View File

@@ -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)
{

View File

@@ -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);
}
}

View File

@@ -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

View File

@@ -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)));

View File

@@ -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
{
}

View File

@@ -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;
}
}

View File

@@ -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;

View File

@@ -1,4 +1,4 @@
using Content.Server.NPC.Components;
using Content.Shared.NPC.Components;
namespace Content.Server.NPC.Systems;

View File

@@ -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.

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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