Merge remote-tracking branch 'refs/remotes/upstream/master' into master-syndie

# Conflicts:
#	.github/CODEOWNERS
#	Content.Client/Options/UI/Tabs/AudioTab.xaml
#	Content.Client/Options/UI/Tabs/AudioTab.xaml.cs
#	Content.Server/Connection/ConnectionManager.cs
#	Content.Shared/Preferences/Loadouts/RoleLoadout.cs
#	Resources/Prototypes/Loadouts/role_loadouts.yml
#	Resources/Prototypes/SoundCollections/NukeMusic.yml
#	Resources/Textures/Clothing/Belt/securitywebbing.rsi/equipped-BELT.png
#	Resources/Textures/Clothing/Belt/securitywebbing.rsi/icon.png
#	Resources/Textures/Clothing/Belt/securitywebbing.rsi/inhand-left.png
#	Resources/Textures/Clothing/Belt/securitywebbing.rsi/inhand-right.png
#	Resources/Textures/Clothing/Belt/securitywebbing.rsi/meta.json
#	Resources/Textures/Objects/Devices/nuke.rsi/nuclearbomb_gay.png
This commit is contained in:
Morb0
2024-06-27 13:40:52 +03:00
229 changed files with 12751 additions and 6435 deletions
+1
View File
@@ -9,5 +9,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions-ecosystem/action-add-labels@v1
if: join(github.event.issue.labels) == ''
with:
labels: "Status: Untriaged"
@@ -5,7 +5,7 @@
<LineEdit Name="FilterLineEdit"
MinSize="100 0"
HorizontalExpand="True"
PlaceHolder="{Loc Filter}"/>
PlaceHolder="{Loc player-list-filter}"/>
<PanelContainer Name="BackgroundPanel"
VerticalExpand="True"
HorizontalExpand="True">
@@ -4,7 +4,7 @@
Title="{Loc admin-player-actions-window-title}" MinSize="425 272">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Reason}" MinWidth="100" />
<Label Text="{Loc admin-player-actions-reason}" MinWidth="100" />
<Control MinWidth="50" />
<LineEdit Name="ReasonLine" MinWidth="100" HorizontalExpand="True" />
</BoxContainer>
@@ -1,9 +1,9 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
Title="{Loc Teleport}" MinSize="425 230">
Title="{Loc admin-ui-teleport}" MinSize="425 230">
<BoxContainer Orientation="Vertical">
<cc:PlayerListControl Name="PlayerList" />
<Button Name="SubmitButton" Text="{Loc Teleport}" />
<Button Name="SubmitButton" Text="{Loc admin-ui-teleport}" />
</BoxContainer>
</DefaultWindow>
@@ -1,33 +1,33 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Load Blueprint}">
xmlns="https://spacestation14.io" Title="{Loc admin-ui-blueprint-load}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Map}" MinSize="100 0" />
<Label Text="{Loc admin-ui-blueprint-map}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="MapOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Path}" MinSize="100 0" />
<Label Text="{Loc admin-ui-blueprint-path}" MinSize="100 0" />
<Control MinSize="50 0" />
<LineEdit Name="MapPath" MinSize="200 0" HorizontalExpand="True" Text="/Maps/" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc X}" MinSize="100 0" />
<Label Text="{Loc admin-ui-blueprint-x}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="XCoordinate" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Y}" MinSize="100 0" />
<Label Text="{Loc admin-ui-blueprint-y}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="YCoordinate" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Rotation}" MinSize="100 0" />
<Label Text="{Loc admin-ui-blueprint-rotation}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="RotationSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="SubmitButton" Text="{Loc Load Blueprint}" />
<Button Name="TeleportButton" Text="{Loc Teleport to}" />
<Button Name="ResetButton" Text="{Loc Reset to default}"></Button>
<Button Name="SubmitButton" Text="{Loc admin-ui-blueprint-load}" />
<Button Name="TeleportButton" Text="{Loc admin-ui-blueprint-teleport}" />
<Button Name="ResetButton" Text="{Loc admin-ui-blueprint-reset}"></Button>
</BoxContainer>
</DefaultWindow>
@@ -1,11 +1,11 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Add Atmos}">
xmlns="https://spacestation14.io" Title="{Loc admin-ui-atmos-add}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Grid}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-grid}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GridOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="SubmitButton" Text="{Loc Add Atmos}" />
<Button Name="SubmitButton" Text="{Loc admin-ui-atmos-add}" />
</BoxContainer>
</DefaultWindow>
@@ -35,7 +35,7 @@ namespace Content.Client.Administration.UI.Tabs.AtmosTab
while (query.MoveNext(out var uid, out var grid))
{
_data.Add((uid, grid));
GridOptions.AddItem($"{uid} {(playerGrid == uid ? " (Current)" : "")}");
GridOptions.AddItem($"{uid} {(playerGrid == uid ? Loc.GetString($"admin-ui-atmos-grid-current") : "")}");
}
GridOptions.OnItemSelected += eventArgs => GridOptions.SelectId(eventArgs.Id);
@@ -1,31 +1,31 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Add Gas}">
xmlns="https://spacestation14.io" Title="{Loc admin-ui-atmos-add-gas}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Grid}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-grid}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GridOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc TileX}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-tile-x}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="TileXSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc TileY}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-tile-y}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="TileYSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Gas}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-gas}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GasOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Amount}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-gas-amount}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="AmountSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="SubmitButton" Text="{Loc Add Gas}" />
<Button Name="SubmitButton" Text="{Loc admin-ui-atmos-add-gas}" />
</BoxContainer>
</DefaultWindow>
@@ -33,7 +33,7 @@ namespace Content.Client.Administration.UI.Tabs.AtmosTab
_gridData.Add(entManager.GetNetEntity(uid));
var player = playerManager.LocalEntity;
var playerGrid = entManager.GetComponentOrNull<TransformComponent>(player)?.GridUid;
GridOptions.AddItem($"{uid} {(playerGrid == uid ? " (Current)" : "")}");
GridOptions.AddItem($"{uid} {(playerGrid == uid ? Loc.GetString("admin-ui-atmos-grid-current") : "")}");
}
GridOptions.OnItemSelected += eventArgs => GridOptions.SelectId(eventArgs.Id);
@@ -6,10 +6,10 @@
Margin="4"
MinSize="50 50">
<GridContainer Columns="4">
<cc:UICommandButton Text="{Loc Add Atmos}" Command="addatmos" WindowType="{x:Type at:AddAtmosWindow}" />
<cc:UICommandButton Text="{Loc Add Gas}" Command="addgas" WindowType="{x:Type at:AddGasWindow}" />
<cc:UICommandButton Text="{Loc Fill Gas}" Command="fillgas" WindowType="{x:Type at:FillGasWindow}" />
<cc:UICommandButton Text="{Loc Set Temperature}" Command="settemp"
<cc:UICommandButton Text="{Loc admin-ui-atmos-add}" Command="addatmos" WindowType="{x:Type at:AddAtmosWindow}" />
<cc:UICommandButton Text="{Loc admin-ui-atmos-add-gas}" Command="addgas" WindowType="{x:Type at:AddGasWindow}" />
<cc:UICommandButton Text="{Loc admin-ui-atmos-fill-gas}" Command="fillgas" WindowType="{x:Type at:FillGasWindow}" />
<cc:UICommandButton Text="{Loc admin-ui-atmos-set-temperature}" Command="settemp"
WindowType="{x:Type at:SetTemperatureWindow}" />
</GridContainer>
</Control>
@@ -1,21 +1,21 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Fill Gas}">
xmlns="https://spacestation14.io" Title="{Loc admin-ui-atmos-fill-gas}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Grid}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-grid}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GridOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Gas}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-gas}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GasOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Amount}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-gas-amount}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="AmountSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="SubmitButton" Text="{Loc Fill Gas}" />
<Button Name="SubmitButton" Text="{Loc admin-ui-atmos-fill-gas}" />
</BoxContainer>
</DefaultWindow>
@@ -36,7 +36,7 @@ namespace Content.Client.Administration.UI.Tabs.AtmosTab
{
var player = playerManager.LocalEntity;
var playerGrid = entManager.GetComponentOrNull<TransformComponent>(player)?.GridUid;
GridOptions.AddItem($"{uid} {(playerGrid == uid ? " (Current)" : "")}");
GridOptions.AddItem($"{uid} {(playerGrid == uid ? Loc.GetString($"admin-ui-atmos-grid-current") : "")}");
_gridData.Add(entManager.GetNetEntity(uid));
}
@@ -1,26 +1,26 @@
<DefaultWindow
xmlns="https://spacestation14.io" Title="{Loc Set Temperature}">
xmlns="https://spacestation14.io" Title="{Loc admin-ui-atmos-set-temperature}">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Grid}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-grid}" MinSize="100 0" />
<Control MinSize="50 0" />
<OptionButton Name="GridOptions" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc TileX}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-tile-x}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="TileXSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc TileY}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-tile-y}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="TileYSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc Temperature}" MinSize="100 0" />
<Label Text="{Loc admin-ui-atmos-temperature}" MinSize="100 0" />
<Control MinSize="50 0" />
<SpinBox Name="TemperatureSpin" MinSize="100 0" HorizontalExpand="True" />
</BoxContainer>
<Button Name="SubmitButton" Text="{Loc Set Temperature}" />
<Button Name="SubmitButton" Text="{Loc admin-ui-atmos-set-temperature}" />
</BoxContainer>
</DefaultWindow>
@@ -32,7 +32,7 @@ namespace Content.Client.Administration.UI.Tabs.AtmosTab
{
var player = playerManager.LocalEntity;
var playerGrid = entManager.GetComponentOrNull<TransformComponent>(player)?.GridUid;
GridOptions.AddItem($"{uid} {(playerGrid == uid ? " (Current)" : "")}");
GridOptions.AddItem($"{uid} {(playerGrid == uid ? Loc.GetString($"admin-ui-atmos-grid-current") : "")}");
_data.Add(entManager.GetNetEntity(uid));
}
@@ -5,8 +5,8 @@
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label HorizontalExpand="True" SizeFlagsStretchRatio="0.50"
Text="{Loc Object type:}" />
<LineEdit Name="SearchLineEdit" PlaceHolder="{Loc Search...}" HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
Text="{Loc object-tab-object-type}" />
<LineEdit Name="SearchLineEdit" PlaceHolder="{Loc object-tab-object-search}" HorizontalExpand="True" SizeFlagsStretchRatio="1"/>
<OptionButton Name="ObjectTypeOptions" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
@@ -41,7 +41,7 @@ public sealed partial class ObjectsTab : Control
foreach (var type in Enum.GetValues(typeof(ObjectsTabSelection)))
{
_selections.Add((ObjectsTabSelection)type!);
ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!);
ObjectTypeOptions.AddItem(Loc.GetString($"object-tab-object-type-{((Enum.GetName((ObjectsTabSelection)type))!.ToString().ToLower())}"));
}
ListHeader.OnHeaderClicked += HeaderClicked;
@@ -4,7 +4,7 @@
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
<Label Name="PlayerCount" HorizontalExpand="True" Text="{Loc Player Count}" />
<Label Name="PlayerCount" HorizontalExpand="True" Text="{Loc player-tab-player-count}" />
<LineEdit Name="SearchLineEdit" HorizontalExpand="True"
PlaceHolder="{Loc player-tab-filter-line-edit-placeholder}" />
<Button Name="ShowDisconnectedButton" HorizontalExpand="True"
@@ -109,7 +109,7 @@ namespace Content.Client.Administration.UI.Tabs.PlayerTab
private void RefreshPlayerList(IReadOnlyList<PlayerInfo> players)
{
_players = players;
PlayerCount.Text = $"Players: {_playerMan.PlayerCount}";
PlayerCount.Text = Loc.GetString("player-tab-player-count", ("count", _playerMan.PlayerCount));
var filteredPlayers = players.Where(info => _showDisconnected || info.Connected).ToList();
@@ -5,9 +5,9 @@
MinSize="50 50">
<GridContainer
Columns="3">
<cc:CommandButton Command="startround" Text="{Loc Start Round}" />
<cc:CommandButton Command="endround" Text="{Loc End Round}" />
<cc:CommandButton Command="restartround" Text="{Loc Restart Round}" />
<cc:CommandButton Command="startround" Text="{Loc administration-ui-round-tab-start-round}" />
<cc:CommandButton Command="endround" Text="{Loc administration-ui-round-tab-end-round}" />
<cc:CommandButton Command="restartround" Text="{Loc administration-ui-round-tab-restart-round}" />
<cc:CommandButton Command="restartroundnow" Text="{Loc administration-ui-round-tab-restart-round-now}" />
</GridContainer>
</Control>
+2
View File
@@ -46,6 +46,8 @@ namespace Content.Client.Examine
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<GetVerbsEvent<ExamineVerb>>(AddExamineVerb);
@@ -0,0 +1,6 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal">
<Label Name="NameLabel" MinWidth="400" />
<OptionButton Name="Button" Access="Public" />
</BoxContainer>
</Control>
@@ -0,0 +1,21 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
namespace Content.Client.Options.UI;
/// <summary>
/// Standard UI control used for drop-downs in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow.AddOptionDropDown{T}"/>
[GenerateTypedNameReferences]
public sealed partial class OptionDropDown : Control
{
/// <summary>
/// The text describing what this drop-down controls.
/// </summary>
public string? Title
{
get => NameLabel.Text;
set => NameLabel.Text = value;
}
}
@@ -0,0 +1,7 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal">
<Label Name="NameLabel" MinWidth="400" />
<Slider Name="Slider" Access="Public" HorizontalExpand="True" />
<Label Name="ValueLabel" Access="Public" Margin="8 0 4 0" MinWidth="48" Align="Right" />
</BoxContainer>
</Control>
@@ -0,0 +1,22 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
namespace Content.Client.Options.UI;
/// <summary>
/// Standard UI control used for sliders in the options menu. Intended for use with <see cref="OptionsTabControlRow"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow.AddOptionSlider"/>
/// <seealso cref="OptionsTabControlRow.AddOptionPercentSlider"/>
[GenerateTypedNameReferences]
public sealed partial class OptionSlider : Control
{
/// <summary>
/// The text describing what this slider controls.
/// </summary>
public string? Title
{
get => NameLabel.Text;
set => NameLabel.Text = value;
}
}
@@ -7,5 +7,6 @@
<tabs:GraphicsTab Name="GraphicsTab" />
<tabs:KeyRebindTab Name="KeyRebindTab" />
<tabs:AudioTab Name="AudioTab" />
<tabs:AccessibilityTab Name="AccessibilityTab" />
</TabContainer>
</DefaultWindow>
@@ -1,9 +1,6 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.IoC;
using Content.Client.Options.UI.Tabs;
namespace Content.Client.Options.UI
{
@@ -19,13 +16,17 @@ namespace Content.Client.Options.UI
Tabs.SetTabTitle(1, Loc.GetString("ui-options-tab-graphics"));
Tabs.SetTabTitle(2, Loc.GetString("ui-options-tab-controls"));
Tabs.SetTabTitle(3, Loc.GetString("ui-options-tab-audio"));
Tabs.SetTabTitle(4, Loc.GetString("ui-options-tab-accessibility"));
UpdateTabs();
}
public void UpdateTabs()
{
GraphicsTab.UpdateProperties();
GraphicsTab.Control.ReloadValues();
MiscTab.Control.ReloadValues();
AccessibilityTab.Control.ReloadValues();
AudioTab.Control.ReloadValues();
}
}
}
@@ -0,0 +1,18 @@
<Control xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<controls:StripeBack HasBottomEdge="False">
<BoxContainer Orientation="Horizontal" Align="End" Margin="2">
<Button Name="DefaultButton"
Text="{Loc 'ui-options-default'}"
TextAlign="Center"
Margin="8 0" />
<Button Name="ResetButton"
Text="{Loc 'ui-options-reset-all'}"
StyleClasses="Caution" />
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
StyleClasses="OpenLeft" />
</BoxContainer>
</controls:StripeBack>
</Control>
@@ -0,0 +1,684 @@
using System.Linq;
using Content.Client.Stylesheets;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Collections;
using Robust.Shared.Configuration;
namespace Content.Client.Options.UI;
/// <summary>
/// Control used on all tabs of the in-game options menu,
/// contains the "save" and "reset" buttons and controls the entire logic.
/// </summary>
/// <remarks>
/// <para>
/// Basic operation is simple: options tabs put this control at the bottom of the tab,
/// they bind UI controls to it with calls such as <see cref="AddOptionCheckBox"/>,
/// then they call <see cref="Initialize"/>. The rest is all handled by the control.
/// </para>
/// <para>
/// Individual options are implementations of <see cref="BaseOption"/>. See the type for details.
/// Common implementations for building on top of CVars are already exist,
/// but tabs can define their own if they need to.
/// </para>
/// <para>
/// Generally, options are added via helper methods such as <see cref="AddOptionCheckBox"/>,
/// however it is totally possible to directly instantiate the backing types
/// and add them via <see cref="AddOption{T}"/>.
/// </para>
/// <para>
/// The options system is general purpose enough that <see cref="OptionsTabControlRow"/> does not, itself,
/// know what a CVar is. It does automatically save CVars to config when save is pressed, but otherwise CVar interaction
/// is handled by <see cref="BaseOption"/> implementations.
/// </para>
/// <para>
/// Behaviorally, the row has 3 control buttons: save, reset changed, and reset to default.
/// "Save" writes the configuration changes and saves the configuration.
/// "Reset changed" discards changes made in the menu and re-loads the saved settings.
/// "Reset to default" resets the settings on the menu to be the default, out-of-the-box values.
/// Note that "Reset to default" does not save immediately, the user must still press save manually.
/// </para>
/// <para>
/// The disabled state of the 3 buttons is updated dynamically based on the values of the options.
/// </para>
/// </remarks>
[GenerateTypedNameReferences]
public sealed partial class OptionsTabControlRow : Control
{
[Dependency] private readonly ILocalizationManager _loc = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
private ValueList<BaseOption> _options;
public OptionsTabControlRow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ResetButton.StyleClasses.Add(StyleBase.ButtonOpenRight);
ApplyButton.OnPressed += ApplyButtonPressed;
ResetButton.OnPressed += ResetButtonPressed;
DefaultButton.OnPressed += DefaultButtonPressed;
}
/// <summary>
/// Add a new option to be tracked by the control.
/// </summary>
/// <param name="option">The option object that manages this object's logic</param>
/// <typeparam name="T">
/// The type of option being passed in. Necessary to allow the return type to match the parameter type
/// for easy chaining.
/// </typeparam>
/// <returns>The same <paramref name="option"/> as passed in, for easy chaining.</returns>
public T AddOption<T>(T option) where T : BaseOption
{
_options.Add(option);
return option;
}
/// <summary>
/// Add a checkbox option backed by a simple boolean CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the checkbox.</param>
/// <param name="checkBox">The UI control for the option.</param>
/// <param name="invert">
/// If true, the checkbox is inverted relative to the CVar: if the CVar is true, the checkbox will be unchecked.
/// </param>
/// <returns>The option instance backing the added option.</returns>
/// <seealso cref="OptionCheckboxCVar"/>
public OptionCheckboxCVar AddOptionCheckBox(CVarDef<bool> cVar, CheckBox checkBox, bool invert = false)
{
return AddOption(new OptionCheckboxCVar(this, _cfg, cVar, checkBox, invert));
}
/// <summary>
/// Add a slider option, displayed in percent, backed by a simple float CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the slider.</param>
/// <param name="slider">The UI control for the option.</param>
/// <param name="min">The minimum value the slider should allow. The default value represents "0%"</param>
/// <param name="max">The maximum value the slider should allow. The default value represents "100%"</param>
/// <param name="scale">
/// Scale with which to multiply slider values when mapped to the backing CVar.
/// For example, if a scale of 2 is set, a slider at 75% writes a value of 1.5 to the CVar.
/// </param>
/// <returns>The option instance backing the added option.</returns>
/// <remarks>
/// <para>
/// Note that percentage values are represented as ratios in code, i.e. a value of 100% is "1".
/// </para>
/// </remarks>
public OptionSliderFloatCVar AddOptionPercentSlider(
CVarDef<float> cVar,
OptionSlider slider,
float min = 0,
float max = 1,
float scale = 1)
{
return AddOption(new OptionSliderFloatCVar(this, _cfg, cVar, slider, min, max, scale, FormatPercent));
}
/// <summary>
/// Add a slider option, backed by a simple integer CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the slider.</param>
/// <param name="slider">The UI control for the option.</param>
/// <param name="min">The minimum value the slider should allow.</param>
/// <param name="max">The maximum value the slider should allow.</param>
/// <param name="format">
/// An optional delegate used to format the textual value display of the slider.
/// If not provided, the default behavior is to directly format the integer value as text.
/// </param>
/// <returns>The option instance backing the added option.</returns>
public OptionSliderIntCVar AddOptionSlider(
CVarDef<int> cVar,
OptionSlider slider,
int min,
int max,
Func<OptionSliderIntCVar, int, string>? format = null)
{
return AddOption(new OptionSliderIntCVar(this, _cfg, cVar, slider, min, max, format ?? FormatInt));
}
/// <summary>
/// Add a drop-down option, backed by a CVar.
/// </summary>
/// <param name="cVar">The CVar represented by the drop-down.</param>
/// <param name="dropDown">The UI control for the option.</param>
/// <param name="options">
/// The set of options that will be shown in the drop-down. Items are ordered as provided.
/// </param>
/// <typeparam name="T">The type of the CVar being controlled.</typeparam>
/// <returns>The option instance backing the added option.</returns>
public OptionDropDownCVar<T> AddOptionDropDown<T>(
CVarDef<T> cVar,
OptionDropDown dropDown,
IReadOnlyCollection<OptionDropDownCVar<T>.ValueOption> options)
where T : notnull
{
return AddOption(new OptionDropDownCVar<T>(this, _cfg, cVar, dropDown, options));
}
/// <summary>
/// Initializes the control row. This should be called after all options have been added.
/// </summary>
public void Initialize()
{
foreach (var option in _options)
{
option.LoadValue();
}
UpdateButtonState();
}
/// <summary>
/// Re-loads options in the settings from backing values.
/// Should be called when the options window is opened to make sure all values are up-to-date.
/// </summary>
public void ReloadValues()
{
Initialize();
}
/// <summary>
/// Called by <see cref="BaseOption"/> to signal that an option's value changed through user interaction.
/// </summary>
/// <remarks>
/// <see cref="BaseOption"/> implementations should not call this function directly,
/// instead they should call <see cref="BaseOption.ValueChanged"/>.
/// </remarks>
public void ValueChanged()
{
UpdateButtonState();
}
private void UpdateButtonState()
{
var anyModified = _options.Any(option => option.IsModified());
var anyModifiedFromDefault = _options.Any(option => option.IsModifiedFromDefault());
DefaultButton.Disabled = !anyModifiedFromDefault;
ApplyButton.Disabled = !anyModified;
ResetButton.Disabled = !anyModified;
}
private void ApplyButtonPressed(BaseButton.ButtonEventArgs obj)
{
foreach (var option in _options)
{
if (option.IsModified())
option.SaveValue();
}
_cfg.SaveToFile();
UpdateButtonState();
}
private void ResetButtonPressed(BaseButton.ButtonEventArgs obj)
{
foreach (var option in _options)
{
option.LoadValue();
}
UpdateButtonState();
}
private void DefaultButtonPressed(BaseButton.ButtonEventArgs obj)
{
foreach (var option in _options)
{
option.ResetToDefault();
}
UpdateButtonState();
}
private string FormatPercent(OptionSliderFloatCVar slider, float value)
{
return _loc.GetString("ui-options-value-percent", ("value", value));
}
private static string FormatInt(OptionSliderIntCVar slider, int value)
{
return value.ToString();
}
}
/// <summary>
/// Base class of a single "option" for <see cref="OptionsTabControlRow"/>.
/// </summary>
/// <remarks>
/// <para>
/// Implementations of this class handle loading values from backing storage or defaults,
/// handling UI controls, and saving. The main <see cref="OptionsTabControlRow"/> does not know what a CVar is.
/// </para>
/// <para>
/// <see cref="BaseOptionCVar{TValue}"/> is a derived class that makes it easier to work with options
/// backed by a single CVar.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <seealso cref="OptionsTabControlRow"/>
public abstract class BaseOption(OptionsTabControlRow controller)
{
/// <summary>
/// Should be called by derived implementations to indicate that their value changed, due to user interaction.
/// </summary>
protected virtual void ValueChanged()
{
controller.ValueChanged();
}
/// <summary>
/// Loads the value represented by this option from its backing store, into the UI state.
/// </summary>
public abstract void LoadValue();
/// <summary>
/// Saves the value in the UI state to the backing store.
/// </summary>
public abstract void SaveValue();
/// <summary>
/// Resets the UI state to that of the factory-default value. This should not write to the backing store.
/// </summary>
public abstract void ResetToDefault();
/// <summary>
/// Called to check if this option's UI value is different from the backing store value.
/// </summary>
/// <returns>If true, the UI value is different and was modified by the user.</returns>
public abstract bool IsModified();
/// <summary>
/// Called to check if this option's UI value is different from the backing store's default value.
/// </summary>
/// <returns>If true, the UI value is different.</returns>
public abstract bool IsModifiedFromDefault();
}
/// <summary>
/// Derived class of <see cref="BaseOption"/> intended for making mappings to simple CVars easier.
/// </summary>
/// <typeparam name="TValue">The type of the CVar.</typeparam>
/// <seealso cref="OptionsTabControlRow"/>
public abstract class BaseOptionCVar<TValue> : BaseOption
where TValue : notnull
{
/// <summary>
/// Raised immediately when the UI value of this option is changed by the user, even before saving.
/// </summary>
/// <remarks>
/// <para>
/// This can be used to update parts of the options UI based on the state of a checkbox.
/// </para>
/// </remarks>
public event Action<TValue>? ImmediateValueChanged;
private readonly IConfigurationManager _cfg;
private readonly CVarDef<TValue> _cVar;
/// <summary>
/// Sets and gets the actual CVar value to/from the frontend UI state or control.
/// </summary>
/// <remarks>
/// <para>
/// In the simplest case, this function should set a UI control's state to represent the CVar,
/// and inversely conver the UI control's state to the CVar value. For simple controls like a checkbox or slider,
/// this just means passing through their value property.
/// </para>
/// </remarks>
protected abstract TValue Value { get; set; }
protected BaseOptionCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<TValue> cVar)
: base(controller)
{
_cfg = cfg;
_cVar = cVar;
}
public override void LoadValue()
{
Value = _cfg.GetCVar(_cVar);
}
public override void SaveValue()
{
_cfg.SetCVar(_cVar, Value);
}
public override void ResetToDefault()
{
Value = _cVar.DefaultValue;
}
public override bool IsModified()
{
return !IsValueEqual(Value, _cfg.GetCVar(_cVar));
}
public override bool IsModifiedFromDefault()
{
return !IsValueEqual(Value, _cVar.DefaultValue);
}
protected virtual bool IsValueEqual(TValue a, TValue b)
{
// Use different logic for floats so there's some error margin.
// This check is handled cleanly at compile-time by the JIT.
if (typeof(TValue) == typeof(float))
return MathHelper.CloseToPercent((float) (object) a, (float) (object) b);
return EqualityComparer<TValue>.Default.Equals(a, b);
}
protected override void ValueChanged()
{
base.ValueChanged();
ImmediateValueChanged?.Invoke(Value);
}
}
/// <summary>
/// Implementation of a CVar option that simply corresponds with a <see cref="CheckBox"/>.
/// </summary>
/// <remarks>
/// <para>
/// Generally, you should just call <c>AddOption</c> methods on <see cref="OptionsTabControlRow"/>
/// instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <seealso cref="OptionsTabControlRow"/>
public sealed class OptionCheckboxCVar : BaseOptionCVar<bool>
{
private readonly CheckBox _checkBox;
private readonly bool _invert;
protected override bool Value
{
get => _checkBox.Pressed ^ _invert;
set => _checkBox.Pressed = value ^ _invert;
}
/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="checkBox">The UI control for the option.</param>
/// <param name="invert">
/// If true, the checkbox is inverted relative to the CVar: if the CVar is true, the checkbox will be unchecked.
/// </param>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionCheckBox"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
public OptionCheckboxCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<bool> cVar,
CheckBox checkBox,
bool invert)
: base(controller, cfg, cVar)
{
_checkBox = checkBox;
_invert = invert;
checkBox.OnToggled += _ =>
{
ValueChanged();
};
}
}
/// <summary>
/// Implementation of a CVar option that simply corresponds with a floating-point <see cref="OptionSlider"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow"/>
public sealed class OptionSliderFloatCVar : BaseOptionCVar<float>
{
/// <summary>
/// Scale with which to multiply slider values when mapped to the backing CVar.
/// </summary>
/// <remarks>
/// For example, if a scale of 2 is set, a slider at 75% writes a value of 1.5 to the CVar.
/// </remarks>
public float Scale { get; }
private readonly OptionSlider _slider;
private readonly Func<OptionSliderFloatCVar, float, string> _format;
protected override float Value
{
get => _slider.Slider.Value * Scale;
set
{
_slider.Slider.Value = value / Scale;
UpdateLabelValue();
}
}
/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="slider">The UI control for the option.</param>
/// <param name="minValue">The minimum value the slider should allow.</param>
/// <param name="maxValue">The maximum value the slider should allow.</param>
/// <param name="scale">
/// Scale with which to multiply slider values when mapped to the backing CVar. See <see cref="Scale"/>.
/// </param>
/// <param name="format">Function that will be called to format the value display next to the slider.</param>
public OptionSliderFloatCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<float> cVar,
OptionSlider slider,
float minValue,
float maxValue,
float scale,
Func<OptionSliderFloatCVar, float, string> format) : base(controller, cfg, cVar)
{
Scale = scale;
_slider = slider;
_format = format;
slider.Slider.MinValue = minValue;
slider.Slider.MaxValue = maxValue;
slider.Slider.OnValueChanged += _ =>
{
ValueChanged();
UpdateLabelValue();
};
}
private void UpdateLabelValue()
{
_slider.ValueLabel.Text = _format(this, _slider.Slider.Value);
}
}
/// <summary>
/// Implementation of a CVar option that simply corresponds with an integer <see cref="OptionSlider"/>.
/// </summary>
/// <seealso cref="OptionsTabControlRow"/>
public sealed class OptionSliderIntCVar : BaseOptionCVar<int>
{
private readonly OptionSlider _slider;
private readonly Func<OptionSliderIntCVar, int, string> _format;
protected override int Value
{
get => (int) _slider.Slider.Value;
set
{
_slider.Slider.Value = value;
UpdateLabelValue();
}
}
/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionPercentSlider"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="slider">The UI control for the option.</param>
/// <param name="minValue">The minimum value the slider should allow.</param>
/// <param name="maxValue">The maximum value the slider should allow.</param>
/// <param name="format">Function that will be called to format the value display next to the slider.</param>
public OptionSliderIntCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<int> cVar,
OptionSlider slider,
int minValue,
int maxValue,
Func<OptionSliderIntCVar, int, string> format) : base(controller, cfg, cVar)
{
_slider = slider;
_format = format;
slider.Slider.MinValue = minValue;
slider.Slider.MaxValue = maxValue;
slider.Slider.Rounded = true;
slider.Slider.OnValueChanged += _ =>
{
ValueChanged();
UpdateLabelValue();
};
}
private void UpdateLabelValue()
{
_slider.ValueLabel.Text = _format(this, (int) _slider.Slider.Value);
}
}
/// <summary>
/// Implementation of a CVar option via a drop-down.
/// </summary>
/// <seealso cref="OptionsTabControlRow"/>
public sealed class OptionDropDownCVar<T> : BaseOptionCVar<T> where T : notnull
{
private readonly OptionDropDown _dropDown;
private readonly ItemEntry[] _entries;
protected override T Value
{
get => (T) _dropDown.Button.SelectedMetadata!;
set => _dropDown.Button.SelectId(FindValueId(value));
}
/// <summary>
/// Creates a new instance of this type.
/// </summary>
/// <remarks>
/// <para>
/// It is generally more convenient to call overloads on <see cref="OptionsTabControlRow"/>
/// such as <see cref="OptionsTabControlRow.AddOptionDropDown{T}"/> instead of instantiating this type directly.
/// </para>
/// </remarks>
/// <param name="controller">The control row that owns this option.</param>
/// <param name="cfg">The configuration manager to get and set values from.</param>
/// <param name="cVar">The CVar that is being controlled by this option.</param>
/// <param name="dropDown">The UI control for the option.</param>
/// <param name="options">The list of options shown to the user.</param>
public OptionDropDownCVar(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CVarDef<T> cVar,
OptionDropDown dropDown,
IReadOnlyCollection<ValueOption> options) : base(controller, cfg, cVar)
{
if (options.Count == 0)
throw new ArgumentException("Need at least one option!");
_dropDown = dropDown;
_entries = new ItemEntry[options.Count];
var button = dropDown.Button;
var i = 0;
foreach (var option in options)
{
_entries[i] = new ItemEntry
{
Key = option.Key,
};
button.AddItem(option.Label, i);
button.SetItemMetadata(button.GetIdx(i), option.Key);
i += 1;
}
dropDown.Button.OnItemSelected += args =>
{
dropDown.Button.SelectId(args.Id);
ValueChanged();
};
}
private int FindValueId(T value)
{
for (var i = 0; i < _entries.Length; i++)
{
if (IsValueEqual(_entries[i].Key, value))
return i;
}
// This will just default select the first entry or whatever.
return 0;
}
/// <summary>
/// A single option for a drop-down.
/// </summary>
/// <param name="key">The value that this option has. This is what will be written to the CVar if selected.</param>
/// <param name="label">The visual text shown to the user for the option.</param>
/// <seealso cref="OptionDropDownCVar{T}"/>
/// <seealso cref="OptionsTabControlRow.AddOptionDropDown{T}"/>
public sealed class ValueOption(T key, string label)
{
/// <summary>
/// The value that this option has. This is what will be written to the CVar if selected.
/// </summary>
public readonly T Key = key;
/// <summary>
/// The visual text shown to the user for the option.
/// </summary>
public readonly string Label = label;
}
private struct ItemEntry
{
public T Key;
}
}
@@ -0,0 +1,16 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HScrollEnabled="False">
<BoxContainer Orientation="Vertical" Margin="8">
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<ui:OptionSlider Name="ChatWindowOpacitySlider" Title="{Loc 'ui-options-chat-window-opacity'}" />
<ui:OptionSlider Name="ScreenShakeIntensitySlider" Title="{Loc 'ui-options-screen-shake-intensity'}" />
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</Control>
@@ -0,0 +1,24 @@
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class AccessibilityTab : Control
{
public AccessibilityTab()
{
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CCVars.ChatEnableColorName, EnableColorNameCheckBox);
Control.AddOptionCheckBox(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox);
Control.AddOptionCheckBox(CCVars.ReducedMotion, ReducedMotionCheckBox);
Control.AddOptionPercentSlider(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider);
Control.AddOptionPercentSlider(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider);
Control.Initialize();
}
}
+12 -126
View File
@@ -1,141 +1,27 @@
<Control xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:Content.Client.Stylesheets"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<Label Text="{Loc 'ui-options-volume-label'}"
FontColorOverride="{x:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<BoxContainer Orientation="Vertical" Margin="0 3 0 0">
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-master-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="MasterVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="MasterVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<Control MinSize="0 8" />
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-midi-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="MidiVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="MidiVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-ambient-music-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="AmbientMusicVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="AmbientMusicVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-ambience-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="AmbienceVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="AmbienceVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-lobby-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="LobbyVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="LobbyVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-interface-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="InterfaceVolumeSlider"
MinValue="0"
MaxValue="100"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="InterfaceVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-tts-volume'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="TtsVolumeSlider"
MinValue="0"
MaxValue="200"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="TtsVolumeLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Margin="5 0 0 0">
<Label Text="{Loc 'ui-options-ambience-max-sounds'}" HorizontalExpand="True" />
<Control MinSize="8 0" />
<Slider Name="AmbienceSoundsSlider"
MinValue="0"
MaxValue="1"
HorizontalExpand="True"
MinSize="80 0"
Rounded="True" />
<Control MinSize="8 0" />
<Label Name="AmbienceSoundsLabel" MinSize="48 0" Align="Right" />
<Control MinSize="4 0"/>
</BoxContainer>
<Control MinSize="0 8" />
<ui:OptionSlider Name="SliderVolumeMaster" Title="{Loc 'ui-options-master-volume'}"
Margin="0 0 0 8" />
<!-- Corvax-TTS-Start --><ui:OptionSlider Name="SliderVolumeTts" Title="{Loc 'ui-options-tts-volume'}" /><!-- Corvax-TTS-End -->
<ui:OptionSlider Name="SliderVolumeMidi" Title="{Loc 'ui-options-midi-volume'}" />
<ui:OptionSlider Name="SliderVolumeAmbientMusic" Title="{Loc 'ui-options-ambient-music-volume'}" />
<ui:OptionSlider Name="SliderVolumeAmbience" Title="{Loc 'ui-options-ambience-volume'}" />
<ui:OptionSlider Name="SliderVolumeLobby" Title="{Loc 'ui-options-lobby-volume'}" />
<ui:OptionSlider Name="SliderVolumeInterface" Title="{Loc 'ui-options-interface-volume'}" />
<ui:OptionSlider Name="SliderMaxAmbienceSounds" Title="{Loc 'ui-options-ambience-max-sounds'}"
Margin="0 0 0 8" />
<CheckBox Name="LobbyMusicCheckBox" Text="{Loc 'ui-options-lobby-music'}" />
<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'}" />
</BoxContainer>
</BoxContainer>
<controls:StripeBack HasBottomEdge="False" HasMargins="False">
<BoxContainer Orientation="Horizontal"
Align="End"
HorizontalExpand="True"
VerticalExpand="True">
<Button Name="ResetButton"
Text="{Loc 'ui-options-reset-all'}"
StyleClasses="Caution"
HorizontalExpand="True"
HorizontalAlignment="Right" />
<Control MinSize="2 0" />
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
TextAlign="Center"
HorizontalAlignment="Right" />
</BoxContainer>
</controls:StripeBack>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</Control>
+51 -195
View File
@@ -4,216 +4,72 @@ using Content.Shared.Corvax.CCCVars;
using Robust.Client.Audio;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Range = Robust.Client.UserInterface.Controls.Range;
namespace Content.Client.Options.UI.Tabs
namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class AudioTab : Control
{
[GenerateTypedNameReferences]
public sealed partial class AudioTab : Control
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IAudioManager _audio = default!;
public AudioTab()
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
private readonly IAudioManager _audio;
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
public AudioTab()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var masterVolume = Control.AddOptionPercentSlider(
CVars.AudioMasterVolume,
SliderVolumeMaster,
scale: ContentAudioSystem.MasterVolumeMultiplier);
masterVolume.ImmediateValueChanged += OnMasterVolumeSliderChanged;
_audio = IoCManager.Resolve<IAudioManager>();
LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled);
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
Control.AddOptionPercentSlider(
CVars.MidiVolume,
SliderVolumeMidi,
scale: ContentAudioSystem.MidiVolumeMultiplier);
ApplyButton.OnPressed += OnApplyButtonPressed;
ResetButton.OnPressed += OnResetButtonPressed;
MasterVolumeSlider.OnValueChanged += OnMasterVolumeSliderChanged;
MidiVolumeSlider.OnValueChanged += OnMidiVolumeSliderChanged;
AmbientMusicVolumeSlider.OnValueChanged += OnAmbientMusicVolumeSliderChanged;
AmbienceVolumeSlider.OnValueChanged += OnAmbienceVolumeSliderChanged;
AmbienceSoundsSlider.OnValueChanged += OnAmbienceSoundsSliderChanged;
LobbyVolumeSlider.OnValueChanged += OnLobbyVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged += OnInterfaceVolumeSliderChanged;
TtsVolumeSlider.OnValueChanged += OnTtsVolumeSliderChanged; // Corvax-TTS
LobbyMusicCheckBox.OnToggled += OnLobbyMusicCheckToggled;
RestartSoundsCheckBox.OnToggled += OnRestartSoundsCheckToggled;
EventMusicCheckBox.OnToggled += OnEventMusicCheckToggled;
AdminSoundsCheckBox.OnToggled += OnAdminSoundsCheckToggled;
Control.AddOptionPercentSlider(
CCVars.AmbientMusicVolume,
SliderVolumeAmbientMusic,
scale: ContentAudioSystem.AmbientMusicMultiplier);
AmbienceSoundsSlider.MinValue = _cfg.GetCVar(CCVars.MinMaxAmbientSourcesConfigured);
AmbienceSoundsSlider.MaxValue = _cfg.GetCVar(CCVars.MaxMaxAmbientSourcesConfigured);
Control.AddOptionPercentSlider(
CCVars.AmbienceVolume,
SliderVolumeAmbience,
scale: ContentAudioSystem.AmbienceMultiplier);
Reset();
}
Control.AddOptionPercentSlider(
CCVars.LobbyMusicVolume,
SliderVolumeLobby,
scale: ContentAudioSystem.LobbyMultiplier);
protected override void Dispose(bool disposing)
{
ApplyButton.OnPressed -= OnApplyButtonPressed;
ResetButton.OnPressed -= OnResetButtonPressed;
MasterVolumeSlider.OnValueChanged -= OnMasterVolumeSliderChanged;
MidiVolumeSlider.OnValueChanged -= OnMidiVolumeSliderChanged;
AmbientMusicVolumeSlider.OnValueChanged -= OnAmbientMusicVolumeSliderChanged;
AmbienceVolumeSlider.OnValueChanged -= OnAmbienceVolumeSliderChanged;
LobbyVolumeSlider.OnValueChanged -= OnLobbyVolumeSliderChanged;
InterfaceVolumeSlider.OnValueChanged -= OnInterfaceVolumeSliderChanged;
TtsVolumeSlider.OnValueChanged -= OnTtsVolumeSliderChanged; // Corvax-TTS
base.Dispose(disposing);
}
Control.AddOptionPercentSlider(
CCVars.InterfaceVolume,
SliderVolumeInterface,
scale: ContentAudioSystem.InterfaceMultiplier);
private void OnLobbyVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
Control.AddOptionSlider(
CCVars.MaxAmbientSources,
SliderMaxAmbienceSounds,
_cfg.GetCVar(CCVars.MinMaxAmbientSourcesConfigured),
_cfg.GetCVar(CCVars.MaxMaxAmbientSourcesConfigured));
private void OnInterfaceVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
Control.AddOptionCheckBox(CCVars.LobbyMusicEnabled, LobbyMusicCheckBox);
Control.AddOptionCheckBox(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox);
Control.AddOptionCheckBox(CCVars.EventMusicEnabled, EventMusicCheckBox);
Control.AddOptionCheckBox(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox);
private void OnAmbientMusicVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
Control.Initialize();
}
private void OnAmbienceVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
private void OnAmbienceSoundsSliderChanged(Range obj)
{
UpdateChanges();
}
private void OnMasterVolumeSliderChanged(Range range)
{
_audio.SetMasterGain(MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier);
UpdateChanges();
}
private void OnMidiVolumeSliderChanged(Range range)
{
UpdateChanges();
}
// Corvax-TTS-Start
private void OnTtsVolumeSliderChanged(Range obj)
{
UpdateChanges();
}
// Corvax-TTS-End
private void OnLobbyMusicCheckToggled(BaseButton.ButtonEventArgs args)
{
UpdateChanges();
}
private void OnRestartSoundsCheckToggled(BaseButton.ButtonEventArgs args)
{
UpdateChanges();
}
private void OnEventMusicCheckToggled(BaseButton.ButtonEventArgs args)
{
UpdateChanges();
}
private void OnAdminSoundsCheckToggled(BaseButton.ButtonEventArgs args)
{
UpdateChanges();
}
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
{
_cfg.SetCVar(CVars.AudioMasterVolume, MasterVolumeSlider.Value / 100f * ContentAudioSystem.MasterVolumeMultiplier);
// Want the CVar updated values to have the multiplier applied
// For the UI we just display 0-100 still elsewhere
_cfg.SetCVar(CVars.MidiVolume, MidiVolumeSlider.Value / 100f * ContentAudioSystem.MidiVolumeMultiplier);
_cfg.SetCVar(CCVars.AmbienceVolume, AmbienceVolumeSlider.Value / 100f * ContentAudioSystem.AmbienceMultiplier);
_cfg.SetCVar(CCVars.AmbientMusicVolume, AmbientMusicVolumeSlider.Value / 100f * ContentAudioSystem.AmbientMusicMultiplier);
_cfg.SetCVar(CCVars.LobbyMusicVolume, LobbyVolumeSlider.Value / 100f * ContentAudioSystem.LobbyMultiplier);
_cfg.SetCVar(CCVars.InterfaceVolume, InterfaceVolumeSlider.Value / 100f * ContentAudioSystem.InterfaceMultiplier);
_cfg.SetCVar(CCCVars.TTSVolume, TtsVolumeSlider.Value / 100f * ContentAudioSystem.TtsMultiplier); // Corvax-TTS
_cfg.SetCVar(CCVars.MaxAmbientSources, (int)AmbienceSoundsSlider.Value);
_cfg.SetCVar(CCVars.LobbyMusicEnabled, LobbyMusicCheckBox.Pressed);
_cfg.SetCVar(CCVars.RestartSoundsEnabled, RestartSoundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.EventMusicEnabled, EventMusicCheckBox.Pressed);
_cfg.SetCVar(CCVars.AdminSoundsEnabled, AdminSoundsCheckBox.Pressed);
_cfg.SaveToFile();
UpdateChanges();
}
private void OnResetButtonPressed(BaseButton.ButtonEventArgs args)
{
Reset();
}
private void Reset()
{
MasterVolumeSlider.Value = _cfg.GetCVar(CVars.AudioMasterVolume) * 100f / ContentAudioSystem.MasterVolumeMultiplier;
MidiVolumeSlider.Value = _cfg.GetCVar(CVars.MidiVolume) * 100f / ContentAudioSystem.MidiVolumeMultiplier;
AmbienceVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbienceVolume) * 100f / ContentAudioSystem.AmbienceMultiplier;
AmbientMusicVolumeSlider.Value = _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier;
LobbyVolumeSlider.Value = _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier;
InterfaceVolumeSlider.Value = _cfg.GetCVar(CCVars.InterfaceVolume) * 100f / ContentAudioSystem.InterfaceMultiplier;
TtsVolumeSlider.Value = _cfg.GetCVar(CCCVars.TTSVolume) * 100f / ContentAudioSystem.TtsMultiplier; // Corvax-TTS
AmbienceSoundsSlider.Value = _cfg.GetCVar(CCVars.MaxAmbientSources);
LobbyMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.LobbyMusicEnabled);
RestartSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.RestartSoundsEnabled);
EventMusicCheckBox.Pressed = _cfg.GetCVar(CCVars.EventMusicEnabled);
AdminSoundsCheckBox.Pressed = _cfg.GetCVar(CCVars.AdminSoundsEnabled);
UpdateChanges();
}
private void UpdateChanges()
{
// y'all need jesus.
var isMasterVolumeSame =
Math.Abs(MasterVolumeSlider.Value - _cfg.GetCVar(CVars.AudioMasterVolume) * 100f / ContentAudioSystem.MasterVolumeMultiplier) < 0.01f;
var isMidiVolumeSame =
Math.Abs(MidiVolumeSlider.Value - _cfg.GetCVar(CVars.MidiVolume) * 100f / ContentAudioSystem.MidiVolumeMultiplier) < 0.01f;
var isAmbientVolumeSame =
Math.Abs(AmbienceVolumeSlider.Value - _cfg.GetCVar(CCVars.AmbienceVolume) * 100f / ContentAudioSystem.AmbienceMultiplier) < 0.01f;
var isAmbientMusicVolumeSame =
Math.Abs(AmbientMusicVolumeSlider.Value - _cfg.GetCVar(CCVars.AmbientMusicVolume) * 100f / ContentAudioSystem.AmbientMusicMultiplier) < 0.01f;
var isLobbyVolumeSame =
Math.Abs(LobbyVolumeSlider.Value - _cfg.GetCVar(CCVars.LobbyMusicVolume) * 100f / ContentAudioSystem.LobbyMultiplier) < 0.01f;
var isInterfaceVolumeSame =
Math.Abs(InterfaceVolumeSlider.Value - _cfg.GetCVar(CCVars.InterfaceVolume) * 100f / ContentAudioSystem.InterfaceMultiplier) < 0.01f;
var isTtsVolumeSame =
Math.Abs(TtsVolumeSlider.Value - _cfg.GetCVar(CCCVars.TTSVolume) * 100f / ContentAudioSystem.TtsMultiplier) < 0.01f;
var isAmbientSoundsSame = (int)AmbienceSoundsSlider.Value == _cfg.GetCVar(CCVars.MaxAmbientSources);
var isLobbySame = LobbyMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.LobbyMusicEnabled);
var isRestartSoundsSame = RestartSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.RestartSoundsEnabled);
var isEventSame = EventMusicCheckBox.Pressed == _cfg.GetCVar(CCVars.EventMusicEnabled);
var isAdminSoundsSame = AdminSoundsCheckBox.Pressed == _cfg.GetCVar(CCVars.AdminSoundsEnabled);
var isEverythingSame = isMasterVolumeSame && isMidiVolumeSame && isAmbientVolumeSame && isAmbientMusicVolumeSame && isAmbientSoundsSame && isLobbySame && isRestartSoundsSame && isEventSame
&& isAdminSoundsSame && isLobbyVolumeSame && isInterfaceVolumeSame;
isEverythingSame = isEverythingSame && isTtsVolumeSame; // Corvax-TTS
ApplyButton.Disabled = isEverythingSame;
ResetButton.Disabled = isEverythingSame;
MasterVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", MasterVolumeSlider.Value / 100));
MidiVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", MidiVolumeSlider.Value / 100));
AmbientMusicVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", AmbientMusicVolumeSlider.Value / 100));
AmbienceVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", AmbienceVolumeSlider.Value / 100));
LobbyVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", LobbyVolumeSlider.Value / 100));
InterfaceVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", InterfaceVolumeSlider.Value / 100));
TtsVolumeLabel.Text =
Loc.GetString("ui-options-volume-percent", ("volume", TtsVolumeSlider.Value / 100)); // Corvax-TTS
AmbienceSoundsLabel.Text = ((int)AmbienceSoundsSlider.Value).ToString();
}
private void OnMasterVolumeSliderChanged(float value)
{
// TODO: I was thinking of giving OptionsTabControlRow a flag to "set CVar immediately", but I'm deferring that
// until there's a proper system for enforcing people don't close the window with pending changes.
_audio.SetMasterGain(value);
}
}
+32 -47
View File
@@ -1,53 +1,38 @@
<tabs:GraphicsTab xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:tabs="clr-namespace:Content.Client.Options.UI.Tabs">
xmlns:tabs="clr-namespace:Content.Client.Options.UI.Tabs"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<CheckBox Name="VSyncCheckBox" Text="{Loc 'ui-options-vsync'}" />
<CheckBox Name="FullscreenCheckBox" Text="{Loc 'ui-options-fullscreen'}" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-lighting-label'}" />
<Control MinSize="4 0" />
<OptionButton Name="LightingPresetOption" MinSize="100 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-scale-label'}" />
<Control MinSize="4 0" />
<OptionButton Name="UIScaleOption" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 8 8 8">
<!-- Display -->
<Label Text="{Loc 'ui-options-display-label'}" StyleClasses="LabelKeyText"/>
<CheckBox Name="VSyncCheckBox" Text="{Loc 'ui-options-vsync'}" />
<CheckBox Name="FullscreenCheckBox" Text="{Loc 'ui-options-fullscreen'}" />
<!-- Quality -->
<Label Text="{Loc 'ui-options-quality-label'}" StyleClasses="LabelKeyText"/>
<ui:OptionDropDown Name="DropDownLightingQuality" Title="{Loc 'ui-options-lighting-label'}" />
<CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
<CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
<!-- Interface -->
<Label Text="{Loc 'ui-options-interface-label'}" StyleClasses="LabelKeyText"/>
<ui:OptionDropDown Name="DropDownUIScale" Title="{Loc 'ui-options-scale-label'}" />
<CheckBox Name="ViewportStretchCheckBox" Text="{Loc 'ui-options-vp-stretch'}" />
<BoxContainer Name="ViewportScaleBox" Orientation="Horizontal">
<Label Name="ViewportScaleText" Margin="8 0" />
<Slider Name="ViewportScaleSlider"
MinValue="1"
MaxValue="5"
Rounded="True"
MinWidth="200" />
</BoxContainer>
<ui:OptionSlider Name="ViewportScaleSlider" Title="{Loc ui-options-vp-scale}" />
<ui:OptionSlider Name="ViewportWidthSlider" Title="{Loc ui-options-vp-width}" />
<CheckBox Name="IntegerScalingCheckBox"
Text="{Loc 'ui-options-vp-integer-scaling'}"
ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" />
<CheckBox Name="ViewportVerticalFitCheckBox"
Text="{Loc 'ui-options-vp-vertical-fit'}"
ToolTip="{Loc 'ui-options-vp-vertical-fit-tooltip'}" />
<!-- Misc -->
<Label Text="{Loc 'ui-options-misc-label'}" StyleClasses="LabelKeyText"/>
<CheckBox Name="FpsCounterCheckBox" Text="{Loc 'ui-options-fps-counter'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Name="ViewportWidthSliderDisplay" />
<Control MinSize="4 0" />
<Slider Name="ViewportWidthSlider"
Rounded="True"
MinWidth="200" />
</BoxContainer>
<CheckBox Name="IntegerScalingCheckBox"
Text="{Loc 'ui-options-vp-integer-scaling'}"
ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" />
<CheckBox Name="ViewportVerticalFitCheckBox"
Text="{Loc 'ui-options-vp-vertical-fit'}"
ToolTip="{Loc 'ui-options-vp-vertical-fit-tooltip'}" />
<CheckBox Name="ViewportLowResCheckBox" Text="{Loc 'ui-options-vp-low-res'}" />
<CheckBox Name="ParallaxLowQualityCheckBox" Text="{Loc 'ui-options-parallax-low-quality'}" />
<CheckBox Name="FpsCounterCheckBox" Text="{Loc 'ui-options-fps-counter'}" />
</BoxContainer>
<controls:StripeBack HasBottomEdge="False" HasMargins="False">
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
TextAlign="Center"
HorizontalAlignment="Right" />
</controls:StripeBack>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</tabs:GraphicsTab>
+181 -217
View File
@@ -7,220 +7,141 @@ using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
namespace Content.Client.Options.UI.Tabs
namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class GraphicsTab : Control
{
[GenerateTypedNameReferences]
public sealed partial class GraphicsTab : Control
[Dependency] private readonly IConfigurationManager _cfg = default!;
public GraphicsTab()
{
private static readonly float[] UIScaleOptions =
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CVars.DisplayVSync, VSyncCheckBox);
Control.AddOption(new OptionFullscreen(Control, _cfg, FullscreenCheckBox));
Control.AddOption(new OptionLightingQuality(Control, _cfg, DropDownLightingQuality));
Control.AddOptionDropDown(
CVars.DisplayUIScale,
DropDownUIScale,
[
new OptionDropDownCVar<float>.ValueOption(
0f,
Loc.GetString("ui-options-scale-auto", ("scale", UserInterfaceManager.DefaultUIScale))),
new OptionDropDownCVar<float>.ValueOption(0.75f, Loc.GetString("ui-options-scale-75")),
new OptionDropDownCVar<float>.ValueOption(1.00f, Loc.GetString("ui-options-scale-100")),
new OptionDropDownCVar<float>.ValueOption(1.25f, Loc.GetString("ui-options-scale-125")),
new OptionDropDownCVar<float>.ValueOption(1.50f, Loc.GetString("ui-options-scale-150")),
new OptionDropDownCVar<float>.ValueOption(1.75f, Loc.GetString("ui-options-scale-175")),
new OptionDropDownCVar<float>.ValueOption(2.00f, Loc.GetString("ui-options-scale-200")),
]);
var vpStretch = Control.AddOptionCheckBox(CCVars.ViewportStretch, ViewportStretchCheckBox);
var vpVertFit = Control.AddOptionCheckBox(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox);
Control.AddOptionSlider(
CCVars.ViewportFixedScaleFactor,
ViewportScaleSlider,
1,
5,
(_, value) => Loc.GetString("ui-options-vp-scale-value", ("scale", value)));
vpStretch.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility();
vpVertFit.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility();
Control.AddOptionSlider(
CCVars.ViewportWidth,
ViewportWidthSlider,
(int)ViewportWidthSlider.Slider.MinValue,
(int)ViewportWidthSlider.Slider.MaxValue);
Control.AddOption(new OptionIntegerScaling(Control, _cfg, IntegerScalingCheckBox));
Control.AddOptionCheckBox(CCVars.ViewportScaleRender, ViewportLowResCheckBox, invert: true);
Control.AddOptionCheckBox(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox);
Control.AddOptionCheckBox(CCVars.HudFpsCounterVisible, FpsCounterCheckBox);
Control.Initialize();
_cfg.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportWidthRange());
_cfg.OnValueChanged(CCVars.ViewportMaximumWidth, _ => UpdateViewportWidthRange());
UpdateViewportWidthRange();
UpdateViewportSettingsVisibility();
}
private void UpdateViewportSettingsVisibility()
{
ViewportScaleSlider.Visible = !ViewportStretchCheckBox.Pressed;
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportWidthSlider.Visible = !ViewportStretchCheckBox.Pressed || !ViewportVerticalFitCheckBox.Pressed;
}
private void UpdateViewportWidthRange()
{
var min = _cfg.GetCVar(CCVars.ViewportMinimumWidth);
var max = _cfg.GetCVar(CCVars.ViewportMaximumWidth);
ViewportWidthSlider.Slider.MinValue = min;
ViewportWidthSlider.Slider.MaxValue = max;
}
private sealed class OptionLightingQuality : BaseOption
{
private readonly IConfigurationManager _cfg;
private readonly OptionDropDown _dropDown;
private const int QualityVeryLow = 0;
private const int QualityLow = 1;
private const int QualityMedium = 2;
private const int QualityHigh = 3;
private const int QualityDefault = QualityMedium;
public OptionLightingQuality(OptionsTabControlRow controller, IConfigurationManager cfg, OptionDropDown dropDown) : base(controller)
{
0f,
0.75f,
1f,
1.25f,
1.50f,
1.75f,
2f
};
[Dependency] private readonly IConfigurationManager _cfg = default!;
public GraphicsTab()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
VSyncCheckBox.OnToggled += OnCheckBoxToggled;
FullscreenCheckBox.OnToggled += OnCheckBoxToggled;
LightingPresetOption.AddItem(Loc.GetString("ui-options-lighting-very-low"));
LightingPresetOption.AddItem(Loc.GetString("ui-options-lighting-low"));
LightingPresetOption.AddItem(Loc.GetString("ui-options-lighting-medium"));
LightingPresetOption.AddItem(Loc.GetString("ui-options-lighting-high"));
LightingPresetOption.OnItemSelected += OnLightingQualityChanged;
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-auto",
("scale", UserInterfaceManager.DefaultUIScale)));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-75"));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-100"));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-125"));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-150"));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-175"));
UIScaleOption.AddItem(Loc.GetString("ui-options-scale-200"));
UIScaleOption.OnItemSelected += OnUIScaleChanged;
ViewportStretchCheckBox.OnToggled += _ =>
{
UpdateViewportScale();
UpdateApplyButton();
};
ViewportScaleSlider.OnValueChanged += _ =>
{
UpdateApplyButton();
UpdateViewportScale();
};
ViewportWidthSlider.OnValueChanged += _ =>
{
UpdateViewportWidthDisplay();
UpdateApplyButton();
};
ViewportVerticalFitCheckBox.OnToggled += _ =>
{
UpdateViewportScale();
UpdateApplyButton();
};
IntegerScalingCheckBox.OnToggled += OnCheckBoxToggled;
ViewportLowResCheckBox.OnToggled += OnCheckBoxToggled;
ParallaxLowQualityCheckBox.OnToggled += OnCheckBoxToggled;
FpsCounterCheckBox.OnToggled += OnCheckBoxToggled;
ApplyButton.OnPressed += OnApplyButtonPressed;
VSyncCheckBox.Pressed = _cfg.GetCVar(CVars.DisplayVSync);
FullscreenCheckBox.Pressed = ConfigIsFullscreen;
LightingPresetOption.SelectId(GetConfigLightingQuality());
UIScaleOption.SelectId(GetConfigUIScalePreset(ConfigUIScale));
ViewportScaleSlider.Value = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
ViewportStretchCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportStretch);
IntegerScalingCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0;
ViewportVerticalFitCheckBox.Pressed = _cfg.GetCVar(CCVars.ViewportVerticalFit);
ViewportLowResCheckBox.Pressed = !_cfg.GetCVar(CCVars.ViewportScaleRender);
ParallaxLowQualityCheckBox.Pressed = _cfg.GetCVar(CCVars.ParallaxLowQuality);
FpsCounterCheckBox.Pressed = _cfg.GetCVar(CCVars.HudFpsCounterVisible);
ViewportWidthSlider.Value = _cfg.GetCVar(CCVars.ViewportWidth);
_cfg.OnValueChanged(CCVars.ViewportMinimumWidth, _ => UpdateViewportWidthRange());
_cfg.OnValueChanged(CCVars.ViewportMaximumWidth, _ => UpdateViewportWidthRange());
UpdateViewportWidthRange();
UpdateViewportWidthDisplay();
UpdateViewportScale();
UpdateApplyButton();
_cfg = cfg;
_dropDown = dropDown;
var button = dropDown.Button;
button.AddItem(Loc.GetString("ui-options-lighting-very-low"), QualityVeryLow);
button.AddItem(Loc.GetString("ui-options-lighting-low"), QualityLow);
button.AddItem(Loc.GetString("ui-options-lighting-medium"), QualityMedium);
button.AddItem(Loc.GetString("ui-options-lighting-high"), QualityHigh);
button.OnItemSelected += OnOptionSelected;
}
private void OnUIScaleChanged(OptionButton.ItemSelectedEventArgs args)
private void OnOptionSelected(OptionButton.ItemSelectedEventArgs obj)
{
UIScaleOption.SelectId(args.Id);
UpdateApplyButton();
_dropDown.Button.SelectId(obj.Id);
ValueChanged();
}
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
public override void LoadValue()
{
_cfg.SetCVar(CVars.DisplayVSync, VSyncCheckBox.Pressed);
SetConfigLightingQuality(LightingPresetOption.SelectedId);
_cfg.SetCVar(CVars.DisplayWindowMode,
(int) (FullscreenCheckBox.Pressed ? WindowMode.Fullscreen : WindowMode.Windowed));
_cfg.SetCVar(CVars.DisplayUIScale, UIScaleOptions[UIScaleOption.SelectedId]);
_cfg.SetCVar(CCVars.ViewportStretch, ViewportStretchCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportFixedScaleFactor, (int) ViewportScaleSlider.Value);
_cfg.SetCVar(CCVars.ViewportSnapToleranceMargin,
IntegerScalingCheckBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0);
_cfg.SetCVar(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportScaleRender, !ViewportLowResCheckBox.Pressed);
_cfg.SetCVar(CCVars.ParallaxLowQuality, ParallaxLowQualityCheckBox.Pressed);
_cfg.SetCVar(CCVars.HudFpsCounterVisible, FpsCounterCheckBox.Pressed);
_cfg.SetCVar(CCVars.ViewportWidth, (int) ViewportWidthSlider.Value);
_cfg.SaveToFile();
UpdateApplyButton();
_dropDown.Button.SelectId(GetConfigLightingQuality());
}
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
public override void SaveValue()
{
UpdateApplyButton();
}
private void OnLightingQualityChanged(OptionButton.ItemSelectedEventArgs args)
{
LightingPresetOption.SelectId(args.Id);
UpdateApplyButton();
}
private void UpdateApplyButton()
{
var isVSyncSame = VSyncCheckBox.Pressed == _cfg.GetCVar(CVars.DisplayVSync);
var isFullscreenSame = FullscreenCheckBox.Pressed == ConfigIsFullscreen;
var isLightingQualitySame = LightingPresetOption.SelectedId == GetConfigLightingQuality();
var isUIScaleSame = MathHelper.CloseToPercent(UIScaleOptions[UIScaleOption.SelectedId], ConfigUIScale);
var isVPStretchSame = ViewportStretchCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportStretch);
var isVPScaleSame = (int) ViewportScaleSlider.Value == _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var isIntegerScalingSame = IntegerScalingCheckBox.Pressed == (_cfg.GetCVar(CCVars.ViewportSnapToleranceMargin) != 0);
var isVPVerticalFitSame = ViewportVerticalFitCheckBox.Pressed == _cfg.GetCVar(CCVars.ViewportVerticalFit);
var isVPResSame = ViewportLowResCheckBox.Pressed == !_cfg.GetCVar(CCVars.ViewportScaleRender);
var isPLQSame = ParallaxLowQualityCheckBox.Pressed == _cfg.GetCVar(CCVars.ParallaxLowQuality);
var isFpsCounterVisibleSame = FpsCounterCheckBox.Pressed == _cfg.GetCVar(CCVars.HudFpsCounterVisible);
var isWidthSame = (int) ViewportWidthSlider.Value == _cfg.GetCVar(CCVars.ViewportWidth);
ApplyButton.Disabled = isVSyncSame &&
isFullscreenSame &&
isLightingQualitySame &&
isUIScaleSame &&
isVPStretchSame &&
isVPScaleSame &&
isIntegerScalingSame &&
isVPVerticalFitSame &&
isVPResSame &&
isPLQSame &&
isFpsCounterVisibleSame &&
isWidthSame;
}
private bool ConfigIsFullscreen =>
_cfg.GetCVar(CVars.DisplayWindowMode) == (int) WindowMode.Fullscreen;
public void UpdateProperties()
{
FullscreenCheckBox.Pressed = ConfigIsFullscreen;
}
private float ConfigUIScale => _cfg.GetCVar(CVars.DisplayUIScale);
private int GetConfigLightingQuality()
{
var val = _cfg.GetCVar(CVars.LightResolutionScale);
var soft = _cfg.GetCVar(CVars.LightSoftShadows);
if (val <= 0.125)
switch (_dropDown.Button.SelectedId)
{
return 0;
}
else if ((val <= 0.5) && !soft)
{
return 1;
}
else if (val <= 0.5)
{
return 2;
}
else
{
return 3;
}
}
private void SetConfigLightingQuality(int value)
{
switch (value)
{
case 0:
case QualityVeryLow:
_cfg.SetCVar(CVars.LightResolutionScale, 0.125f);
_cfg.SetCVar(CVars.LightSoftShadows, false);
_cfg.SetCVar(CVars.LightBlur, false);
break;
case 1:
case QualityLow:
_cfg.SetCVar(CVars.LightResolutionScale, 0.5f);
_cfg.SetCVar(CVars.LightSoftShadows, false);
_cfg.SetCVar(CVars.LightBlur, true);
break;
case 2:
default: // = QualityMedium
_cfg.SetCVar(CVars.LightResolutionScale, 0.5f);
_cfg.SetCVar(CVars.LightSoftShadows, true);
_cfg.SetCVar(CVars.LightBlur, true);
break;
case 3:
case QualityHigh:
_cfg.SetCVar(CVars.LightResolutionScale, 1);
_cfg.SetCVar(CVars.LightSoftShadows, true);
_cfg.SetCVar(CVars.LightBlur, true);
@@ -228,40 +149,83 @@ namespace Content.Client.Options.UI.Tabs
}
}
private static int GetConfigUIScalePreset(float value)
public override void ResetToDefault()
{
for (var i = 0; i < UIScaleOptions.Length; i++)
_dropDown.Button.SelectId(QualityDefault);
}
public override bool IsModified()
{
return _dropDown.Button.SelectedId != GetConfigLightingQuality();
}
public override bool IsModifiedFromDefault()
{
return _dropDown.Button.SelectedId != QualityDefault;
}
private int GetConfigLightingQuality()
{
var val = _cfg.GetCVar(CVars.LightResolutionScale);
var soft = _cfg.GetCVar(CVars.LightSoftShadows);
if (val <= 0.125)
return QualityVeryLow;
if ((val <= 0.5) && !soft)
return QualityLow;
if (val <= 0.5)
return QualityMedium;
return QualityHigh;
}
}
private sealed class OptionFullscreen : BaseOptionCVar<int>
{
private readonly CheckBox _checkBox;
protected override int Value
{
get => _checkBox.Pressed ? (int) WindowMode.Fullscreen : (int) WindowMode.Windowed;
set => _checkBox.Pressed = (value == (int) WindowMode.Fullscreen);
}
public OptionFullscreen(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CheckBox checkBox)
: base(controller, cfg, CVars.DisplayWindowMode)
{
_checkBox = checkBox;
_checkBox.OnToggled += _ =>
{
if (MathHelper.CloseToPercent(UIScaleOptions[i], value))
{
return i;
}
}
ValueChanged();
};
}
}
return 0;
private sealed class OptionIntegerScaling : BaseOptionCVar<int>
{
private readonly CheckBox _checkBox;
protected override int Value
{
get => _checkBox.Pressed ? CCVars.ViewportSnapToleranceMargin.DefaultValue : 0;
set => _checkBox.Pressed = (value != 0);
}
private void UpdateViewportScale()
public OptionIntegerScaling(
OptionsTabControlRow controller,
IConfigurationManager cfg,
CheckBox checkBox)
: base(controller, cfg, CCVars.ViewportSnapToleranceMargin)
{
ViewportScaleBox.Visible = !ViewportStretchCheckBox.Pressed;
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportWidthSlider.Visible = ViewportWidthSliderDisplay.Visible = !ViewportStretchCheckBox.Pressed || ViewportStretchCheckBox.Pressed && !ViewportVerticalFitCheckBox.Pressed;
ViewportScaleText.Text = Loc.GetString("ui-options-vp-scale", ("scale", ViewportScaleSlider.Value));
}
private void UpdateViewportWidthRange()
{
var min = _cfg.GetCVar(CCVars.ViewportMinimumWidth);
var max = _cfg.GetCVar(CCVars.ViewportMaximumWidth);
ViewportWidthSlider.MinValue = min;
ViewportWidthSlider.MaxValue = max;
}
private void UpdateViewportWidthDisplay()
{
ViewportWidthSliderDisplay.Text = Loc.GetString("ui-options-vp-width", ("width", (int) ViewportWidthSlider.Value));
_checkBox = checkBox;
_checkBox.OnToggled += _ =>
{
ValueChanged();
};
}
}
}
+6 -48
View File
@@ -1,76 +1,34 @@
<tabs:MiscTab xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:tabs="clr-namespace:Content.Client.Options.UI.Tabs"
xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:s="clr-namespace:Content.Client.Stylesheets">
xmlns:tabs="clr-namespace:Content.Client.Options.UI.Tabs"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="clr-namespace:Content.Client.Options.UI">
<BoxContainer Orientation="Vertical">
<ScrollContainer VerticalExpand="True" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" Margin="8 8 8 8" VerticalExpand="True">
<Label Text="{Loc 'ui-options-general-ui-style'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-hud-theme'}" />
<Control MinSize="4 0" />
<OptionButton Name="HudThemeOption" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-hud-layout'}" />
<Control MinSize="4 0" />
<OptionButton Name="HudLayoutOption" />
</BoxContainer>
<Label Text="{Loc 'ui-options-general-accessibility'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ReducedMotionCheckBox" Text="{Loc 'ui-options-reduced-motion'}" />
<CheckBox Name="EnableColorNameCheckBox" Text="{Loc 'ui-options-enable-color-name'}" />
<CheckBox Name="ColorblindFriendlyCheckBox" Text="{Loc 'ui-options-colorblind-friendly'}" />
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-chat-window-opacity'}" Margin="8 0" />
<Slider Name="ChatWindowOpacitySlider"
MinValue="0"
MaxValue="1"
MinWidth="200" />
<Label Name="ChatWindowOpacityLabel" Margin="8 0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'ui-options-screen-shake-intensity'}" Margin="8 0" />
<Slider Name="ScreenShakeIntensitySlider"
MinValue="0"
MaxValue="100"
Rounded="True"
MinWidth="200" />
<Label Name="ScreenShakeIntensityLabel" Margin="8 0" />
</BoxContainer>
<ui:OptionDropDown Name="DropDownHudTheme" Title="{Loc 'ui-options-hud-theme'}" />
<ui:OptionDropDown Name="DropDownHudLayout" Title="{Loc 'ui-options-hud-layout'}" />
<Label Text="{Loc 'ui-options-general-discord'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="DiscordRich" Text="{Loc 'ui-options-discordrich'}" />
<Label Text="{Loc 'ui-options-general-speech'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ShowOocPatronColor" Text="{Loc 'ui-options-show-ooc-patron-color'}" />
<CheckBox Name="ShowLoocAboveHeadCheckBox" Text="{Loc 'ui-options-show-looc-on-head'}" />
<CheckBox Name="FancySpeechBubblesCheckBox" Text="{Loc 'ui-options-fancy-speech'}" />
<CheckBox Name="FancyNameBackgroundsCheckBox" Text="{Loc 'ui-options-fancy-name-background'}" />
<Label Text="{Loc 'ui-options-general-cursor'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="ShowHeldItemCheckBox" Text="{Loc 'ui-options-show-held-item'}" />
<CheckBox Name="ShowCombatModeIndicatorsCheckBox" Text="{Loc 'ui-options-show-combat-mode-indicators'}" />
<Label Text="{Loc 'ui-options-general-storage'}"
FontColorOverride="{xNamespace:Static s:StyleNano.NanoGold}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="OpaqueStorageWindowCheckBox" Text="{Loc 'ui-options-opaque-storage-window'}" />
<CheckBox Name="StaticStorageUI" Text="{Loc 'ui-options-static-storage-ui'}" />
<!-- <CheckBox Name="ToggleWalk" Text="{Loc 'ui-options-hotkey-toggle-walk'}" /> -->
</BoxContainer>
</ScrollContainer>
<controls:StripeBack HasBottomEdge="False" HasMargins="False">
<Button Name="ApplyButton"
Text="{Loc 'ui-options-apply'}"
TextAlign="Center"
HorizontalAlignment="Right" />
</controls:StripeBack>
<ui:OptionsTabControlRow Name="Control" Access="Public" />
</BoxContainer>
</tabs:MiscTab>
+33 -180
View File
@@ -5,201 +5,54 @@ using Content.Shared.HUD;
using Robust.Client.AutoGenerated;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Range = Robust.Client.UserInterface.Controls.Range;
namespace Content.Client.Options.UI.Tabs
namespace Content.Client.Options.UI.Tabs;
[GenerateTypedNameReferences]
public sealed partial class MiscTab : Control
{
[GenerateTypedNameReferences]
public sealed partial class MiscTab : Control
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
public MiscTab()
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
private readonly Dictionary<string, int> _hudThemeIdToIndex = new();
public MiscTab()
var themes = _prototypeManager.EnumeratePrototypes<HudThemePrototype>().ToList();
themes.Sort();
var themeEntries = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var gear in themes)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
var themes = _prototypeManager.EnumeratePrototypes<HudThemePrototype>().ToList();
themes.Sort();
foreach (var gear in themes)
{
HudThemeOption.AddItem(Loc.GetString(gear.Name));
_hudThemeIdToIndex.Add(gear.ID, HudThemeOption.GetItemId(HudThemeOption.ItemCount - 1));
}
var hudLayout = _cfg.GetCVar(CCVars.UILayout);
var id = 0;
foreach (var layout in Enum.GetValues(typeof(ScreenType)))
{
var name = layout.ToString()!;
HudLayoutOption.AddItem(name, id);
if (name == hudLayout)
{
HudLayoutOption.SelectId(id);
}
HudLayoutOption.SetItemMetadata(id, name);
id++;
}
HudLayoutOption.OnItemSelected += args =>
{
HudLayoutOption.SelectId(args.Id);
UpdateApplyButton();
};
// Channel can be null in replays so.
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel?.UserData.PatronTier is { };
HudThemeOption.OnItemSelected += OnHudThemeChanged;
DiscordRich.OnToggled += OnCheckBoxToggled;
ShowOocPatronColor.OnToggled += OnCheckBoxToggled;
ShowLoocAboveHeadCheckBox.OnToggled += OnCheckBoxToggled;
ShowHeldItemCheckBox.OnToggled += OnCheckBoxToggled;
ShowCombatModeIndicatorsCheckBox.OnToggled += OnCheckBoxToggled;
OpaqueStorageWindowCheckBox.OnToggled += OnCheckBoxToggled;
FancySpeechBubblesCheckBox.OnToggled += OnCheckBoxToggled;
FancyNameBackgroundsCheckBox.OnToggled += OnCheckBoxToggled;
EnableColorNameCheckBox.OnToggled += OnCheckBoxToggled;
ColorblindFriendlyCheckBox.OnToggled += OnCheckBoxToggled;
ReducedMotionCheckBox.OnToggled += OnCheckBoxToggled;
ChatWindowOpacitySlider.OnValueChanged += OnChatWindowOpacitySliderChanged;
ScreenShakeIntensitySlider.OnValueChanged += OnScreenShakeIntensitySliderChanged;
// ToggleWalk.OnToggled += OnCheckBoxToggled;
StaticStorageUI.OnToggled += OnCheckBoxToggled;
HudThemeOption.SelectId(_hudThemeIdToIndex.GetValueOrDefault(_cfg.GetCVar(CVars.InterfaceTheme), 0));
DiscordRich.Pressed = _cfg.GetCVar(CVars.DiscordEnabled);
ShowOocPatronColor.Pressed = _cfg.GetCVar(CCVars.ShowOocPatronColor);
ShowLoocAboveHeadCheckBox.Pressed = _cfg.GetCVar(CCVars.LoocAboveHeadShow);
ShowHeldItemCheckBox.Pressed = _cfg.GetCVar(CCVars.HudHeldItemShow);
ShowCombatModeIndicatorsCheckBox.Pressed = _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow);
OpaqueStorageWindowCheckBox.Pressed = _cfg.GetCVar(CCVars.OpaqueStorageWindow);
FancySpeechBubblesCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableFancyBubbles);
FancyNameBackgroundsCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatFancyNameBackground);
EnableColorNameCheckBox.Pressed = _cfg.GetCVar(CCVars.ChatEnableColorName);
ColorblindFriendlyCheckBox.Pressed = _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
ReducedMotionCheckBox.Pressed = _cfg.GetCVar(CCVars.ReducedMotion);
ChatWindowOpacitySlider.Value = _cfg.GetCVar(CCVars.ChatWindowOpacity);
ScreenShakeIntensitySlider.Value = _cfg.GetCVar(CCVars.ScreenShakeIntensity) * 100f;
// ToggleWalk.Pressed = _cfg.GetCVar(CCVars.ToggleWalk);
StaticStorageUI.Pressed = _cfg.GetCVar(CCVars.StaticStorageUI);
ApplyButton.OnPressed += OnApplyButtonPressed;
UpdateApplyButton();
themeEntries.Add(new OptionDropDownCVar<string>.ValueOption(gear.ID, Loc.GetString(gear.Name)));
}
private void OnCheckBoxToggled(BaseButton.ButtonToggledEventArgs args)
var layoutEntries = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var layout in Enum.GetValues(typeof(ScreenType)))
{
UpdateApplyButton();
layoutEntries.Add(new OptionDropDownCVar<string>.ValueOption(layout.ToString()!, layout.ToString()!));
}
private void OnHudThemeChanged(OptionButton.ItemSelectedEventArgs args)
{
HudThemeOption.SelectId(args.Id);
UpdateApplyButton();
}
// Channel can be null in replays so.
// ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract
ShowOocPatronColor.Visible = _playerManager.LocalSession?.Channel?.UserData.PatronTier is { };
private void OnChatWindowOpacitySliderChanged(Range range)
{
ChatWindowOpacityLabel.Text = Loc.GetString("ui-options-chat-window-opacity-percent",
("opacity", range.Value));
UpdateApplyButton();
}
Control.AddOptionDropDown(CVars.InterfaceTheme, DropDownHudTheme, themeEntries);
Control.AddOptionDropDown(CCVars.UILayout, DropDownHudLayout, layoutEntries);
private void OnScreenShakeIntensitySliderChanged(Range obj)
{
ScreenShakeIntensityLabel.Text = Loc.GetString("ui-options-screen-shake-percent", ("intensity", ScreenShakeIntensitySlider.Value / 100f));
UpdateApplyButton();
}
private void OnApplyButtonPressed(BaseButton.ButtonEventArgs args)
{
foreach (var theme in _prototypeManager.EnumeratePrototypes<HudThemePrototype>())
{
if (_hudThemeIdToIndex[theme.ID] != HudThemeOption.SelectedId)
continue;
_cfg.SetCVar(CVars.InterfaceTheme, theme.ID);
break;
}
_cfg.SetCVar(CVars.DiscordEnabled, DiscordRich.Pressed);
_cfg.SetCVar(CCVars.HudHeldItemShow, ShowHeldItemCheckBox.Pressed);
_cfg.SetCVar(CCVars.CombatModeIndicatorsPointShow, ShowCombatModeIndicatorsCheckBox.Pressed);
_cfg.SetCVar(CCVars.OpaqueStorageWindow, OpaqueStorageWindowCheckBox.Pressed);
_cfg.SetCVar(CCVars.ShowOocPatronColor, ShowOocPatronColor.Pressed);
_cfg.SetCVar(CCVars.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatEnableColorName, EnableColorNameCheckBox.Pressed);
_cfg.SetCVar(CCVars.AccessibilityColorblindFriendly, ColorblindFriendlyCheckBox.Pressed);
_cfg.SetCVar(CCVars.ReducedMotion, ReducedMotionCheckBox.Pressed);
_cfg.SetCVar(CCVars.ChatWindowOpacity, ChatWindowOpacitySlider.Value);
_cfg.SetCVar(CCVars.ScreenShakeIntensity, ScreenShakeIntensitySlider.Value / 100f);
// _cfg.SetCVar(CCVars.ToggleWalk, ToggleWalk.Pressed);
_cfg.SetCVar(CCVars.StaticStorageUI, StaticStorageUI.Pressed);
if (HudLayoutOption.SelectedMetadata is string opt)
{
_cfg.SetCVar(CCVars.UILayout, opt);
}
_cfg.SaveToFile();
UpdateApplyButton();
}
private void UpdateApplyButton()
{
var isHudThemeSame = HudThemeOption.SelectedId == _hudThemeIdToIndex.GetValueOrDefault(_cfg.GetCVar(CVars.InterfaceTheme), 0);
var isLayoutSame = HudLayoutOption.SelectedMetadata is string opt && opt == _cfg.GetCVar(CCVars.UILayout);
var isDiscordSame = DiscordRich.Pressed == _cfg.GetCVar(CVars.DiscordEnabled);
var isShowHeldItemSame = ShowHeldItemCheckBox.Pressed == _cfg.GetCVar(CCVars.HudHeldItemShow);
var isCombatModeIndicatorsSame = ShowCombatModeIndicatorsCheckBox.Pressed == _cfg.GetCVar(CCVars.CombatModeIndicatorsPointShow);
var isOpaqueStorageWindow = OpaqueStorageWindowCheckBox.Pressed == _cfg.GetCVar(CCVars.OpaqueStorageWindow);
var isOocPatronColorShowSame = ShowOocPatronColor.Pressed == _cfg.GetCVar(CCVars.ShowOocPatronColor);
var isLoocShowSame = ShowLoocAboveHeadCheckBox.Pressed == _cfg.GetCVar(CCVars.LoocAboveHeadShow);
var isFancyChatSame = FancySpeechBubblesCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableFancyBubbles);
var isFancyBackgroundSame = FancyNameBackgroundsCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatFancyNameBackground);
var isEnableColorNameSame = EnableColorNameCheckBox.Pressed == _cfg.GetCVar(CCVars.ChatEnableColorName);
var isColorblindFriendly = ColorblindFriendlyCheckBox.Pressed == _cfg.GetCVar(CCVars.AccessibilityColorblindFriendly);
var isReducedMotionSame = ReducedMotionCheckBox.Pressed == _cfg.GetCVar(CCVars.ReducedMotion);
var isChatWindowOpacitySame = Math.Abs(ChatWindowOpacitySlider.Value - _cfg.GetCVar(CCVars.ChatWindowOpacity)) < 0.01f;
var isScreenShakeIntensitySame = Math.Abs(ScreenShakeIntensitySlider.Value / 100f - _cfg.GetCVar(CCVars.ScreenShakeIntensity)) < 0.01f;
// var isToggleWalkSame = ToggleWalk.Pressed == _cfg.GetCVar(CCVars.ToggleWalk);
var isStaticStorageUISame = StaticStorageUI.Pressed == _cfg.GetCVar(CCVars.StaticStorageUI);
ApplyButton.Disabled = isHudThemeSame &&
isLayoutSame &&
isDiscordSame &&
isShowHeldItemSame &&
isCombatModeIndicatorsSame &&
isOpaqueStorageWindow &&
isOocPatronColorShowSame &&
isLoocShowSame &&
isFancyChatSame &&
isFancyBackgroundSame &&
isEnableColorNameSame &&
isColorblindFriendly &&
isReducedMotionSame &&
isChatWindowOpacitySame &&
isScreenShakeIntensitySame &&
// isToggleWalkSame &&
isStaticStorageUISame;
}
Control.AddOptionCheckBox(CVars.DiscordEnabled, DiscordRich);
Control.AddOptionCheckBox(CCVars.ShowOocPatronColor, ShowOocPatronColor);
Control.AddOptionCheckBox(CCVars.LoocAboveHeadShow, ShowLoocAboveHeadCheckBox);
Control.AddOptionCheckBox(CCVars.HudHeldItemShow, ShowHeldItemCheckBox);
Control.AddOptionCheckBox(CCVars.CombatModeIndicatorsPointShow, ShowCombatModeIndicatorsCheckBox);
Control.AddOptionCheckBox(CCVars.OpaqueStorageWindow, OpaqueStorageWindowCheckBox);
Control.AddOptionCheckBox(CCVars.ChatEnableFancyBubbles, FancySpeechBubblesCheckBox);
Control.AddOptionCheckBox(CCVars.ChatFancyNameBackground, FancyNameBackgroundsCheckBox);
Control.AddOptionCheckBox(CCVars.StaticStorageUI, StaticStorageUI);
Control.Initialize();
}
}
@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Client.Animations;
using Content.Shared.Hands;
@@ -69,7 +69,7 @@ public sealed class StorageSystem : SharedStorageSystem
public void CloseStorageWindow(Entity<StorageComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
if (!Resolve(entity, ref entity.Comp, false))
return;
if (!_openStorages.Contains((entity, entity.Comp)))
@@ -52,20 +52,12 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
TooltipSupplier = SupplyTooltip;
Alert = alert;
_severity = severity;
_spriteViewEntity = _entityManager.Spawn(Alert.AlertViewEntity);
if (_entityManager.TryGetComponent<SpriteComponent>(_spriteViewEntity, out var sprite))
{
var icon = Alert.GetIcon(_severity);
if (sprite.LayerMapTryGet(AlertVisualLayers.Base, out var layer))
sprite.LayerSetSprite(layer, icon);
}
_icon = new SpriteView
{
Scale = new Vector2(2, 2)
};
_icon.SetEntity(_spriteViewEntity);
SetupIcon();
Children.Add(_icon);
_cooldownGraphic = new CooldownGraphic
@@ -113,6 +105,36 @@ namespace Content.Client.UserInterface.Systems.Alerts.Controls
_cooldownGraphic.FromTime(Cooldown.Value.Start, Cooldown.Value.End);
}
private void SetupIcon()
{
if (!_entityManager.Deleted(_spriteViewEntity))
_entityManager.QueueDeleteEntity(_spriteViewEntity);
_spriteViewEntity = _entityManager.Spawn(Alert.AlertViewEntity);
if (_entityManager.TryGetComponent<SpriteComponent>(_spriteViewEntity, out var sprite))
{
var icon = Alert.GetIcon(_severity);
if (sprite.LayerMapTryGet(AlertVisualLayers.Base, out var layer))
sprite.LayerSetSprite(layer, icon);
}
_icon.SetEntity(_spriteViewEntity);
}
protected override void EnteredTree()
{
base.EnteredTree();
SetupIcon();
}
protected override void ExitedTree()
{
base.ExitedTree();
if (!_entityManager.Deleted(_spriteViewEntity))
_entityManager.QueueDeleteEntity(_spriteViewEntity);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
@@ -4,10 +4,11 @@ using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Content.Client.Stylesheets;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using FancyWindow = Content.Client.UserInterface.Controls.FancyWindow;
using Content.Shared.IdentityManagement;
using Robust.Shared.Timing;
namespace Content.Client.VendingMachines.UI
{
@@ -15,6 +16,10 @@ namespace Content.Client.VendingMachines.UI
public sealed partial class VendingMachineMenu : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly Dictionary<EntProtoId, EntityUid> _dummies = [];
public event Action<ItemList.ItemListSelectedEventArgs>? OnItemSelected;
public event Action<string>? OnSearchChanged;
@@ -37,6 +42,22 @@ namespace Content.Client.VendingMachines.UI
};
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
// Don't clean up dummies during disposal or we'll just have to spawn them again
if (!disposing)
return;
// Delete any dummy items we spawned
foreach (var entity in _dummies.Values)
{
_entityManager.QueueDeleteEntity(entity);
}
_dummies.Clear();
}
/// <summary>
/// Populates the list of available items on the vending machine interface
/// and sets icons based on their prototypes
@@ -73,11 +94,16 @@ namespace Content.Client.VendingMachines.UI
vendingItem.Text = string.Empty;
vendingItem.Icon = null;
var itemName = entry.ID;
if (!_dummies.TryGetValue(entry.ID, out var dummy))
{
dummy = _entityManager.Spawn(entry.ID);
_dummies.Add(entry.ID, dummy);
}
var itemName = Identity.Name(dummy, _entityManager);
Texture? icon = null;
if (_prototypeManager.TryIndex<EntityPrototype>(entry.ID, out var prototype))
{
itemName = prototype.Name;
icon = spriteSystem.GetPrototypeIcon(prototype).Default;
}
@@ -0,0 +1,88 @@
using Content.Server.Maps;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
namespace Content.IntegrationTests.Tests.Commands;
[TestFixture]
public sealed class ForceMapTest
{
private const string DefaultMapName = "Empty";
private const string BadMapName = "asdf_asd-fa__sdfAsd_f"; // Hopefully no one ever names a map this...
private const string TestMapEligibleName = "ForceMapTestEligible";
private const string TestMapIneligibleName = "ForceMapTestIneligible";
[TestPrototypes]
private static readonly string TestMaps = @$"
- type: gameMap
id: {TestMapIneligibleName}
mapName: {TestMapIneligibleName}
mapPath: /Maps/Test/empty.yml
minPlayers: 20
maxPlayers: 80
stations:
Empty:
stationProto: StandardNanotrasenStation
components:
- type: StationNameSetup
mapNameTemplate: ""Empty""
- type: gameMap
id: {TestMapEligibleName}
mapName: {TestMapEligibleName}
mapPath: /Maps/Test/empty.yml
minPlayers: 0
stations:
Empty:
stationProto: StandardNanotrasenStation
components:
- type: StationNameSetup
mapNameTemplate: ""Empty""
";
[Test]
public async Task TestForceMapCommand()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var entMan = server.EntMan;
var configManager = server.ResolveDependency<IConfigurationManager>();
var consoleHost = server.ResolveDependency<IConsoleHost>();
var gameMapMan = server.ResolveDependency<IGameMapManager>();
await server.WaitAssertion(() =>
{
// Make sure we're set to the default map
Assert.That(gameMapMan.GetSelectedMap()?.ID, Is.EqualTo(DefaultMapName),
$"Test didn't start on expected map ({DefaultMapName})!");
// Try changing to a map that doesn't exist
consoleHost.ExecuteCommand($"forcemap {BadMapName}");
Assert.That(gameMapMan.GetSelectedMap()?.ID, Is.EqualTo(DefaultMapName),
$"Forcemap succeeded with a map that does not exist ({BadMapName})!");
// Try changing to a valid map
consoleHost.ExecuteCommand($"forcemap {TestMapEligibleName}");
Assert.That(gameMapMan.GetSelectedMap()?.ID, Is.EqualTo(TestMapEligibleName),
$"Forcemap failed with a valid map ({TestMapEligibleName})");
// Try changing to a map that exists but is ineligible
consoleHost.ExecuteCommand($"forcemap {TestMapIneligibleName}");
Assert.That(gameMapMan.GetSelectedMap()?.ID, Is.EqualTo(TestMapIneligibleName),
$"Forcemap failed with valid but ineligible map ({TestMapIneligibleName})!");
// Try clearing the force-selected map
consoleHost.ExecuteCommand("forcemap \"\"");
Assert.That(gameMapMan.GetSelectedMap(), Is.Null,
$"Running 'forcemap \"\"' did not clear the forced map!");
});
// Cleanup
configManager.SetCVar(CCVars.GameMap, DefaultMapName);
await pair.CleanReturnAsync();
}
}
@@ -0,0 +1,89 @@
using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Systems;
using Content.Server.Station.Systems;
using Content.Shared.Preferences;
using Content.Shared.Roles.Jobs;
namespace Content.IntegrationTests.Tests.Internals;
[TestFixture]
[TestOf(typeof(InternalsSystem))]
public sealed class AutoInternalsTests
{
[Test]
public async Task TestInternalsAutoActivateInSpaceForStationSpawn()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var testMap = await pair.CreateTestMap();
var stationSpawning = server.System<StationSpawningSystem>();
var atmos = server.System<AtmosphereSystem>();
var internals = server.System<InternalsSystem>();
await server.WaitAssertion(() =>
{
var profile = new HumanoidCharacterProfile();
var dummy = stationSpawning.SpawnPlayerMob(testMap.GridCoords, new JobComponent()
{
Prototype = "TestInternalsDummy"
}, profile, station: null);
Assert.That(atmos.HasAtmosphere(testMap.Grid), Is.False, "Test map has atmosphere - test needs adjustment!");
Assert.That(internals.AreInternalsWorking(dummy), "Internals did not automatically connect!");
server.EntMan.DeleteEntity(dummy);
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestInternalsAutoActivateInSpaceForEntitySpawn()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var testMap = await pair.CreateTestMap();
var atmos = server.System<AtmosphereSystem>();
var internals = server.System<InternalsSystem>();
await server.WaitAssertion(() =>
{
var dummy = server.EntMan.Spawn("TestInternalsDummyEntity", testMap.MapCoords);
Assert.That(atmos.HasAtmosphere(testMap.Grid), Is.False, "Test map has atmosphere - test needs adjustment!");
Assert.That(internals.AreInternalsWorking(dummy), "Internals did not automatically connect!");
server.EntMan.DeleteEntity(dummy);
});
await pair.CleanReturnAsync();
}
[TestPrototypes]
private const string Prototypes = @"
- type: playTimeTracker
id: PlayTimeInternalsDummy
- type: startingGear
id: InternalsDummyGear
equipment:
mask: ClothingMaskBreath
suitstorage: OxygenTankFilled
- type: job
id: TestInternalsDummy
playTimeTracker: PlayTimeInternalsDummy
startingGear: InternalsDummyGear
- type: entity
id: TestInternalsDummyEntity
parent: MobHuman
components:
- type: Loadout
prototypes: [InternalsDummyGear]
";
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class ConnectionLogTimeIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_connection_log_time",
table: "connection_log",
column: "time");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_connection_log_time",
table: "connection_log");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace Content.Server.Database.Migrations.Postgres
{
/// <inheritdoc />
public partial class BanTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ban_template",
columns: table => new
{
ban_template_id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
title = table.Column<string>(type: "text", nullable: false),
length = table.Column<TimeSpan>(type: "interval", nullable: false),
reason = table.Column<string>(type: "text", nullable: false),
exempt_flags = table.Column<int>(type: "integer", nullable: false),
severity = table.Column<int>(type: "integer", nullable: false),
auto_delete = table.Column<bool>(type: "boolean", nullable: false),
hidden = table.Column<bool>(type: "boolean", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_template", x => x.ban_template_id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ban_template");
}
}
}
@@ -512,6 +512,51 @@ namespace Content.Server.Database.Migrations.Postgres
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("ban_template_id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<bool>("AutoDelete")
.HasColumnType("boolean")
.HasColumnName("auto_delete");
b.Property<int>("ExemptFlags")
.HasColumnType("integer")
.HasColumnName("exempt_flags");
b.Property<bool>("Hidden")
.HasColumnType("boolean")
.HasColumnName("hidden");
b.Property<TimeSpan>("Length")
.HasColumnType("interval")
.HasColumnName("length");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("text")
.HasColumnName("reason");
b.Property<int>("Severity")
.HasColumnType("integer")
.HasColumnName("severity");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("text")
.HasColumnName("title");
b.HasKey("Id")
.HasName("PK_ban_template");
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -559,6 +604,8 @@ namespace Content.Server.Database.Migrations.Postgres
b.HasIndex("ServerId")
.HasDatabaseName("IX_connection_log_server_id");
b.HasIndex("Time");
b.HasIndex("UserId");
b.ToTable("connection_log", null, t =>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,27 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class ConnectionLogTimeIndex : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateIndex(
name: "IX_connection_log_time",
table: "connection_log",
column: "time");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(
name: "IX_connection_log_time",
table: "connection_log");
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Content.Server.Database.Migrations.Sqlite
{
/// <inheritdoc />
public partial class BanTemplate : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ban_template",
columns: table => new
{
ban_template_id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
title = table.Column<string>(type: "TEXT", nullable: false),
length = table.Column<TimeSpan>(type: "TEXT", nullable: false),
reason = table.Column<string>(type: "TEXT", nullable: false),
exempt_flags = table.Column<int>(type: "INTEGER", nullable: false),
severity = table.Column<int>(type: "INTEGER", nullable: false),
auto_delete = table.Column<bool>(type: "INTEGER", nullable: false),
hidden = table.Column<bool>(type: "INTEGER", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_ban_template", x => x.ban_template_id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ban_template");
}
}
}
@@ -483,6 +483,49 @@ namespace Content.Server.Database.Migrations.Sqlite
b.ToTable("assigned_user_id", (string)null);
});
modelBuilder.Entity("Content.Server.Database.BanTemplate", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER")
.HasColumnName("ban_template_id");
b.Property<bool>("AutoDelete")
.HasColumnType("INTEGER")
.HasColumnName("auto_delete");
b.Property<int>("ExemptFlags")
.HasColumnType("INTEGER")
.HasColumnName("exempt_flags");
b.Property<bool>("Hidden")
.HasColumnType("INTEGER")
.HasColumnName("hidden");
b.Property<TimeSpan>("Length")
.HasColumnType("TEXT")
.HasColumnName("length");
b.Property<string>("Reason")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("reason");
b.Property<int>("Severity")
.HasColumnType("INTEGER")
.HasColumnName("severity");
b.Property<string>("Title")
.IsRequired()
.HasColumnType("TEXT")
.HasColumnName("title");
b.HasKey("Id")
.HasName("PK_ban_template");
b.ToTable("ban_template", (string)null);
});
modelBuilder.Entity("Content.Server.Database.ConnectionLog", b =>
{
b.Property<int>("Id")
@@ -528,6 +571,8 @@ namespace Content.Server.Database.Migrations.Sqlite
b.HasIndex("ServerId")
.HasDatabaseName("IX_connection_log_server_id");
b.HasIndex("Time");
b.HasIndex("UserId");
b.ToTable("connection_log", (string)null);
+57
View File
@@ -41,6 +41,7 @@ namespace Content.Server.Database
public DbSet<AdminWatchlist> AdminWatchlists { get; set; } = null!;
public DbSet<AdminMessage> AdminMessages { get; set; } = null!;
public DbSet<RoleWhitelist> RoleWhitelists { get; set; } = null!;
public DbSet<BanTemplate> BanTemplate { get; set; } = null!;
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
@@ -187,6 +188,9 @@ namespace Content.Server.Database
modelBuilder.Entity<ConnectionLog>()
.HasIndex(p => p.UserId);
modelBuilder.Entity<ConnectionLog>()
.HasIndex(p => p.Time);
modelBuilder.Entity<ConnectionLog>()
.Property(p => p.ServerId)
.HasDefaultValue(0);
@@ -1132,4 +1136,57 @@ namespace Content.Server.Database
[Required]
public string RoleId { get; set; } = default!;
}
/// <summary>
/// Defines a template that admins can use to quickly fill out ban information.
/// </summary>
/// <remarks>
/// <para>
/// This information is not currently used by the game itself, but it is used by SS14.Admin.
/// </para>
/// </remarks>
public sealed class BanTemplate
{
public int Id { get; set; }
/// <summary>
/// Title of the ban template. This is purely for reference by admins and not copied into the ban.
/// </summary>
public required string Title { get; set; }
/// <summary>
/// How long the ban should last. 0 for permanent.
/// </summary>
public TimeSpan Length { get; set; }
/// <summary>
/// The reason for the ban.
/// </summary>
/// <seealso cref="ServerBan.Reason"/>
public string Reason { get; set; } = "";
/// <summary>
/// Exemptions granted to the ban.
/// </summary>
/// <seealso cref="ServerBan.ExemptFlags"/>
public ServerBanExemptFlags ExemptFlags { get; set; }
/// <summary>
/// Severity of the ban
/// </summary>
/// <seealso cref="ServerBan.Severity"/>
public NoteSeverity Severity { get; set; }
/// <summary>
/// Ban will be automatically deleted once expired.
/// </summary>
/// <seealso cref="ServerBan.AutoDelete"/>
public bool AutoDelete { get; set; }
/// <summary>
/// Ban is not visible to players in the remarks menu.
/// </summary>
/// <seealso cref="ServerBan.Hidden"/>
public bool Hidden { get; set; }
}
}
+74
View File
@@ -0,0 +1,74 @@
using Content.Server.Wires;
using Content.Shared.Access;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Emag.Components;
using Content.Shared.Wires;
namespace Content.Server.Access;
public sealed partial class LogWireAction : ComponentWireAction<AccessReaderComponent>
{
public override Color Color { get; set; } = Color.Blue;
public override string Name { get; set; } = "wire-name-log";
[DataField]
public int PulseTimeout = 30;
[DataField]
public LocId PulseLog = "log-wire-pulse-access-log";
private AccessReaderSystem _access = default!;
public override StatusLightState? GetLightState(Wire wire, AccessReaderComponent comp)
{
return comp.LoggingDisabled ? StatusLightState.Off : StatusLightState.On;
}
public override object StatusKey => AccessWireActionKey.Status;
public override void Initialize()
{
base.Initialize();
_access = EntityManager.System<AccessReaderSystem>();
}
public override bool Cut(EntityUid user, Wire wire, AccessReaderComponent comp)
{
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
comp.LoggingDisabled = true;
EntityManager.Dirty(wire.Owner, comp);
return true;
}
public override bool Mend(EntityUid user, Wire wire, AccessReaderComponent comp)
{
comp.LoggingDisabled = false;
return true;
}
public override void Pulse(EntityUid user, Wire wire, AccessReaderComponent comp)
{
_access.LogAccess((wire.Owner, comp), Loc.GetString(PulseLog));
comp.LoggingDisabled = true;
WiresSystem.StartWireAction(wire.Owner, PulseTimeout, PulseTimeoutKey.Key, new TimedWireEvent(AwaitPulseCancel, wire));
}
public override void Update(Wire wire)
{
if (!IsPowered(wire.Owner))
WiresSystem.TryCancelWireAction(wire.Owner, PulseTimeoutKey.Key);
}
private void AwaitPulseCancel(Wire wire)
{
if (!wire.IsCut && EntityManager.TryGetComponent<AccessReaderComponent>(wire.Owner, out var comp))
comp.LoggingDisabled = false;
}
private enum PulseTimeoutKey : byte
{
Key
}
}
@@ -40,7 +40,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("underPressureLockoutThreshold")]
public float UnderPressureLockoutThreshold = 60; // this must be tuned in conjunction with atmos.mmos_spacing_speed
public float UnderPressureLockoutThreshold = 80; // this must be tuned in conjunction with atmos.mmos_spacing_speed
/// <summary>
/// Pressure locked vents still leak a little (leading to eventual pressurization of sealed sections)
@@ -1,8 +1,10 @@
using System.Collections.Immutable;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Content.Corvax.Interfaces.Server;
using Content.Corvax.Interfaces.Shared;
using Content.Server.Chat.Managers;
using Content.Server.Database;
using Content.Server.GameTicking;
using Content.Server.Preferences.Managers;
@@ -12,7 +14,9 @@ using Content.Shared.GameTicking;
using Content.Shared.Players.PlayTimeTracking;
using Robust.Server.Player;
using Robust.Shared.Configuration;
using Robust.Shared.Enums;
using Robust.Shared.Network;
using Robust.Shared.Player;
using Robust.Shared.Timing;
/*
@@ -53,6 +57,7 @@ namespace Content.Server.Connection
[Dependency] private readonly ServerDbEntryManager _serverDbEntry = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly IChatManager _chatManager = default!;
private ISharedSponsorsManager? _sponsorsMgr; // Corvax-Sponsors
private IServerVPNGuardManager? _vpnGuardMgr; // Corvax-VPNGuard
@@ -66,6 +71,7 @@ namespace Content.Server.Connection
IoCManager.Instance!.TryResolveType(out _sponsorsMgr); // Corvax-Sponsors
_netMgr.Connecting += NetMgrOnConnecting;
_netMgr.AssignUserIdCallback = AssignUserIdCallback;
_plyMgr.PlayerStatusChanged += PlayerStatusChanged;
// Approval-based IP bans disabled because they don't play well with Happy Eyeballs.
// _netMgr.HandleApprovalCallback = HandleApproval;
}
@@ -134,6 +140,42 @@ namespace Content.Server.Connection
}
}
private async void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
{
if (args.NewStatus == SessionStatus.Connected)
{
AdminAlertIfSharedConnection(args.Session);
}
}
private void AdminAlertIfSharedConnection(ICommonSession newSession)
{
var playerThreshold = _cfg.GetCVar(CCVars.AdminAlertMinPlayersSharingConnection);
if (playerThreshold < 0)
return;
var addr = newSession.Channel.RemoteEndPoint.Address;
var otherConnectionsFromAddress = _plyMgr.Sessions.Where(session =>
session.Status is SessionStatus.Connected or SessionStatus.InGame
&& session.Channel.RemoteEndPoint.Address.Equals(addr)
&& session.UserId != newSession.UserId)
.ToList();
var otherConnectionCount = otherConnectionsFromAddress.Count;
if (otherConnectionCount + 1 < playerThreshold) // Add one for the total, not just others, using the address
return;
var username = newSession.Name;
var otherUsernames = string.Join(", ",
otherConnectionsFromAddress.Select(session => session.Name));
_chatManager.SendAdminAlert(Loc.GetString("admin-alert-shared-connection",
("player", username),
("otherCount", otherConnectionCount),
("otherList", otherUsernames)));
}
/*
* TODO: Jesus H Christ what is this utter mess of a function
* TODO: Break this apart into is constituent steps.
@@ -4,7 +4,7 @@ using Robust.Shared.Console;
namespace Content.Server.Connection;
[AdminCommand(AdminFlags.Admin)]
[AdminCommand(AdminFlags.Moderator)]
public sealed class GrantConnectBypassCommand : LocalizedCommands
{
private static readonly TimeSpan DefaultDuration = TimeSpan.FromHours(1);
@@ -0,0 +1,43 @@
using Content.Server.Ame.Components;
using Content.Shared.Construction;
using JetBrains.Annotations;
using Content.Shared.Examine;
namespace Content.Server.Construction.Conditions;
[UsedImplicitly]
[DataDefinition]
public sealed partial class AmeShieldIntegrity : IGraphCondition
{
[DataField]
public float IntegrityThreshold = 80;
/// <summary>
/// If true, checks for the integrity being above the threshold.
/// if false, checks for it being below.
/// </summary>
[DataField]
public bool CheckAbove = true;
public bool Condition(EntityUid uid, IEntityManager entityManager)
{
if (!entityManager.TryGetComponent<AmeShieldComponent>(uid, out var shield))
return true;
if (CheckAbove)
{
return shield.CoreIntegrity >= IntegrityThreshold;
}
return shield.CoreIntegrity < IntegrityThreshold;
}
public bool DoExamine(ExaminedEvent args)
{
return false;
}
public IEnumerable<ConstructionGuideEntry> GenerateGuideEntry()
{
yield return new ConstructionGuideEntry();
}
}
@@ -17,6 +17,7 @@ using Content.Shared.Disposal.Components;
using Content.Shared.DoAfter;
using Content.Shared.DragDrop;
using Content.Shared.Emag.Systems;
using Content.Shared.Explosion;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
@@ -76,6 +77,7 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
SubscribeLocalEvent<DisposalUnitComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<DisposalUnitComponent, DragDropTargetEvent>(OnDragDropOn);
SubscribeLocalEvent<DisposalUnitComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<DisposalUnitComponent, BeforeExplodeEvent>(OnExploded);
SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<InteractionVerb>>(AddInsertVerb);
SubscribeLocalEvent<DisposalUnitComponent, GetVerbsEvent<AlternativeVerb>>(AddDisposalAltVerbs);
@@ -778,6 +780,12 @@ public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
Joints.RecursiveClearJoints(inserted);
UpdateVisualState(uid, component);
}
private void OnExploded(Entity<DisposalUnitComponent> ent, ref BeforeExplodeEvent args)
{
args.Contents.AddRange(ent.Comp.Container.ContainedEntities);
}
}
/// <summary>
@@ -93,7 +93,7 @@ namespace Content.Server.Doors.Systems
if (!TryComp<DoorComponent>(uid, out var doorComponent))
return;
if (args.AlarmType == AtmosAlarmType.Normal || args.AlarmType == AtmosAlarmType.Warning)
if (args.AlarmType == AtmosAlarmType.Normal)
{
if (doorComponent.State == DoorState.Closed)
_doorSystem.TryOpen(uid);
@@ -29,8 +29,19 @@ namespace Content.Server.GameTicking.Commands
var gameMap = IoCManager.Resolve<IGameMapManager>();
var name = args[0];
// An empty string clears the forced map
if (!string.IsNullOrEmpty(name) && !gameMap.CheckMapExists(name))
{
shell.WriteLine(Loc.GetString("forcemap-command-map-not-found", ("map", name)));
return;
}
_configurationManager.SetCVar(CCVars.GameMap, name);
shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name)));
if (string.IsNullOrEmpty(name))
shell.WriteLine(Loc.GetString("forcemap-command-cleared"));
else
shell.WriteLine(Loc.GetString("forcemap-command-success", ("map", name)));
}
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
+1 -15
View File
@@ -5,7 +5,6 @@ using Content.Shared.Examine;
using Content.Shared.Labels;
using Content.Shared.Labels.Components;
using Content.Shared.Labels.EntitySystems;
using Content.Shared.NameModifier.EntitySystems;
using JetBrains.Annotations;
using Robust.Shared.Containers;
@@ -19,7 +18,6 @@ namespace Content.Server.Labels
{
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly NameModifierSystem _nameMod = default!;
public const string ContainerName = "paper_label";
@@ -27,7 +25,6 @@ namespace Content.Server.Labels
{
base.Initialize();
SubscribeLocalEvent<LabelComponent, MapInitEvent>(OnLabelCompMapInit);
SubscribeLocalEvent<PaperLabelComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<PaperLabelComponent, ComponentRemove>(OnComponentRemove);
SubscribeLocalEvent<PaperLabelComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
@@ -35,17 +32,6 @@ namespace Content.Server.Labels
SubscribeLocalEvent<PaperLabelComponent, ExaminedEvent>(OnExamined);
}
private void OnLabelCompMapInit(EntityUid uid, LabelComponent component, MapInitEvent args)
{
if (!string.IsNullOrEmpty(component.CurrentLabel))
{
component.CurrentLabel = Loc.GetString(component.CurrentLabel);
Dirty(uid, component);
}
_nameMod.RefreshNameModifiers(uid);
}
/// <summary>
/// Apply or remove a label on an entity.
/// </summary>
@@ -59,7 +45,7 @@ namespace Content.Server.Labels
label = EnsureComp<LabelComponent>(uid);
label.CurrentLabel = text;
_nameMod.RefreshNameModifiers(uid);
NameMod.RefreshNameModifiers(uid);
Dirty(uid, label);
}
@@ -26,7 +26,7 @@ public sealed class ContainmentFieldSystem : EntitySystem
{
var otherBody = args.OtherEntity;
if (HasComp<SpaceGarbageComponent>(otherBody))
if (component.DestroyGarbage && HasComp<SpaceGarbageComponent>(otherBody))
{
_popupSystem.PopupEntity(Loc.GetString("comp-field-vaporized", ("entity", otherBody)), uid, PopupType.LargeCaution);
QueueDel(otherBody);
@@ -65,6 +65,13 @@ public sealed partial class AccessReaderComponent : Component
[DataField, ViewVariables(VVAccess.ReadWrite)]
public int AccessLogLimit = 20;
/// <summary>
/// If true logging on successful access uses will be disabled.
/// Can be set by LOG wire.
/// </summary>
[DataField]
public bool LoggingDisabled;
/// <summary>
/// Whether or not emag interactions have an effect on this.
/// </summary>
@@ -382,18 +382,15 @@ public sealed class AccessReaderSystem : EntitySystem
}
/// <summary>
/// Logs an access
/// Logs an access for a specific entity.
/// </summary>
/// <param name="ent">The reader to log the access on</param>
/// <param name="accessor">The accessor to log</param>
private void LogAccess(Entity<AccessReaderComponent> ent, EntityUid accessor)
public void LogAccess(Entity<AccessReaderComponent> ent, EntityUid accessor)
{
if (IsPaused(ent))
if (IsPaused(ent) || ent.Comp.LoggingDisabled)
return;
if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
ent.Comp.AccessLog.Dequeue();
string? name = null;
if (TryComp<NameIdentifierComponent>(accessor, out var nameIdentifier))
name = nameIdentifier.FullIdentifier;
@@ -404,7 +401,21 @@ public sealed class AccessReaderSystem : EntitySystem
&& idCard.Comp is { BypassLogging: false, FullName: not null })
name = idCard.Comp.FullName;
name ??= Loc.GetString("access-reader-unknown-id");
LogAccess(ent, name ?? Loc.GetString("access-reader-unknown-id"));
}
/// <summary>
/// Logs an access with a predetermined name
/// </summary>
/// <param name="ent">The reader to log the access on</param>
/// <param name="name">The name to log as</param>
public void LogAccess(Entity<AccessReaderComponent> ent, string name)
{
if (IsPaused(ent) || ent.Comp.LoggingDisabled)
return;
if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
ent.Comp.AccessLog.Dequeue();
var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
ent.Comp.AccessLog.Enqueue(new AccessRecord(stationTime, name));
+2 -1
View File
@@ -286,7 +286,8 @@ namespace Content.Shared.Atmos
/// (The pressure threshold is so low that it doesn't make sense to do any calculations,
/// so it just applies this flat value).
/// </summary>
public const int LowPressureDamage = 4;
// Original value is 4, buff back when we have proper ways for players to deal with breaches.
public const int LowPressureDamage = 1;
public const float WindowHeatTransferCoefficient = 0.1f;
+9
View File
@@ -832,6 +832,15 @@ namespace Content.Shared.CCVar
public static readonly CVarDef<bool> ServerBanErasePlayer =
CVarDef.Create("admin.server_ban_erase_player", false, CVar.ARCHIVE | CVar.SERVER | CVar.REPLICATED);
/// <summary>
/// Minimum players sharing a connection required to create an alert. -1 to disable the alert.
/// </summary>
/// <remarks>
/// If you set this to 0 or 1 then it will alert on every connection, so probably don't do that.
/// </remarks>
public static readonly CVarDef<int> AdminAlertMinPlayersSharingConnection =
CVarDef.Create("admin.alert.min_players_sharing_connection", -1, CVar.SERVERONLY);
/// <summary>
/// Minimum explosion intensity to create an admin alert message. -1 to disable the alert.
/// </summary>
+3 -1
View File
@@ -1,4 +1,5 @@
using System.Linq;
using Content.Shared.Body.Systems;
using Content.Shared.Clothing.Components;
using Content.Shared.Humanoid;
using Content.Shared.Preferences;
@@ -27,7 +28,8 @@ public sealed class LoadoutSystem : EntitySystem
{
base.Initialize();
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit);
// Wait until the character has all their organs before we give them their loadout
SubscribeLocalEvent<LoadoutComponent, MapInitEvent>(OnMapInit, after: [typeof(SharedBodySystem)]);
}
public static string GetJobPrototype(string? loadout)
@@ -54,7 +54,8 @@ public sealed class PacificationSystem : EntitySystem
&& !(_timing.CurTime > user.Comp.NextPopupTime))
return;
_popup.PopupClient(Loc.GetString(reason, ("entity", target)), user, user);
var targetName = Identity.Entity(target, EntityManager);
_popup.PopupClient(Loc.GetString(reason, ("entity", targetName)), user, user);
user.Comp.NextPopupTime = _timing.CurTime + user.Comp.PopupCooldown;
user.Comp.LastAttackedEntity = target;
}
@@ -21,6 +21,7 @@ public abstract class SharedFirelockSystem : EntitySystem
// Access/Prying
SubscribeLocalEvent<FirelockComponent, BeforeDoorOpenedEvent>(OnBeforeDoorOpened);
SubscribeLocalEvent<FirelockComponent, BeforePryEvent>(OnBeforePry);
SubscribeLocalEvent<FirelockComponent, GetPryTimeModifierEvent>(OnDoorGetPryTimeModifier);
SubscribeLocalEvent<FirelockComponent, PriedEvent>(OnAfterPried);
@@ -60,6 +61,14 @@ public abstract class SharedFirelockSystem : EntitySystem
WarnPlayer((uid, component), args.User.Value);
}
private void OnBeforePry(EntityUid uid, FirelockComponent component, ref BeforePryEvent args)
{
if (args.Cancelled || !component.Powered || args.StrongPry || args.PryPowered)
return;
args.Cancelled = true;
}
private void OnDoorGetPryTimeModifier(EntityUid uid, FirelockComponent component, ref GetPryTimeModifierEvent args)
{
WarnPlayer((uid, component), args.User);
@@ -1,3 +1,4 @@
using Content.Shared.Ghost;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
@@ -14,6 +15,8 @@ namespace Content.Shared.Examine
base.Initialize();
SubscribeLocalEvent<GroupExamineComponent, GetVerbsEvent<ExamineVerb>>(OnGroupExamineVerb);
_ghostQuery = GetEntityQuery<GhostComponent>();
}
/// <summary>
@@ -3,6 +3,7 @@ using System.Globalization;
using System.Linq;
using System.Text;
using Content.Shared.Eye.Blinding.Components;
using Content.Shared.Ghost;
using Content.Shared.Interaction;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -44,6 +45,8 @@ namespace Content.Shared.Examine
protected const float ExamineBlurrinessMult = 2.5f;
private EntityQuery<GhostComponent> _ghostQuery;
/// <summary>
/// Creates a new examine tooltip with arbitrary info.
/// </summary>
@@ -54,6 +57,10 @@ namespace Content.Shared.Examine
if (IsClientSide(entity))
return true;
// Ghosts can see everything.
if (_ghostQuery.HasComp(examiner))
return true;
// check if the mob is in critical or dead
if (MobStateSystem.IsIncapacitated(examiner))
return false;
@@ -7,14 +7,27 @@ namespace Content.Shared.Labels.EntitySystems;
public abstract partial class SharedLabelSystem : EntitySystem
{
[Dependency] protected readonly NameModifierSystem NameMod = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<LabelComponent, MapInitEvent>(OnLabelCompMapInit);
SubscribeLocalEvent<LabelComponent, ExaminedEvent>(OnExamine);
SubscribeLocalEvent<LabelComponent, RefreshNameModifiersEvent>(OnRefreshNameModifiers);
}
private void OnLabelCompMapInit(EntityUid uid, LabelComponent component, MapInitEvent args)
{
if (!string.IsNullOrEmpty(component.CurrentLabel))
{
component.CurrentLabel = Loc.GetString(component.CurrentLabel);
Dirty(uid, component);
}
NameMod.RefreshNameModifiers(uid);
}
public virtual void Label(EntityUid uid, string? text, MetaDataComponent? metadata = null, LabelComponent? label = null){}
private void OnExamine(EntityUid uid, LabelComponent? label, ExaminedEvent args)
@@ -42,7 +42,7 @@ public abstract class SharedMagicMirrorSystem : EntitySystem
return;
}
if (!_interaction.InRangeUnobstructed(uid, component.Target.Value))
if (!_interaction.InRangeUnobstructed(component.Target.Value, uid))
args.Result = BoundUserInterfaceRangeResult.Fail;
}
@@ -145,9 +145,12 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
// If you put invalid ones first but that's your fault for not using sensible defaults
if (loadouts.Count < groupProto.MinLimit)
{
for (var i = 0; i < Math.Min(groupProto.MinLimit, groupProtoLoadouts.Count); i++) // Corvax-Loadout: Use groupProtoLoadouts instead of groupProto.Loadouts
foreach (var protoId in groupProtoLoadouts) // Corvax-Loadout: Use groupProtoLoadouts instead of groupProto.Loadouts
{
if (!protoManager.TryIndex(groupProtoLoadouts[i], out var loadoutProto)) // Corvax-Loadout
if (loadouts.Count >= groupProto.MinLimit)
break;
if (!protoManager.TryIndex(protoId, out var loadoutProto))
continue;
var defaultLoadout = new Loadout()
@@ -214,11 +217,10 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
if (groupProto.MinLimit > 0)
{
// Apply any loadouts we can.
var addedCount = 0;
foreach (var protoId in groupProto.Loadouts)
{
// Reached the limit, time to stop
if (addedCount >= groupProto.MinLimit)
if (loadouts.Count >= groupProto.MinLimit)
break;
if (!protoManager.TryIndex(protoId, out var loadoutProto))
@@ -235,7 +237,6 @@ public sealed partial class RoleLoadout : IEquatable<RoleLoadout>
loadouts.Add(defaultLoadout);
Apply(loadoutProto);
addedCount++;
}
}
}
@@ -43,14 +43,26 @@ public sealed partial class PryingComponent : Component
/// Cancel to stop the entity from being pried open.
/// </summary>
[ByRefEvent]
public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force)
public record struct BeforePryEvent(EntityUid User, bool PryPowered, bool Force, bool StrongPry)
{
public readonly EntityUid User = User;
/// <summary>
/// Whether prying should be allowed even if whatever is being pried is powered.
/// </summary>
public readonly bool PryPowered = PryPowered;
/// <summary>
/// Whether prying should be allowed to go through under most circumstances. (E.g. airlock is bolted).
/// Systems may still wish to ignore this occasionally.
/// </summary>
public readonly bool Force = Force;
/// <summary>
/// Whether anything other than bare hands were used. This should only be false if prying is being performed without a prying comp.
/// </summary>
public readonly bool StrongPry = StrongPry;
public string? Message;
public bool Cancelled;
@@ -109,7 +109,7 @@ public sealed class PryingSystem : EntitySystem
if (comp != null || Resolve(user, ref comp, false))
{
canev = new BeforePryEvent(user, comp.PryPowered, comp.Force);
canev = new BeforePryEvent(user, comp.PryPowered, comp.Force, true);
}
else
{
@@ -119,7 +119,7 @@ public sealed class PryingSystem : EntitySystem
return false;
}
canev = new BeforePryEvent(user, false, false);
canev = new BeforePryEvent(user, false, false, false);
}
RaiseLocalEvent(target, ref canev);
@@ -18,4 +18,10 @@ public sealed partial class ContainmentFieldComponent : Component
/// </summary>
[DataField("maxMass")]
public float MaxMass = 10000f;
/// <summary>
/// Should field vaporize garbage that collides with it?
/// </summary>
[DataField]
public bool DestroyGarbage = true;
}
@@ -1,17 +1,17 @@
using Robust.Shared.Audio;
namespace Content.Shared.Sound.Components
{
/// <summary>
/// Base sound emitter which defines most of the data fields.
/// Accepts both single sounds and sound collections.
/// </summary>
public abstract partial class BaseEmitSoundComponent : Component
{
public static readonly AudioParams DefaultParams = AudioParams.Default.WithVolume(-2f);
namespace Content.Shared.Sound.Components;
[ViewVariables(VVAccess.ReadWrite)]
[DataField("sound", required: true)]
public SoundSpecifier? Sound;
}
/// <summary>
/// Base sound emitter which defines most of the data fields.
/// Accepts both single sounds and sound collections.
/// </summary>
public abstract partial class BaseEmitSoundComponent : Component
{
public static readonly AudioParams DefaultParams = AudioParams.Default.WithVolume(-2f);
[AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
[DataField(required: true)]
public SoundSpecifier? Sound;
}
@@ -96,6 +96,7 @@ public abstract class SharedStorageSystem : EntitySystem
subs.Event<BoundUIClosedEvent>(OnBoundUIClosed);
});
SubscribeLocalEvent<StorageComponent, ComponentRemove>(OnRemove);
SubscribeLocalEvent<StorageComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<StorageComponent, GetVerbsEvent<ActivationVerb>>(AddUiVerb);
SubscribeLocalEvent<StorageComponent, ComponentGetState>(OnStorageGetState);
@@ -133,6 +134,11 @@ public abstract class SharedStorageSystem : EntitySystem
UpdatePrototypeCache();
}
private void OnRemove(Entity<StorageComponent> entity, ref ComponentRemove args)
{
_ui.CloseUi(entity.Owner, StorageComponent.StorageUiKey.Key);
}
private void OnMapInit(Entity<StorageComponent> entity, ref MapInitEvent args)
{
UseDelay.SetLength(entity.Owner, entity.Comp.QuickInsertCooldown, QuickInsertUseDelayID);
@@ -21,42 +21,17 @@ public sealed partial class ReflectComponent : Component
[ViewVariables(VVAccess.ReadWrite), DataField("reflects")]
public ReflectType Reflects = ReflectType.Energy | ReflectType.NonEnergy;
/// <summary>
/// Probability for a projectile to be reflected.
/// </summary>
[DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float ReflectProb = 0.25f;
[DataField("spread"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public Angle Spread = Angle.FromDegrees(45);
[DataField("soundOnReflect")]
public SoundSpecifier? SoundOnReflect = new SoundPathSpecifier("/Audio/Weapons/Guns/Hits/laser_sear_wall.ogg");
/// <summary>
/// Is the deflection an innate power or something actively maintained? If true, this component grants a flat
/// deflection chance rather than a chance that degrades when moving/weightless/stunned/etc.
/// </summary>
[DataField]
public bool Innate = false;
/// <summary>
/// Maximum probability for a projectile to be reflected.
/// </summary>
[DataField("reflectProb"), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
public float ReflectProb = 0.25f;
/// <summary>
/// The maximum velocity a wielder can move at before losing effectiveness.
/// </summary>
[DataField]
public float VelocityBeforeNotMaxProb = 2.5f; // Walking speed for a human. Suitable for a weightless deflector like an e-sword.
/// <summary>
/// The velocity a wielder has to be moving at to use the minimum effectiveness value.
/// </summary>
[DataField]
public float VelocityBeforeMinProb = 4.5f; // Sprinting speed for a human. Suitable for a weightless deflector like an e-sword.
/// <summary>
/// Minimum probability for a projectile to be reflected.
/// </summary>
[DataField]
public float MinReflectProb = 0.1f;
}
[Flags]
+84 -190
View File
@@ -3,18 +3,16 @@ using System.Numerics;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Audio;
using Content.Shared.Damage.Components;
using Content.Shared.Database;
using Content.Shared.Gravity;
using Content.Shared.Hands;
using Content.Shared.Inventory;
using Content.Shared.Inventory.Events;
using Content.Shared.Item.ItemToggle.Components;
using Content.Shared.Popups;
using Content.Shared.Projectiles;
using Content.Shared.Standing;
using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Physics.Components;
@@ -38,134 +36,73 @@ public sealed class ReflectSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly StandingStateSystem _standing = default!;
[Dependency] private readonly AlertsSystem _alerts = default!;
[ValidatePrototypeId<AlertPrototype>]
private const string DeflectingAlert = "Deflecting";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReflectComponent, ProjectileReflectAttemptEvent>(OnObjectReflectProjectileAttempt);
SubscribeLocalEvent<ReflectComponent, HitScanReflectAttemptEvent>(OnObjectReflectHitscanAttempt);
SubscribeLocalEvent<ReflectComponent, ProjectileReflectAttemptEvent>(OnReflectCollide);
SubscribeLocalEvent<ReflectComponent, HitScanReflectAttemptEvent>(OnReflectHitscan);
SubscribeLocalEvent<ReflectComponent, GotEquippedEvent>(OnReflectEquipped);
SubscribeLocalEvent<ReflectComponent, GotUnequippedEvent>(OnReflectUnequipped);
SubscribeLocalEvent<ReflectComponent, GotEquippedHandEvent>(OnReflectHandEquipped);
SubscribeLocalEvent<ReflectComponent, GotUnequippedHandEvent>(OnReflectHandUnequipped);
SubscribeLocalEvent<ReflectComponent, ItemToggledEvent>(OnToggleReflect);
SubscribeLocalEvent<ReflectUserComponent, ProjectileReflectAttemptEvent>(OnUserProjectileReflectAttempt);
SubscribeLocalEvent<ReflectUserComponent, HitScanReflectAttemptEvent>(OnUserHitscanReflectAttempt);
SubscribeLocalEvent<ReflectUserComponent, ProjectileReflectAttemptEvent>(OnReflectUserCollide);
SubscribeLocalEvent<ReflectUserComponent, HitScanReflectAttemptEvent>(OnReflectUserHitscan);
}
private void OnUserHitscanReflectAttempt(Entity<ReflectUserComponent> user, ref HitScanReflectAttemptEvent args)
private void OnReflectUserHitscan(EntityUid uid, ReflectUserComponent component, ref HitScanReflectAttemptEvent args)
{
if (args.Reflected)
return;
if (!UserCanReflect(user, out var bestReflectorUid))
return;
if (!TryReflectHitscan(user.Owner, bestReflectorUid.Value, args.Shooter, args.SourceItem, args.Direction, out var dir))
return;
args.Direction = dir.Value;
args.Reflected = true;
}
private void OnUserProjectileReflectAttempt(Entity<ReflectUserComponent> user, ref ProjectileReflectAttemptEvent args)
{
if (args.Cancelled)
return;
if (!TryComp<ReflectiveComponent>(args.ProjUid, out var reflectiveComponent))
return;
if (!UserCanReflect(user, out var bestReflectorUid, (args.ProjUid, reflectiveComponent)))
return;
if (!TryReflectProjectile(user, bestReflectorUid.Value, (args.ProjUid, args.Component)))
return;
args.Cancelled = true;
}
private void OnObjectReflectHitscanAttempt(Entity<ReflectComponent> obj, ref HitScanReflectAttemptEvent args)
{
if (args.Reflected || (obj.Comp.Reflects & args.Reflective) == 0x0)
return;
if (!TryReflectHitscan(obj, obj, args.Shooter, args.SourceItem, args.Direction, out var dir))
return;
args.Direction = dir.Value;
args.Reflected = true;
}
private void OnObjectReflectProjectileAttempt(Entity<ReflectComponent> obj, ref ProjectileReflectAttemptEvent args)
{
if (args.Cancelled)
return;
if (!TryReflectProjectile(obj, obj, (args.ProjUid, args.Component)))
return;
args.Cancelled = true;
}
/// <summary>
/// Can a user reflect something that's hit them? Returns true if so, and the best reflector available in the user's equipment.
/// </summary>
private bool UserCanReflect(Entity<ReflectUserComponent> user, [NotNullWhen(true)] out Entity<ReflectComponent>? bestReflector, Entity<ReflectiveComponent>? projectile = null)
{
bestReflector = null;
foreach (var entityUid in _inventorySystem.GetHandOrInventoryEntities(user.Owner, SlotFlags.WITHOUT_POCKET))
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
{
if (!TryComp<ReflectComponent>(entityUid, out var comp))
if (!TryReflectHitscan(uid, ent, args.Shooter, args.SourceItem, args.Direction, out var dir))
continue;
if (!comp.Enabled)
args.Direction = dir.Value;
args.Reflected = true;
break;
}
}
private void OnReflectUserCollide(EntityUid uid, ReflectUserComponent component, ref ProjectileReflectAttemptEvent args)
{
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(uid, SlotFlags.All & ~SlotFlags.POCKET))
{
if (!TryReflectProjectile(uid, ent, args.ProjUid))
continue;
if (bestReflector != null && bestReflector.Value.Comp.ReflectProb >= comp.ReflectProb)
continue;
args.Cancelled = true;
break;
}
}
if (projectile != null && (comp.Reflects & projectile.Value.Comp.Reflective) == 0x0)
continue;
private void OnReflectCollide(EntityUid uid, ReflectComponent component, ref ProjectileReflectAttemptEvent args)
{
if (args.Cancelled)
return;
bestReflector = (entityUid, comp);
if (TryReflectProjectile(uid, uid, args.ProjUid, reflect: component))
args.Cancelled = true;
}
private bool TryReflectProjectile(EntityUid user, EntityUid reflector, EntityUid projectile, ProjectileComponent? projectileComp = null, ReflectComponent? reflect = null)
{
if (!Resolve(reflector, ref reflect, false) ||
!reflect.Enabled ||
!TryComp<ReflectiveComponent>(projectile, out var reflective) ||
(reflect.Reflects & reflective.Reflective) == 0x0 ||
!_random.Prob(reflect.ReflectProb) ||
!TryComp<PhysicsComponent>(projectile, out var physics))
{
return false;
}
return bestReflector != null;
}
private bool TryReflectProjectile(EntityUid user, Entity<ReflectComponent> reflector, Entity<ProjectileComponent> projectile)
{
if (
// Is it on?
!reflector.Comp.Enabled ||
// Is the projectile deflectable?
!TryComp<ReflectiveComponent>(projectile, out var reflective) ||
// Does the deflector deflect the type of projecitle?
(reflector.Comp.Reflects & reflective.Reflective) == 0x0 ||
// Is the projectile correctly set up with physics?
!TryComp<PhysicsComponent>(projectile, out var physics) ||
// If the user of the reflector is a mob with stamina, is it capable of deflecting?
TryComp<StaminaComponent>(user, out var staminaComponent) && staminaComponent.Critical ||
_standing.IsDown(reflector)
)
return false;
// If this dice roll fails, the shot isn't deflected
if (!_random.Prob(GetReflectChance(reflector)))
return false;
// Below handles what happens after being deflected.
var rotation = _random.NextAngle(-reflector.Comp.Spread / 2, reflector.Comp.Spread / 2).Opposite();
var rotation = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2).Opposite();
var existingVelocity = _physics.GetMapLinearVelocity(projectile, component: physics);
var relativeVelocity = existingVelocity - _physics.GetMapLinearVelocity(user);
var newVelocity = rotation.RotateVec(relativeVelocity);
@@ -182,52 +119,63 @@ public sealed class ReflectSystem : EntitySystem
if (_netManager.IsServer)
{
_popup.PopupEntity(Loc.GetString("reflect-shot"), user);
_audio.PlayPvs(reflector.Comp.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
_audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
}
_adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)} from {ToPrettyString(projectile.Comp.Weapon)} shot by {projectile.Comp.Shooter}");
if (Resolve(projectile, ref projectileComp, false))
{
_adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)} from {ToPrettyString(projectileComp.Weapon)} shot by {projectileComp.Shooter}");
projectile.Comp.Shooter = user;
projectile.Comp.Weapon = user;
Dirty(projectile);
projectileComp.Shooter = user;
projectileComp.Weapon = user;
Dirty(projectile, projectileComp);
}
else
{
_adminLogger.Add(LogType.BulletHit, LogImpact.Medium, $"{ToPrettyString(user)} reflected {ToPrettyString(projectile)}");
}
return true;
}
private void OnReflectHitscan(EntityUid uid, ReflectComponent component, ref HitScanReflectAttemptEvent args)
{
if (args.Reflected ||
(component.Reflects & args.Reflective) == 0x0)
{
return;
}
if (TryReflectHitscan(uid, uid, args.Shooter, args.SourceItem, args.Direction, out var dir))
{
args.Direction = dir.Value;
args.Reflected = true;
}
}
private bool TryReflectHitscan(
EntityUid user,
Entity<ReflectComponent> reflector,
EntityUid reflector,
EntityUid? shooter,
EntityUid shotSource,
Vector2 direction,
[NotNullWhen(true)] out Vector2? newDirection)
{
if (
// Is the reflector enabled?
!reflector.Comp.Enabled ||
// If the user is a mob with stamina, is it capable of deflecting?
TryComp<StaminaComponent>(user, out var staminaComponent) && staminaComponent.Critical ||
_standing.IsDown(user))
if (!TryComp<ReflectComponent>(reflector, out var reflect) ||
!reflect.Enabled ||
!_random.Prob(reflect.ReflectProb))
{
newDirection = null;
return false;
}
// If this dice roll fails, the shot is not deflected.
if (!_random.Prob(GetReflectChance(reflector)))
{
newDirection = null;
return false;
}
// Below handles what happens after being deflected.
if (_netManager.IsServer)
{
_popup.PopupEntity(Loc.GetString("reflect-shot"), user);
_audio.PlayPvs(reflector.Comp.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
_audio.PlayPvs(reflect.SoundOnReflect, user, AudioHelpers.WithVariation(0.05f, _random));
}
var spread = _random.NextAngle(-reflector.Comp.Spread / 2, reflector.Comp.Spread / 2);
var spread = _random.NextAngle(-reflect.Spread / 2, reflect.Spread / 2);
newDirection = -spread.RotateVec(direction);
if (shooter != null)
@@ -238,106 +186,52 @@ public sealed class ReflectSystem : EntitySystem
return true;
}
private float GetReflectChance(Entity<ReflectComponent> reflector)
{
/*
* The rules of deflection are as follows:
* If you innately reflect things via magic, biology etc., you always have a full chance.
* If you are standing up and standing still, you're prepared to deflect and have full chance.
* If you have velocity, your deflection chance depends on your velocity, clamped.
* If you are floating, your chance is the minimum value possible.
*/
if (reflector.Comp.Innate)
return reflector.Comp.ReflectProb;
if (_gravity.IsWeightless(reflector))
return reflector.Comp.MinReflectProb;
if (!TryComp<PhysicsComponent>(reflector, out var reflectorPhysics))
return reflector.Comp.ReflectProb;
return MathHelper.Lerp(
reflector.Comp.MinReflectProb,
reflector.Comp.ReflectProb,
// Inverse progression between velocities fed in as progression between probabilities. We go high -> low so the output here needs to be _inverted_.
1 - Math.Clamp((reflectorPhysics.LinearVelocity.Length() - reflector.Comp.VelocityBeforeNotMaxProb) / (reflector.Comp.VelocityBeforeMinProb - reflector.Comp.VelocityBeforeNotMaxProb), 0, 1)
);
}
private void OnReflectEquipped(Entity<ReflectComponent> reflector, ref GotEquippedEvent args)
private void OnReflectEquipped(EntityUid uid, ReflectComponent component, GotEquippedEvent args)
{
if (_gameTiming.ApplyingState)
return;
EnsureComp<ReflectUserComponent>(args.Equipee);
if (reflector.Comp.Enabled)
EnableAlert(args.Equipee);
}
private void OnReflectUnequipped(Entity<ReflectComponent> reflector, ref GotUnequippedEvent args)
private void OnReflectUnequipped(EntityUid uid, ReflectComponent comp, GotUnequippedEvent args)
{
RefreshReflectUser(args.Equipee);
}
private void OnReflectHandEquipped(Entity<ReflectComponent> reflector, ref GotEquippedHandEvent args)
private void OnReflectHandEquipped(EntityUid uid, ReflectComponent component, GotEquippedHandEvent args)
{
if (_gameTiming.ApplyingState)
return;
EnsureComp<ReflectUserComponent>(args.User);
if (reflector.Comp.Enabled)
EnableAlert(args.User);
}
private void OnReflectHandUnequipped(Entity<ReflectComponent> reflector, ref GotUnequippedHandEvent args)
private void OnReflectHandUnequipped(EntityUid uid, ReflectComponent component, GotUnequippedHandEvent args)
{
RefreshReflectUser(args.User);
}
private void OnToggleReflect(Entity<ReflectComponent> reflector, ref ItemToggledEvent args)
private void OnToggleReflect(EntityUid uid, ReflectComponent comp, ref ItemToggledEvent args)
{
reflector.Comp.Enabled = args.Activated;
Dirty(reflector);
if (args.User == null)
return;
if (reflector.Comp.Enabled)
EnableAlert(args.User.Value);
else
DisableAlert(args.User.Value);
comp.Enabled = args.Activated;
Dirty(uid, comp);
}
/// <summary>
/// Refreshes whether someone has reflection potential, so we can raise directed events on them.
/// Refreshes whether someone has reflection potential so we can raise directed events on them.
/// </summary>
private void RefreshReflectUser(EntityUid user)
{
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.WITHOUT_POCKET))
foreach (var ent in _inventorySystem.GetHandOrInventoryEntities(user, SlotFlags.All & ~SlotFlags.POCKET))
{
if (!HasComp<ReflectComponent>(ent))
continue;
EnsureComp<ReflectUserComponent>(user);
EnableAlert(user);
return;
}
RemCompDeferred<ReflectUserComponent>(user);
DisableAlert(user);
}
private void EnableAlert(EntityUid alertee)
{
_alerts.ShowAlert(alertee, DeflectingAlert);
}
private void DisableAlert(EntityUid alertee)
{
_alerts.ClearAlert(alertee, DeflectingAlert);
}
}
+3 -3
View File
@@ -34,9 +34,9 @@
source: "https://github.com/ss220-space/Paradise/blob/05043bcfb35c2e7bcf1efbdbfbf976e1a2504bc2/sound/effects/epsilon.ogg"
- files: ["siren.ogg"]
license: "CC-BY-SA-3.0"
copyright: "Taken from Parsdise SS13"
source: "https://github.com/ParadiseSS13/Paradise/blob/master/sound/effects/new_siren.ogg"
license: "CC0-1.0"
copyright: "Created by Bhijin & Myr for Space Station 14"
source: "https://github.com/space-wizards/space-station-14"
- files: ["redalert.ogg"]
license: "CC-BY-SA-3.0"
Binary file not shown.
@@ -6,4 +6,9 @@
- files: ["clearly_nuclear.ogg"]
license: "CC-BY-3.0"
copyright: "Created by mryikes"
source: "https://www.youtube.com/watch?v=chix8uz-oUQ"
source: "https://www.youtube.com/watch?v=chix8uz-oUQ"
- files: ["sound_station_14.ogg"]
license: "CC-BY-3.0"
copyright: "Created by Donchan"
source: "https://www.youtube.com/watch?v=4nUpeYLKUns"
Binary file not shown.
+14
View File
@@ -330,5 +330,19 @@ Entries:
id: 40
time: '2024-06-21T12:06:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29258
- author: Chief-Engineer
changes:
- message: grant_connect_bypass now requires moderator permissions instead of admin.
type: Tweak
id: 41
time: '2024-06-24T21:56:29.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29406
- author: Chief-Engineer
changes:
- message: Added the ability to alert for shared connections.
type: Add
id: 42
time: '2024-06-26T13:43:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29405
Name: Admin
Order: 1
+191 -210
View File
@@ -1,214 +1,4 @@
Entries:
- author: Tayrtahn
changes:
- message: Some devices may have broken wiring at the start of each round.
type: Add
id: 6299
time: '2024-04-04T06:28:09.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26695
- author: lzk228
changes:
- message: Turning off thrusters and gyroscopes now stop consume power.
type: Fix
id: 6300
time: '2024-04-04T06:28:33.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26690
- author: Aexxie
changes:
- message: Added the option to disable your OOC Patron name color.
type: Add
id: 6301
time: '2024-04-04T07:20:06.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26653
- author: DinoWattz
changes:
- message: Fixed pocket slots being able to hide character snout markings.
type: Fix
id: 6302
time: '2024-04-04T08:35:44.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26708
- author: VasilisThePikachu
changes:
- message: Mop buckets and Janitorial Trolleys will not spill their contents as
soon as they are pushed.
type: Fix
- message: Mop buckets are no longer solid. So you can now walk through them.
type: Fix
id: 6303
time: '2024-04-04T08:39:55.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26706
- author: ZeroDayDaemon
changes:
- message: Lowered the number of ducks in the duck crate and reduced the price of
it.
type: Tweak
id: 6304
time: '2024-04-04T19:30:13.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26729
- author: PrPleGoo
changes:
- message: The medical HUD's brightness now scales with lighting again.
type: Tweak
- message: Most HUD icon's brightness now scale with lighting.
type: Tweak
id: 6305
time: '2024-04-04T23:05:01.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26726
- author: Daemon
changes:
- message: Practice projectiles have been given standardized minimal damage.
type: Tweak
- message: The practice laser gun now works with shooting targets.
type: Tweak
id: 6306
time: '2024-04-05T03:15:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26731
- author: Daemon
changes:
- message: Shooting targets can now have their popup type changed with a left click
to show total damage, damage of a single hit, both, or just a notice that it
was hit.
type: Tweak
id: 6307
time: '2024-04-05T07:19:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26734
- author: Vermidia
changes:
- message: Baseball bats now require a knife to craft
type: Tweak
id: 6308
time: '2024-04-05T15:41:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26742
- author: deltanedas
changes:
- message: Making fultons now requires cloth and they are faster to make.
type: Tweak
id: 6309
time: '2024-04-05T15:42:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26747
- author: superjj18
changes:
- message: Broadcasting works again!
type: Fix
id: 6310
time: '2024-04-05T17:29:22.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26750
- author: Golinth
changes:
- message: Fixed mindshield icons being glow in the dark
type: Fix
id: 6311
time: '2024-04-05T20:35:32.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26754
- author: Blackern5000
changes:
- message: Added sap
type: Add
- message: Added syrup, it can be made by boiling sap.
type: Add
- message: Dionae now bleed sap instead of water
type: Tweak
id: 6312
time: '2024-04-05T21:06:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25748
- author: osjarw
changes:
- message: Thin firelocks are now constructable/deconstructable
type: Fix
id: 6313
time: '2024-04-06T01:55:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26745
- author: BITTERLYNX
changes:
- message: Rodents are more visually pleasing to burn
type: Fix
- message: mothroach and hammy also more visually pleasing to burn
type: Fix
id: 6314
time: '2024-04-06T04:41:23.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26758
- author: Plykiya
changes:
- message: You can now carefully walk over glass shards and D4.
type: Tweak
id: 6315
time: '2024-04-06T04:49:14.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26763
- author: PursuitInAshes
changes:
- message: Sake Bottles can now be found in the booze-o-mat.
type: Tweak
id: 6316
time: '2024-04-06T20:16:47.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26776
- author: osjarw
changes:
- message: Removed broken anom behaviour, which causes APE shots to fly through
the anom.
type: Remove
id: 6317
time: '2024-04-06T22:27:16.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26775
- author: Vermidia
changes:
- message: Water coolers show how full/what they're full of again.
type: Fix
id: 6318
time: '2024-04-06T23:58:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26784
- author: Crotalus
changes:
- message: Show missing materials in lathe tooltip
type: Add
id: 6319
time: '2024-04-08T03:16:11.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26795
- author: chromiumboy
changes:
- message: Fixed the RCD not being able to build on top of puddles
type: Fix
- message: RCD constructions can no longer be rotated while in progress
type: Tweak
- message: The RCD now reports construction mode changes to its user
type: Add
id: 6320
time: '2024-04-08T03:17:29.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26792
- author: lzk228
changes:
- message: Bombsuit and jani bombsuit are similar now.
type: Tweak
id: 6321
time: '2024-04-08T15:05:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26806
- author: lzk228
changes:
- message: Clothing restock crate was splitted to clothing and autodrobe restock
crates.
type: Tweak
- message: Clothing is cheaper.
type: Tweak
id: 6322
time: '2024-04-08T15:10:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26805
- author: Hanzdegloker
changes:
- message: Spears can now be quipped to your suit slot and cost slightly more to
make.
type: Tweak
id: 6323
time: '2024-04-08T15:34:35.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/26724
- author: KittenColony
changes:
- message: Added 13 new gauze wraps for moth that fit their twig bodies
type: Add
- message: Gauze markings have been moved to the Overlay category, allowing gauze
to not take up marking points
type: Tweak
id: 6324
time: '2024-04-09T08:04:51.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/25481
- author: Killerqu00
changes:
- message: Quartermasters can now skip a single bounty in the list once every 15
@@ -3850,3 +3640,194 @@
id: 6798
time: '2024-06-21T10:50:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29293
- author: Macoron
changes:
- message: Fixed SSD indicator for dwarfs.
type: Fix
id: 6799
time: '2024-06-22T01:11:40.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29310
- author: EmoGarbage404
changes:
- message: Deconstructing a damaged AME core now yields steel instead of a full
AME flatpack.
type: Tweak
id: 6800
time: '2024-06-22T04:15:21.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29317
- author: Errant
changes:
- message: Items should no longer be missing from backpacks at spawn.
type: Fix
id: 6801
time: '2024-06-22T10:43:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29336
- author: Donchan, marboww
changes:
- message: New Nuke Detonation Song "Sound Station 14"
type: Add
id: 6802
time: '2024-06-22T15:09:41.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29345
- author: deltanedas
changes:
- message: Doors access logging can now be disabled by snipping or pulsing a LOG
wire.
type: Tweak
id: 6803
time: '2024-06-22T15:12:58.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29094
- author: notafet
changes:
- message: Reduce air alarm sensitivity to plasma, tritium, and carbon dioxide.
type: Tweak
id: 6804
time: '2024-06-22T15:22:07.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29331
- author: Damn Feds
changes:
- message: thief toolbox kits now better describe contents
type: Tweak
id: 6805
time: '2024-06-22T15:44:18.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27771
- author: lzk228
changes:
- message: Prying reinforced tile now will give you back metal rod
type: Tweak
id: 6806
time: '2024-06-22T15:47:02.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29084
- author: nikthechampiongr
changes:
- message: Locked firelocks can no longer be pried by hand when powered.
type: Tweak
id: 6807
time: '2024-06-22T17:49:50.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29221
- author: Floofi
changes:
- message: Added lemon juice to the Boozemat!
type: Add
id: 6808
time: '2024-06-22T23:46:19.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/27465
- author: Cojoke-dot
changes:
- message: You can now put hats on Medibots
type: Add
id: 6809
time: '2024-06-23T00:28:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/28584
- author: WarMechanic
changes:
- message: The TEG now powers itself once started.
type: Tweak
id: 6810
time: '2024-06-23T01:46:31.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29072
- author: eoineoineoin
changes:
- message: Hand labelers can now apply labels to buttons, switches, and levers
type: Tweak
id: 6811
time: '2024-06-23T14:52:30.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29378
- author: Tayrtahn
changes:
- message: Jugs in the ChemVend are labeled again.
type: Fix
id: 6812
time: '2024-06-23T17:31:34.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29178
- author: DrEnzyme
changes:
- message: Add bagels and poppy seed bagels.
type: Add
id: 6813
time: '2024-06-23T19:33:12.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/24799
- author: PJB3005
changes:
- message: Ghosts can now examine details on objects from any range.
type: Tweak
id: 6814
time: '2024-06-24T15:36:53.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29404
- author: Emisse
changes:
- message: atmos, elite syndie, and deathsquad hardsuits are now the only fireproof
suits
type: Tweak
id: 6815
time: '2024-06-24T22:03:05.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29416
- author: Bhijn and Myr
changes:
- message: The colors of the LEDs on the heater and freezer thermomachines has been
changed from red/green (respectively) to orange/cyan (respectively). In laymen's
terms, this makes thermomachines far more reasonably differentiable for those
with colorblindness.
type: Tweak
id: 6816
time: '2024-06-24T22:40:20.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29397
- author: Elysium206
changes:
- message: Increased Security Riot shields durability significantly and makes actively
blocking better
type: Tweak
id: 6817
time: '2024-06-25T03:56:46.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29239
- author: Tayrtahn
changes:
- message: Fixed space ninja's starting with their internals disabled.
type: Fix
id: 6818
time: '2024-06-25T06:28:48.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29213
- author: PJB3005
changes:
- message: Low pressure damage now does only 1/4th as much damage. This is a band-aid
until better mechanics exists for the crew to deal with breaches of various
kinds.
type: Remove
id: 6819
time: '2024-06-26T00:48:26.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29478
- author: lzk228
changes:
- message: Blood now has less satiation.
type: Tweak
id: 6820
time: '2024-06-26T02:27:11.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29433
- author: Veritable-Calamity
changes:
- message: Moldy food can now be collected in the Janitors' trash bags.
type: Fix
id: 6821
time: '2024-06-26T03:26:59.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29380
- author: Plykiya
changes:
- message: You can now interact with magic mirrors.
type: Fix
id: 6822
time: '2024-06-26T14:25:11.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29491
- author: Doomsdrayk
changes:
- message: Disposal units no longer protect their contents from nuclear explosions.
type: Fix
id: 6823
time: '2024-06-26T14:25:43.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29438
- author: Tayrtahn
changes:
- message: Pills and pill canisters no longer have two labels.
type: Fix
id: 6824
time: '2024-06-27T02:08:57.0000000+00:00'
url: https://github.com/space-wizards/space-station-14/pull/29499
@@ -36,6 +36,7 @@ limit = 10.0
see_own_notes = true
deadmin_on_join = true
new_player_threshold = 600
alert.min_players_sharing_connection = 2
[worldgen]
enabled = true
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
admin-alert-shared-connection = {$player} is sharing a connection with {$otherCount} connected player(s): {$otherList}
@@ -1,3 +1,4 @@
admin-player-actions-reason = Reason
admin-player-actions-bans = Ban List
admin-player-actions-notes = Notes
admin-player-actions-kick = Kick
@@ -0,0 +1 @@
player-list-filter = Filter
@@ -0,0 +1 @@
admin-ui-teleport = Teleport
@@ -0,0 +1,9 @@
admin-ui-blueprint-map = Map
admin-ui-blueprint-path = Path
admin-ui-blueprint-x = X
admin-ui-blueprint-y = Y
admin-ui-blueprint-rotation = Rotation
admin-ui-blueprint-teleport = Teleport to
admin-ui-blueprint-reset = Reset to default
admin-ui-blueprint-load = Load Blueprint
@@ -0,0 +1,12 @@
admin-ui-atmos-add = Add Atmos
admin-ui-atmos-add-gas = Add Gas
admin-ui-atmos-fill-gas = Fill Gas
admin-ui-atmos-set-temperature = Set Temperature
admin-ui-atmos-grid = Grid
admin-ui-atmos-grid-current = Current
admin-ui-atmos-tile-x = TileX
admin-ui-atmos-tile-y = TileY
admin-ui-atmos-gas = Gas
admin-ui-atmos-gas-amount = Amount
admin-ui-atmos-temperature = Temperature
@@ -1,2 +1,9 @@
object-tab-entity-id = Entity ID
object-tab-object-name = Object name
object-tab-object-type = Object type:
object-tab-object-search = Search...
object-tab-object-type-grids = Grids
object-tab-object-type-maps = Maps
object-tab-object-type-stations = Stations
@@ -1,4 +1,5 @@
player-tab-username = Username
player-tab-player-count = Players: { $count }
player-tab-username = Username
player-tab-character = Character
player-tab-job = Job
player-tab-antagonist = Antagonist
@@ -1,2 +1,5 @@
administration-ui-round-tab-start-round = Start Round
administration-ui-round-tab-end-round = End Round
administration-ui-round-tab-restart-round = Restart Round
administration-ui-round-tab-restart-round-now = Restart NOW
-3
View File
@@ -107,6 +107,3 @@ alerts-revenant-essence-desc = The power of souls. It sustains you and is used f
alerts-revenant-corporeal-name = Corporeal
alerts-revenant-corporeal-desc = You have manifested physically. People around you can see and hurt you.
alerts-deflecting-name = Deflecting
alerts-deflecting-desc = You have a chance to deflect incoming projectiles. Standing still or moving slowly will increase this chance.

Some files were not shown because too many files have changed in this diff Show More