Merge pull request #289 from crystallpunk-14/ed-30-06-2024-upstream

Ed 30 06 2024 upstream sync
This commit is contained in:
Ed
2024-06-30 12:40:08 +03:00
committed by GitHub
420 changed files with 19763 additions and 14225 deletions

7
.github/CODEOWNERS vendored
View File

@@ -55,3 +55,10 @@
#Jezi
/Content.*/Medical @Jezithyr
/Content.*/Body @Jezithyr
# Sloth
/Content.*/Audio @metalgearsloth
/Content.*/Movement @metalgearsloth
/Content.*/NPC @metalgearsloth
/Content.*/Shuttles @metalgearsloth
/Content.*/Weapons @metalgearsloth

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"

View File

@@ -3,38 +3,57 @@ using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI
namespace Content.Client.Administration.UI;
[GenerateTypedNameReferences]
public sealed partial class AdminMenuWindow : DefaultWindow
{
[GenerateTypedNameReferences]
public sealed partial class AdminMenuWindow : DefaultWindow
public event Action? OnDisposed;
public AdminMenuWindow()
{
public event Action? OnDisposed;
MinSize = new Vector2(650, 250);
Title = Loc.GetString("admin-menu-title");
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle((int) TabIndex.Admin, Loc.GetString("admin-menu-admin-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Adminbus, Loc.GetString("admin-menu-adminbus-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Atmos, Loc.GetString("admin-menu-atmos-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Round, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Server, Loc.GetString("admin-menu-server-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.PanicBunker, Loc.GetString("admin-menu-panic-bunker-tab"));
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
MasterTabContainer.SetTabTitle((int) TabIndex.BabyJail, Loc.GetString("admin-menu-baby-jail-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Players, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle((int) TabIndex.Objects, Loc.GetString("admin-menu-objects-tab"));
MasterTabContainer.OnTabChanged += OnTabChanged;
}
public AdminMenuWindow()
{
MinSize = new Vector2(650, 250);
Title = Loc.GetString("admin-menu-title");
RobustXamlLoader.Load(this);
MasterTabContainer.SetTabTitle(0, Loc.GetString("admin-menu-admin-tab"));
MasterTabContainer.SetTabTitle(1, Loc.GetString("admin-menu-adminbus-tab"));
MasterTabContainer.SetTabTitle(2, Loc.GetString("admin-menu-atmos-tab"));
MasterTabContainer.SetTabTitle(3, Loc.GetString("admin-menu-round-tab"));
MasterTabContainer.SetTabTitle(4, Loc.GetString("admin-menu-server-tab"));
MasterTabContainer.SetTabTitle(5, Loc.GetString("admin-menu-panic-bunker-tab"));
/*
* TODO: Remove baby jail code once a more mature gateway process is established. This code is only being issued as a stopgap to help with potential tiding in the immediate future.
*/
MasterTabContainer.SetTabTitle(6, Loc.GetString("admin-menu-baby-jail-tab"));
MasterTabContainer.SetTabTitle(7, Loc.GetString("admin-menu-players-tab"));
MasterTabContainer.SetTabTitle(8, Loc.GetString("admin-menu-objects-tab"));
}
private void OnTabChanged(int tabIndex)
{
var tabEnum = (TabIndex)tabIndex;
if (tabEnum == TabIndex.Objects)
ObjectsTabControl.RefreshObjectList();
}
protected override void Dispose(bool disposing)
{
OnDisposed?.Invoke();
base.Dispose(disposing);
OnDisposed = null;
}
protected override void Dispose(bool disposing)
{
OnDisposed?.Invoke();
base.Dispose(disposing);
OnDisposed = null;
}
private enum TabIndex
{
Admin = 0,
Adminbus,
Atmos,
Round,
Server,
PanicBunker,
BabyJail,
Players,
Objects,
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,18 +4,17 @@
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls">
<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"/>
<OptionButton Name="ObjectTypeOptions" HorizontalExpand="True" SizeFlagsStretchRatio="0.25"/>
<Label Text="{Loc object-tab-object-type}" />
<OptionButton Name="ObjectTypeOptions" HorizontalAlignment="Left" />
<LineEdit Name="SearchLineEdit" PlaceHolder="{Loc object-tab-object-search}" HorizontalExpand="True"
SizeFlagsStretchRatio="1" />
<Button Name="RefreshListButton" Text="{Loc object-tab-refresh-button}" ToggleMode="False" />
</BoxContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
</BoxContainer>
<cc:HSeparator/>
<cc:HSeparator />
<BoxContainer Orientation="Vertical" HorizontalExpand="True" VerticalExpand="True">
<ot:ObjectsTabHeader Name="ListHeader"/>
<cc:HSeparator/>
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True"/>
<ot:ObjectsTabHeader Name="ListHeader" />
<cc:HSeparator />
<co:SearchListContainer Name="SearchList" Access="Public" VerticalExpand="True" />
</BoxContainer>
</BoxContainer>
</Control>

View File

@@ -3,6 +3,7 @@ using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
@@ -15,17 +16,14 @@ public sealed partial class ObjectsTab : Control
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly List<ObjectsTabEntry> _objects = new();
private readonly List<ObjectsTabSelection> _selections = new();
private bool _ascending = false; // Set to false for descending order by default
private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName;
private readonly Color _altColor = Color.FromHex("#292B38");
private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
private bool _ascending;
private ObjectsTabHeader.Header _headerClicked = ObjectsTabHeader.Header.ObjectName;
private readonly TimeSpan _updateFrequency = TimeSpan.FromSeconds(2);
private TimeSpan _nextUpdate;
private readonly List<ObjectsTabSelection> _selections = [];
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public ObjectsTab()
{
@@ -38,40 +36,25 @@ public sealed partial class ObjectsTab : Control
RefreshObjectList(_selections[ev.Id]);
};
foreach (var type in Enum.GetValues(typeof(ObjectsTabSelection)))
foreach (var type in Enum.GetValues<ObjectsTabSelection>())
{
_selections.Add((ObjectsTabSelection)type!);
ObjectTypeOptions.AddItem(Enum.GetName((ObjectsTabSelection)type)!);
_selections.Add(type);
ObjectTypeOptions.AddItem(GetLocalizedEnumValue(type));
}
ListHeader.OnHeaderClicked += HeaderClicked;
SearchList.SearchBar = SearchLineEdit;
SearchList.GenerateItem += GenerateButton;
SearchList.DataFilterCondition += DataFilterCondition;
SearchList.ItemKeyBindDown += (args, data) => OnEntryKeyBindDown?.Invoke(args, data);
RefreshListButton.OnPressed += _ => RefreshObjectList();
RefreshObjectList();
// Set initial selection and refresh the list to apply the initial sort order
var defaultSelection = ObjectsTabSelection.Grids;
ObjectTypeOptions.SelectId((int)defaultSelection); // Set the default selection
RefreshObjectList(defaultSelection); // Refresh the list with the default selection
// Initialize the next update time
_nextUpdate = TimeSpan.Zero;
ObjectTypeOptions.SelectId((int) defaultSelection);
RefreshObjectList(defaultSelection);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_timing.CurTime < _nextUpdate)
return;
_nextUpdate = _timing.CurTime + _updateFrequency;
RefreshObjectList();
}
private void RefreshObjectList()
public void RefreshObjectList()
{
RefreshObjectList(_selections[ObjectTypeOptions.SelectedId]);
}
@@ -101,6 +84,7 @@ public sealed partial class ObjectsTab : Control
{
entities.Add((metadata.EntityName, _entityManager.GetNetEntity(uid)));
}
break;
}
default:
@@ -111,14 +95,18 @@ public sealed partial class ObjectsTab : Control
{
var valueA = GetComparableValue(a, _headerClicked);
var valueB = GetComparableValue(b, _headerClicked);
return _ascending ? Comparer<object>.Default.Compare(valueA, valueB) : Comparer<object>.Default.Compare(valueB, valueA);
return _ascending
? Comparer<object>.Default.Compare(valueA, valueB)
: Comparer<object>.Default.Compare(valueB, valueA);
});
var listData = new List<ObjectsListData>();
for (int index = 0; index < entities.Count; index++)
for (var index = 0; index < entities.Count; index++)
{
var info = entities[index];
listData.Add(new ObjectsListData(info, $"{info.Name} {info.Entity}", index % 2 == 0 ? _altColor : _defaultColor));
listData.Add(new ObjectsListData(info,
$"{info.Name} {info.Entity}",
index % 2 == 0 ? _altColor : _defaultColor));
}
SearchList.PopulateList(listData);
@@ -129,10 +117,11 @@ public sealed partial class ObjectsTab : Control
if (data is not ObjectsListData { Info: var info, BackgroundColor: var backgroundColor })
return;
var entry = new ObjectsTabEntry(info.Name, info.Entity, new StyleBoxFlat { BackgroundColor = backgroundColor });
var entry = new ObjectsTabEntry(info.Name,
info.Entity,
new StyleBoxFlat { BackgroundColor = backgroundColor });
button.ToolTip = $"{info.Name}, {info.Entity}";
button.OnKeyBindDown += args => OnEntryKeyBindDown?.Invoke(args, data);
button.AddChild(entry);
}
@@ -154,7 +143,7 @@ public sealed partial class ObjectsTab : Control
{
ObjectsTabHeader.Header.ObjectName => entity.Name,
ObjectsTabHeader.Header.EntityID => entity.Entity.ToString(),
_ => entity.Name
_ => entity.Name,
};
}
@@ -174,6 +163,17 @@ public sealed partial class ObjectsTab : Control
RefreshObjectList();
}
private string GetLocalizedEnumValue(ObjectsTabSelection selection)
{
return selection switch
{
ObjectsTabSelection.Grids => Loc.GetString("object-tab-object-type-grids"),
ObjectsTabSelection.Maps => Loc.GetString("object-tab-object-type-maps"),
ObjectsTabSelection.Stations => Loc.GetString("object-tab-object-type-stations"),
_ => throw new ArgumentOutOfRangeException(nameof(selection), selection, null),
};
}
private enum ObjectsTabSelection
{
Grids,
@@ -182,4 +182,5 @@ public sealed partial class ObjectsTab : Control
}
}
public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor) : ListData;
public record ObjectsListData((string Name, NetEntity Entity) Info, string FilteringString, Color BackgroundColor)
: ListData;

View File

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

View File

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

View File

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

View File

@@ -65,14 +65,13 @@ public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
{
if (_isClientTyping == isClientTyping)
return;
_isClientTyping = isClientTyping;
// check if player controls any pawn
// check if player controls any entity.
if (_playerManager.LocalEntity == null)
return;
// send a networked event to server
RaiseNetworkEvent(new TypingChangedEvent(isClientTyping));
_isClientTyping = isClientTyping;
RaisePredictiveEvent(new TypingChangedEvent(isClientTyping));
}
private void OnShowTypingChanged(bool showTyping)

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Client.Clothing.Systems;
using Content.Client.Stylesheets;
@@ -74,13 +74,9 @@ public sealed partial class ChameleonMenu : DefaultWindow
};
button.OnPressed += _ => OnIdSelected?.Invoke(id);
Grid.AddChild(button);
var texture = _sprite.GetPrototypeIcon(proto);
button.AddChild(new TextureRect
{
Stretch = TextureRect.StretchMode.KeepAspectCentered,
Texture = texture.Default
});
var entityPrototypeView = new EntityPrototypeView();
button.AddChild(entityPrototypeView);
entityPrototypeView.SetPrototype(proto);
}
}

View File

@@ -1,29 +1,33 @@
<DefaultWindow xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.4" Margin="0 0 5 0">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="0 0 0 5">
<LineEdit Name="SearchBar" PlaceHolder="Search" HorizontalExpand="True"/>
<OptionButton Name="Category" Access="Public" MinSize="130 0"/>
<OptionButton Name="OptionCategories" Access="Public" MinSize="130 0"/>
</BoxContainer>
<ItemList Name="Recipes" Access="Public" SelectMode="Single" VerticalExpand="True"/>
</BoxContainer>
<Control MinSize="10 0"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" SizeFlagsStretchRatio="0.6">
<Button Name="FavoriteButton" Visible="false" HorizontalExpand="False"
HorizontalAlignment="Right" Margin="0 0 0 15"/>
<Control>
<BoxContainer Orientation="Horizontal" Align="Center">
<TextureRect Name="TargetTexture" HorizontalAlignment="Right" Stretch="Keep"/>
<Control MinSize="10 0"/>
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="TargetName"/>
<RichTextLabel Name="TargetDesc"/>
<BoxContainer Orientation="Vertical" HorizontalExpand="True" Margin="0 0 0 5">
<BoxContainer Orientation="Horizontal" Align="Center">
<TextureRect Name="TargetTexture" HorizontalAlignment="Right" Stretch="Keep" Margin="0 0 10 0"/>
<BoxContainer Orientation="Vertical">
<RichTextLabel Name="TargetName"/>
<RichTextLabel Name="TargetDesc"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</Control>
<ItemList Name="RecipeStepList" Access="Public" VerticalExpand="True"/>
<ItemList Name="RecipeStepList" Access="Public" VerticalExpand="True" Margin="0 0 0 5"/>
<BoxContainer Orientation="Vertical">
<Button Name="BuildButton" Disabled="True" ToggleMode="True" VerticalExpand="True" SizeFlagsStretchRatio="0.5"/>
<Button Name="BuildButton" Disabled="True" ToggleMode="True"
VerticalExpand="True" SizeFlagsStretchRatio="0.5"/>
<BoxContainer Orientation="Horizontal" VerticalExpand="True" SizeFlagsStretchRatio="0.5">
<Button Name="EraseButton" ToggleMode="True" HorizontalExpand="True" SizeFlagsStretchRatio="0.7"/>
<Button Name="EraseButton" ToggleMode="True"
HorizontalExpand="True" SizeFlagsStretchRatio="0.7"/>
<Button Name="ClearButton" HorizontalExpand="True" SizeFlagsStretchRatio="0.3"/>
</BoxContainer>
</BoxContainer>

View File

@@ -22,7 +22,7 @@ namespace Content.Client.Construction.UI
// It isn't optimal to expose UI controls like this, but the UI control design is
// questionable so it can't be helped.
string[] Categories { get; set; }
OptionButton Category { get; }
OptionButton OptionCategories { get; }
bool EraseButtonPressed { get; set; }
bool BuildButtonPressed { get; set; }
@@ -32,12 +32,13 @@ namespace Content.Client.Construction.UI
event EventHandler<(string search, string catagory)> PopulateRecipes;
event EventHandler<ItemList.Item?> RecipeSelected;
event EventHandler RecipeFavorited;
event EventHandler<bool> BuildButtonToggled;
event EventHandler<bool> EraseButtonToggled;
event EventHandler ClearAllGhosts;
void ClearRecipeInfo();
void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem);
void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem, bool isFavorite);
void ResetPlacement();
#region Window Control
@@ -84,10 +85,12 @@ namespace Content.Client.Construction.UI
Recipes.OnItemSelected += obj => RecipeSelected?.Invoke(this, obj.ItemList[obj.ItemIndex]);
Recipes.OnItemDeselected += _ => RecipeSelected?.Invoke(this, null);
SearchBar.OnTextChanged += _ => PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[Category.SelectedId]));
Category.OnItemSelected += obj =>
SearchBar.OnTextChanged += _ =>
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[OptionCategories.SelectedId]));
OptionCategories.OnItemSelected += obj =>
{
Category.SelectId(obj.Id);
OptionCategories.SelectId(obj.Id);
SearchBar.SetText(string.Empty);
PopulateRecipes?.Invoke(this, (SearchBar.Text, Categories[obj.Id]));
};
@@ -97,12 +100,14 @@ namespace Content.Client.Construction.UI
ClearButton.OnPressed += _ => ClearAllGhosts?.Invoke(this, EventArgs.Empty);
EraseButton.Text = Loc.GetString("construction-menu-eraser-mode");
EraseButton.OnToggled += args => EraseButtonToggled?.Invoke(this, args.Pressed);
FavoriteButton.OnPressed += args => RecipeFavorited?.Invoke(this, EventArgs.Empty);
}
public event EventHandler? ClearAllGhosts;
public event EventHandler<(string search, string catagory)>? PopulateRecipes;
public event EventHandler<ItemList.Item?>? RecipeSelected;
public event EventHandler? RecipeFavorited;
public event EventHandler<bool>? BuildButtonToggled;
public event EventHandler<bool>? EraseButtonToggled;
@@ -112,13 +117,17 @@ namespace Content.Client.Construction.UI
EraseButton.Pressed = false;
}
public void SetRecipeInfo(string name, string description, Texture iconTexture, bool isItem)
public void SetRecipeInfo(
string name, string description, Texture iconTexture, bool isItem, bool isFavorite)
{
BuildButton.Disabled = false;
BuildButton.Text = Loc.GetString(isItem ? "construction-menu-place-ghost" : "construction-menu-craft");
TargetName.SetMessage(name);
TargetDesc.SetMessage(description);
TargetTexture.Texture = iconTexture;
FavoriteButton.Visible = true;
FavoriteButton.Text = Loc.GetString(
isFavorite ? "construction-add-favorite-button" : "construction-remove-from-favorite-button");
}
public void ClearRecipeInfo()
@@ -127,6 +136,7 @@ namespace Content.Client.Construction.UI
TargetName.SetMessage(string.Empty);
TargetDesc.SetMessage(string.Empty);
TargetTexture.Texture = null;
FavoriteButton.Visible = false;
RecipeStepList.Clear();
}
}

View File

@@ -36,7 +36,10 @@ namespace Content.Client.Construction.UI
private ConstructionSystem? _constructionSystem;
private ConstructionPrototype? _selected;
private List<ConstructionPrototype> _favoritedRecipes = [];
private string _selectedCategory = string.Empty;
private string _favoriteCatName = "construction-category-favorites";
private string _forAllCategoryName = "construction-category-all";
private bool CraftingAvailable
{
get => _uiManager.GetActiveUIWidget<GameTopMenuBar>().CraftingButton.Visible;
@@ -65,7 +68,7 @@ namespace Content.Client.Construction.UI
else
_constructionView.OpenCentered();
if(_selected != null)
if (_selected != null)
PopulateInfo(_selected);
}
else
@@ -105,9 +108,10 @@ namespace Content.Client.Construction.UI
_constructionView.EraseButtonPressed = b;
};
_constructionView.RecipeFavorited += (_, _) => OnViewFavoriteRecipe();
PopulateCategories();
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
}
public void OnHudCraftingButtonToggled(ButtonToggledEventArgs args)
@@ -154,6 +158,13 @@ namespace Content.Client.Construction.UI
recipesList.Clear();
var recipes = new List<ConstructionPrototype>();
var isEmptyCategory = string.IsNullOrEmpty(category) || category == _forAllCategoryName;
if (isEmptyCategory)
_selectedCategory = string.Empty;
else
_selectedCategory = category;
foreach (var recipe in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
if (recipe.Hide)
@@ -173,10 +184,19 @@ namespace Content.Client.Construction.UI
continue;
}
if (!string.IsNullOrEmpty(category) && category != "construction-category-all")
if (!isEmptyCategory)
{
if (recipe.Category != category)
if (category == _favoriteCatName)
{
if (!_favoritedRecipes.Contains(recipe))
{
continue;
}
}
else if (recipe.Category != category)
{
continue;
}
}
recipes.Add(recipe);
@@ -192,13 +212,10 @@ namespace Content.Client.Construction.UI
// There is apparently no way to set which
}
private void PopulateCategories()
private void PopulateCategories(string? selectCategory = null)
{
var uniqueCategories = new HashSet<string>();
// hard-coded to show all recipes
uniqueCategories.Add("construction-category-all");
foreach (var prototype in _prototypeManager.EnumeratePrototypes<ConstructionPrototype>())
{
var category = prototype.Category;
@@ -207,25 +224,49 @@ namespace Content.Client.Construction.UI
uniqueCategories.Add(category);
}
_constructionView.Category.Clear();
var isFavorites = _favoritedRecipes.Count > 0;
var categoriesArray = new string[isFavorites ? uniqueCategories.Count + 2 : uniqueCategories.Count + 1];
var array = uniqueCategories.OrderBy(Loc.GetString).ToArray();
Array.Sort(array);
// hard-coded to show all recipes
var idx = 0;
categoriesArray[idx++] = _forAllCategoryName;
for (var i = 0; i < array.Length; i++)
// hard-coded to show favorites if it need
if (isFavorites)
{
var category = array[i];
_constructionView.Category.AddItem(Loc.GetString(category), i);
categoriesArray[idx++] = _favoriteCatName;
}
_constructionView.Categories = array;
var sortedProtoCategories = uniqueCategories.OrderBy(Loc.GetString);
foreach (var cat in sortedProtoCategories)
{
categoriesArray[idx++] = cat;
}
_constructionView.OptionCategories.Clear();
for (var i = 0; i < categoriesArray.Length; i++)
{
_constructionView.OptionCategories.AddItem(Loc.GetString(categoriesArray[i]), i);
if (!string.IsNullOrEmpty(selectCategory) && selectCategory == categoriesArray[i])
_constructionView.OptionCategories.SelectId(i);
}
_constructionView.Categories = categoriesArray;
}
private void PopulateInfo(ConstructionPrototype prototype)
{
var spriteSys = _systemManager.GetEntitySystem<SpriteSystem>();
_constructionView.ClearRecipeInfo();
_constructionView.SetRecipeInfo(prototype.Name, prototype.Description, spriteSys.Frame0(prototype.Icon), prototype.Type != ConstructionType.Item);
_constructionView.SetRecipeInfo(
prototype.Name, prototype.Description, spriteSys.Frame0(prototype.Icon),
prototype.Type != ConstructionType.Item,
!_favoritedRecipes.Contains(prototype));
var stepList = _constructionView.RecipeStepList;
GenerateStepList(prototype, stepList);
@@ -243,7 +284,7 @@ namespace Content.Client.Construction.UI
var text = entry.Arguments != null
? Loc.GetString(entry.Localization, entry.Arguments) : Loc.GetString(entry.Localization);
if (entry.EntryNumber is {} number)
if (entry.EntryNumber is { } number)
{
text = Loc.GetString("construction-presenter-step-wrapper",
("step-number", number), ("text", text));
@@ -335,6 +376,26 @@ namespace Content.Client.Construction.UI
if (args.System is ConstructionSystem) SystemBindingChanged(null);
}
private void OnViewFavoriteRecipe()
{
if (_selected is not ConstructionPrototype recipe)
return;
if (!_favoritedRecipes.Remove(_selected))
_favoritedRecipes.Add(_selected);
if (_selectedCategory == _favoriteCatName)
{
if (_favoritedRecipes.Count > 0)
OnViewPopulateRecipes(_constructionView, (string.Empty, _favoriteCatName));
else
OnViewPopulateRecipes(_constructionView, (string.Empty, string.Empty));
}
PopulateInfo(_selected);
PopulateCategories(_selectedCategory);
}
private void SystemBindingChanged(ConstructionSystem? newSystem)
{
if (newSystem is null)

View File

@@ -1,23 +1,28 @@
using Content.Client.Administration.Managers;
using Content.Client.Administration.Managers;
using Content.Shared.CCVar;
using Robust.Client;
using Robust.Client.UserInterface;
using Robust.Shared.Configuration;
namespace Content.Client.DebugMon;
/// <summary>
/// This handles preventing certain debug monitors from appearing.
/// This handles preventing certain debug monitors from being usable by non-admins.
/// </summary>
public sealed class DebugMonitorSystem : EntitySystem
internal sealed class DebugMonitorManager
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IUserInterfaceManager _userInterface = default!;
[Dependency] private readonly IBaseClient _baseClient = default!;
public override void FrameUpdate(float frameTime)
public void FrameUpdate()
{
if (!_admin.IsActive() && _cfg.GetCVar(CCVars.DebugCoordinatesAdminOnly))
if (_baseClient.RunLevel == ClientRunLevel.InGame
&& !_admin.IsActive()
&& _cfg.GetCVar(CCVars.DebugCoordinatesAdminOnly))
{
_userInterface.DebugMonitors.SetMonitor(DebugMonitor.Coords, false);
}
}
}

View File

@@ -8,6 +8,7 @@ using Robust.Client.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.Containers;
namespace Content.Client.DoAfter;
@@ -19,6 +20,7 @@ public sealed class DoAfterOverlay : Overlay
private readonly SharedTransformSystem _transform;
private readonly MetaDataSystem _meta;
private readonly ProgressColorSystem _progressColor;
private readonly SharedContainerSystem _container;
private readonly Texture _barTexture;
private readonly ShaderInstance _unshadedShader;
@@ -41,6 +43,7 @@ public sealed class DoAfterOverlay : Overlay
_player = player;
_transform = _entManager.EntitySysManager.GetEntitySystem<SharedTransformSystem>();
_meta = _entManager.EntitySysManager.GetEntitySystem<MetaDataSystem>();
_container = _entManager.EntitySysManager.GetEntitySystem<SharedContainerSystem>();
_progressColor = _entManager.System<ProgressColorSystem>();
var sprite = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/progress_bar.rsi"), "icon");
_barTexture = _entManager.EntitySysManager.GetEntitySystem<SpriteSystem>().Frame0(sprite);
@@ -98,11 +101,13 @@ public sealed class DoAfterOverlay : Overlay
var offset = 0f;
var isInContainer = _container.IsEntityOrParentInContainer(uid, meta, xform);
foreach (var doAfter in comp.DoAfters.Values)
{
// Hide some DoAfters from other players for stealthy actions (ie: thieving gloves)
var alpha = 1f;
if (doAfter.Args.Hidden)
if (doAfter.Args.Hidden || isInContainer)
{
if (uid != localEnt)
continue;

View File

@@ -1,6 +1,7 @@
using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.DebugMon;
using Content.Client.Eui;
using Content.Client.Fullscreen;
using Content.Client.GhostKick;
@@ -34,6 +35,7 @@ using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.Prototypes;
using Robust.Shared.Replays;
using Robust.Shared.Timing;
namespace Content.Client.Entry
{
@@ -69,6 +71,7 @@ namespace Content.Client.Entry
[Dependency] private readonly IReplayLoadManager _replayLoad = default!;
[Dependency] private readonly ILogManager _logManager = default!;
[Dependency] private readonly ContentReplayPlaybackManager _replayMan = default!;
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
public override void Init()
{
@@ -206,5 +209,13 @@ namespace Content.Client.Entry
_stateManager.RequestStateChange<MainScreen>();
}
}
public override void Update(ModUpdateLevel level, FrameEventArgs frameEventArgs)
{
if (level == ModUpdateLevel.FramePreEngine)
{
_debugMonitorManager.FrameUpdate();
}
}
}
}

View File

@@ -46,6 +46,8 @@ namespace Content.Client.Examine
public override void Initialize()
{
base.Initialize();
UpdatesOutsidePrediction = true;
SubscribeLocalEvent<GetVerbsEvent<ExamineVerb>>(AddExamineVerb);

View File

@@ -2,6 +2,7 @@ using Content.Client.Administration.Managers;
using Content.Client.Changelog;
using Content.Client.Chat.Managers;
using Content.Client.Clickable;
using Content.Client.DebugMon;
using Content.Client.Eui;
using Content.Client.GhostKick;
using Content.Client.Launcher;
@@ -48,6 +49,7 @@ namespace Content.Client.IoC
collection.Register<DocumentParsingManager>();
collection.Register<ContentReplayPlaybackManager, ContentReplayPlaybackManager>();
collection.Register<ISharedPlaytimeManager, JobRequirementsManager>();
collection.Register<DebugMonitorManager>();
}
}
}

View File

@@ -480,10 +480,10 @@ namespace Content.Client.Lobby.UI
return;
}
//Setup model
Dictionary<string, List<string>> model = new();
// Setup model
Dictionary<string, List<string>> traitGroups = new();
List<string> defaultTraits = new();
model.Add("default", defaultTraits);
traitGroups.Add(TraitCategoryPrototype.Default, defaultTraits);
foreach (var trait in traits)
{
@@ -493,18 +493,19 @@ namespace Content.Client.Lobby.UI
continue;
}
if (!model.ContainsKey(trait.Category))
{
model.Add(trait.Category, new());
}
model[trait.Category].Add(trait.ID);
if (!_prototypeManager.HasIndex(trait.Category))
continue;
var group = traitGroups.GetOrNew(trait.Category);
group.Add(trait.ID);
}
//Create UI view from model
foreach (var (categoryId, traitId) in model)
// Create UI view from model
foreach (var (categoryId, categoryTraits) in traitGroups)
{
TraitCategoryPrototype? category = null;
if (categoryId != "default")
if (categoryId != TraitCategoryPrototype.Default)
{
category = _prototypeManager.Index<TraitCategoryPrototype>(categoryId);
// Label
@@ -519,7 +520,7 @@ namespace Content.Client.Lobby.UI
List<TraitPreferenceSelector?> selectors = new();
var selectionCount = 0;
foreach (var traitProto in traitId)
foreach (var traitProto in categoryTraits)
{
var trait = _prototypeManager.Index<TraitPrototype>(traitProto);
var selector = new TraitPreferenceSelector(trait);
@@ -530,7 +531,15 @@ namespace Content.Client.Lobby.UI
selector.PreferenceChanged += preference =>
{
Profile = Profile?.WithTraitPreference(trait.ID, categoryId, preference);
if (preference)
{
Profile = Profile?.WithTraitPreference(trait.ID, _prototypeManager);
}
else
{
Profile = Profile?.WithoutTraitPreference(trait.ID, _prototypeManager);
}
SetDirty();
RefreshTraits(); // If too many traits are selected, they will be reset to the real value.
};

View File

@@ -0,0 +1,6 @@
<Control xmlns="https://spacestation14.io">
<BoxContainer Orientation="Horizontal">
<Label Name="NameLabel" MinWidth="400" />
<OptionButton Name="Button" Access="Public" />
</BoxContainer>
</Control>

View File

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

View File

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

View File

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

View File

@@ -7,5 +7,6 @@
<tabs:GraphicsTab Name="GraphicsTab" />
<tabs:KeyRebindTab Name="KeyRebindTab" />
<tabs:AudioTab Name="AudioTab" />
<tabs:AccessibilityTab Name="AccessibilityTab" />
</TabContainer>
</DefaultWindow>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,128 +1,26 @@
<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-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" />
<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>

View File

@@ -3,200 +3,72 @@ using Content.Shared.CCVar;
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;
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;
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();
}
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(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;
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 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;
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));
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);
}
}

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>

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

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>

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

View File

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

View File

@@ -230,7 +230,7 @@ public sealed class AdminUIController : UIController,
if (function == EngineKeyFunctions.UIClick)
_conHost.ExecuteCommand($"vv {uid}");
else if (function == EngineKeyFunctions.UseSecondary)
else if (function == EngineKeyFunctions.UIRightClick)
_verb.OpenVerbMenu(uid, true);
else
return;

View File

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

View File

@@ -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;
@@ -36,6 +41,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
@@ -72,11 +93,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;
}

View File

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

View File

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

View File

@@ -86,7 +86,8 @@ public sealed class DecalPainter
image.Mutate(o => o.Rotate((float) -decal.Angle.Degrees));
var coloredImage = new Image<Rgba32>(image.Width, image.Height);
Color color = decal.Color?.ConvertImgSharp() ?? Color.White;
Color color = decal.Color?.WithAlpha(byte.MaxValue).ConvertImgSharp() ?? Color.White; // remove the encoded color alpha here
var alpha = decal.Color?.A ?? 1; // get the alpha separately so we can use it in DrawImage
coloredImage.Mutate(o => o.BackgroundColor(color));
image.Mutate(o => o
@@ -95,6 +96,6 @@ public sealed class DecalPainter
// Very unsure why the - 1 is needed in the first place but all decals are off by exactly one pixel otherwise
// Woohoo!
canvas.Mutate(o => o.DrawImage(image, new Point((int) data.X, (int) data.Y - 1), 1.0f));
canvas.Mutate(o => o.DrawImage(image, new Point((int) data.X, (int) data.Y - 1), alpha));
}
}

View File

@@ -123,19 +123,28 @@ public sealed class EntityPainter
image.Mutate(o => o.Crop(rect));
var spriteRotation = 0f;
if (!entity.Sprite.NoRotation && !entity.Sprite.SnapCardinals && entity.Sprite.GetLayerDirectionCount(layer) == 1)
{
spriteRotation = (float) worldRotation.Degrees;
}
var colorMix = entity.Sprite.Color * layer.Color;
var imageColor = Color.FromRgba(colorMix.RByte, colorMix.GByte, colorMix.BByte, colorMix.AByte);
var coloredImage = new Image<Rgba32>(image.Width, image.Height);
coloredImage.Mutate(o => o.BackgroundColor(imageColor));
var (imgX, imgY) = rsi?.Size ?? (EyeManager.PixelsPerMeter, EyeManager.PixelsPerMeter);
var offsetX = (int) (entity.Sprite.Offset.X * EyeManager.PixelsPerMeter);
var offsetY = (int) (entity.Sprite.Offset.Y * EyeManager.PixelsPerMeter);
image.Mutate(o => o
.DrawImage(coloredImage, PixelColorBlendingMode.Multiply, PixelAlphaCompositionMode.SrcAtop, 1)
.Resize(imgX, imgY)
.Flip(FlipMode.Vertical));
.Flip(FlipMode.Vertical)
.Rotate(spriteRotation));
var pointX = (int) entity.X - imgX / 2 + EyeManager.PixelsPerMeter / 2;
var pointY = (int) entity.Y - imgY / 2 + EyeManager.PixelsPerMeter / 2;
var pointX = (int) entity.X + offsetX - imgX / 2;
var pointY = (int) entity.Y + offsetY - imgY / 2;
canvas.Mutate(o => o.DrawImage(image, new Point(pointX, pointY), 1));
}
}

View File

@@ -138,8 +138,8 @@ namespace Content.MapRenderer.Painters
var yOffset = (int) -grid.LocalAABB.Bottom;
var tileSize = grid.TileSize;
var x = ((float) Math.Floor(position.X) + xOffset) * tileSize * TilePainter.TileImageSize;
var y = ((float) Math.Floor(position.Y) + yOffset) * tileSize * TilePainter.TileImageSize;
var x = (position.X + xOffset) * tileSize * TilePainter.TileImageSize;
var y = (position.Y + yOffset) * tileSize * TilePainter.TileImageSize;
return (x, y);
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

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)
{
@@ -1134,4 +1135,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; }
}
}

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

View File

@@ -82,7 +82,10 @@ namespace Content.Server.Atmos.Piping.EntitySystems
continue;
var difference = pipe.Air.Pressure - environment.Pressure;
lost += difference * environment.Volume / (environment.Temperature * Atmospherics.R);
lost += Math.Min(
pipe.Volume / pipe.Air.Volume * pipe.Air.TotalMoles,
difference * environment.Volume / (environment.Temperature * Atmospherics.R)
);
timesLost++;
}

View File

@@ -25,6 +25,9 @@ namespace Content.Server.Atmos.Piping.Other.Components
[DataField("spawnTemperature")]
public float SpawnTemperature { get; set; } = Atmospherics.T20C;
/// <summary>
/// Number of moles created per second when the miner is working.
/// </summary>
[ViewVariables(VVAccess.ReadWrite)]
[DataField("spawnAmount")]
public float SpawnAmount { get; set; } = Atmospherics.MolesCellStandard * 20f;

View File

@@ -24,18 +24,22 @@ namespace Content.Server.Atmos.Piping.Other.EntitySystems
private void OnMinerUpdated(Entity<GasMinerComponent> ent, ref AtmosDeviceUpdateEvent args)
{
var miner = ent.Comp;
if (!CheckMinerOperation(ent, out var environment) || !miner.Enabled || !miner.SpawnGas.HasValue || miner.SpawnAmount <= 0f)
// SpawnAmount is declared in mol/s so to get the amount of gas we hope to mine, we have to multiply this by
// how long we have been waiting to spawn it and further cap the number according to the miner's state.
var toSpawn = CapSpawnAmount(ent, miner.SpawnAmount * args.dt, out var environment);
if (toSpawn <= 0f || environment == null || !miner.Enabled || !miner.SpawnGas.HasValue)
return;
// Time to mine some gas.
var merger = new GasMixture(1) { Temperature = miner.SpawnTemperature };
merger.SetMoles(miner.SpawnGas.Value, miner.SpawnAmount);
merger.SetMoles(miner.SpawnGas.Value, toSpawn);
_atmosphereSystem.Merge(environment, merger);
}
private bool CheckMinerOperation(Entity<GasMinerComponent> ent, [NotNullWhen(true)] out GasMixture? environment)
private float CapSpawnAmount(Entity<GasMinerComponent> ent, float toSpawnTarget, [NotNullWhen(true)] out GasMixture? environment)
{
var (uid, miner) = ent;
var transform = Transform(uid);
@@ -47,33 +51,30 @@ namespace Content.Server.Atmos.Piping.Other.EntitySystems
if (_atmosphereSystem.IsTileSpace(transform.GridUid, transform.MapUid, position))
{
miner.Broken = true;
return false;
return 0f;
}
// Air-blocked location.
if (environment == null)
{
miner.Broken = true;
return false;
return 0f;
}
// External pressure above threshold.
if (!float.IsInfinity(miner.MaxExternalPressure) &&
environment.Pressure > miner.MaxExternalPressure - miner.SpawnAmount * miner.SpawnTemperature * Atmospherics.R / environment.Volume)
{
miner.Broken = true;
return false;
}
// How many moles could we theoretically spawn. Cap by pressure and amount.
var allowableMoles = Math.Min(
(miner.MaxExternalPressure - environment.Pressure) * environment.Volume / (miner.SpawnTemperature * Atmospherics.R),
miner.MaxExternalAmount - environment.TotalMoles);
// External gas amount above threshold.
if (!float.IsInfinity(miner.MaxExternalAmount) && environment.TotalMoles > miner.MaxExternalAmount)
{
var toSpawnReal = Math.Clamp(allowableMoles, 0f, toSpawnTarget);
if (toSpawnReal < Atmospherics.GasMinMoles) {
miner.Broken = true;
return false;
return 0f;
}
miner.Broken = false;
return true;
return toSpawnReal;
}
}
}

View File

@@ -10,7 +10,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
public sealed partial class GasVentPumpComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled { get; set; } = true;
public bool Enabled { get; set; } = false;
[ViewVariables]
public bool IsDirty { get; set; } = false;
@@ -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)

View File

@@ -10,7 +10,7 @@ namespace Content.Server.Atmos.Piping.Unary.Components
public sealed partial class GasVentScrubberComponent : Component
{
[ViewVariables(VVAccess.ReadWrite)]
public bool Enabled { get; set; } = true;
public bool Enabled { get; set; } = false;
[ViewVariables]
public bool IsDirty { get; set; } = false;

View File

@@ -83,13 +83,18 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
var timeDelta = args.dt;
var pressureDelta = timeDelta * vent.TargetPressureChange;
var lockout = (environment.Pressure < vent.UnderPressureLockoutThreshold);
if (vent.UnderPressureLockout != lockout) // update visuals only if this changes
{
vent.UnderPressureLockout = lockout;
UpdateState(uid, vent);
}
if (vent.PumpDirection == VentPumpDirection.Releasing && pipe.Air.Pressure > 0)
{
if (environment.Pressure > vent.MaxPressure)
return;
vent.UnderPressureLockout = (environment.Pressure < vent.UnderPressureLockoutThreshold);
if ((vent.PressureChecks & VentPressureBound.ExternalBound) != 0)
{
// Vents cannot supply high pressures from an almost empty pipe, instead it's proportional to the pipe
@@ -267,7 +272,10 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
}
else if (vent.PumpDirection == VentPumpDirection.Releasing)
{
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Out, appearance);
if (vent.UnderPressureLockout & !vent.PressureLockoutOverride)
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Lockout, appearance);
else
_appearance.SetData(uid, VentPumpVisuals.State, VentPumpState.Out, appearance);
}
else if (vent.PumpDirection == VentPumpDirection.Siphoning)
{
@@ -281,7 +289,7 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
return;
if (args.IsInDetailsRange)
{
if (pumpComponent.UnderPressureLockout & !pumpComponent.PressureLockoutOverride)
if (pumpComponent.PumpDirection == VentPumpDirection.Releasing & pumpComponent.UnderPressureLockout & !pumpComponent.PressureLockoutOverride)
{
args.PushMarkup(Loc.GetString("gas-vent-pump-uvlo"));
}

View File

@@ -1,6 +1,6 @@
using Content.Server.Body.Components;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.ReactionEffects;
using Content.Server.EntityEffects.Effects;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;

View File

@@ -6,6 +6,7 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.FixedPoint;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
@@ -193,8 +194,7 @@ namespace Content.Server.Body.Systems
}
var actualEntity = ent.Comp2?.Body ?? solutionEntityUid.Value;
var args = new ReagentEffectArgs(actualEntity, ent, solution, proto, mostToRemove,
EntityManager, null, scale);
var args = new EntityEffectReagentArgs(actualEntity, EntityManager, ent, solution, mostToRemove, proto, null, scale);
// do all effects, if conditions apply
foreach (var effect in entry.Effects)

View File

@@ -3,8 +3,8 @@ using Content.Server.Atmos.EntitySystems;
using Content.Server.Body.Components;
using Content.Server.Chat.Systems;
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Server.Chemistry.ReagentEffectConditions;
using Content.Server.Chemistry.ReagentEffects;
using Content.Server.EntityEffects.EffectConditions;
using Content.Server.EntityEffects.Effects;
using Content.Shared.Alert;
using Content.Shared.Atmos;
using Content.Shared.Body.Components;
@@ -13,6 +13,7 @@ using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.Database;
using Content.Shared.EntityEffects;
using Content.Shared.Mobs.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
@@ -261,7 +262,7 @@ public sealed class RespiratorSystem : EntitySystem
// TODO generalize condition checks
// this is pretty janky, but I just want to bodge a method that checks if an entity can breathe a gas mixture
// Applying actual reaction effects require a full ReagentEffectArgs struct.
bool CanMetabolize(ReagentEffect effect)
bool CanMetabolize(EntityEffect effect)
{
if (effect.Conditions == null)
return true;

View File

@@ -1,7 +1,7 @@
using Content.Server.Botany.Components;
using Content.Server.Botany.Systems;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.EntityEffects;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
@@ -80,7 +80,7 @@ public partial struct SeedChemQuantity
// TODO reduce the number of friends to a reasonable level. Requires ECS-ing things like plant holder component.
[Virtual, DataDefinition]
[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(ReagentEffect), typeof(MutationSystem))]
[Access(typeof(BotanySystem), typeof(PlantHolderSystem), typeof(SeedExtractorSystem), typeof(PlantHolderComponent), typeof(EntityEffect), typeof(MutationSystem))]
public partial class SeedData
{
#region Tracking

View File

@@ -1,64 +1,7 @@
using Content.Shared.ActionBlocker;
using Content.Shared.Chat.TypingIndicator;
using Robust.Shared.Player;
namespace Content.Server.Chat.TypingIndicator;
// Server-side typing system
// It receives networked typing events from clients
// And sync typing indicator using appearance component
public sealed class TypingIndicatorSystem : SharedTypingIndicatorSystem
{
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PlayerAttachedEvent>(OnPlayerAttached);
SubscribeLocalEvent<TypingIndicatorComponent, PlayerDetachedEvent>(OnPlayerDetached);
SubscribeNetworkEvent<TypingChangedEvent>(OnClientTypingChanged);
}
private void OnPlayerAttached(PlayerAttachedEvent ev)
{
// when player poses entity we want to make sure that there is typing indicator
EnsureComp<TypingIndicatorComponent>(ev.Entity);
// we also need appearance component to sync visual state
EnsureComp<AppearanceComponent>(ev.Entity);
}
private void OnPlayerDetached(EntityUid uid, TypingIndicatorComponent component, PlayerDetachedEvent args)
{
// player left entity body - hide typing indicator
SetTypingIndicatorEnabled(uid, false);
}
private void OnClientTypingChanged(TypingChangedEvent ev, EntitySessionEventArgs args)
{
var uid = args.SenderSession.AttachedEntity;
if (!Exists(uid))
{
Log.Warning($"Client {args.SenderSession} sent TypingChangedEvent without an attached entity.");
return;
}
// check if this entity can speak or emote
if (!_actionBlocker.CanEmote(uid.Value) && !_actionBlocker.CanSpeak(uid.Value))
{
// nah, make sure that typing indicator is disabled
SetTypingIndicatorEnabled(uid.Value, false);
return;
}
SetTypingIndicatorEnabled(uid.Value, ev.IsTyping);
}
private void SetTypingIndicatorEnabled(EntityUid uid, bool isEnabled, AppearanceComponent? appearance = null)
{
if (!Resolve(uid, ref appearance, false))
return;
_appearance.SetData(uid, TypingIndicatorVisuals.IsTyping, isEnabled, appearance);
}
}

View File

@@ -1,7 +1,7 @@
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components.SolutionManager;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Chemistry;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Forensics;

View File

@@ -1,85 +0,0 @@
using Content.Server.Fluids.EntitySystems;
using Content.Shared.Audio;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Database;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.ReactionEffects
{
/// <summary>
/// Basically smoke and foam reactions.
/// </summary>
[UsedImplicitly]
[DataDefinition]
public sealed partial class AreaReactionEffect : ReagentEffect
{
/// <summary>
/// How many seconds will the effect stay, counting after fully spreading.
/// </summary>
[DataField("duration")] private float _duration = 10;
/// <summary>
/// How many units of reaction for 1 smoke entity.
/// </summary>
[DataField] public FixedPoint2 OverflowThreshold = FixedPoint2.New(2.5);
/// <summary>
/// The entity prototype that will be spawned as the effect.
/// </summary>
[DataField("prototypeId", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<EntityPrototype>))]
private string _prototypeId = default!;
/// <summary>
/// Sound that will get played when this reaction effect occurs.
/// </summary>
[DataField("sound", required: true)] private SoundSpecifier _sound = default!;
public override bool ShouldLog => true;
protected override string ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-area-reaction",
("duration", _duration)
);
public override LogImpact LogImpact => LogImpact.High;
public override void Effect(ReagentEffectArgs args)
{
if (args.Source == null)
return;
var spreadAmount = (int) Math.Max(0, Math.Ceiling((args.Quantity / OverflowThreshold).Float()));
var splitSolution = args.Source.SplitSolution(args.Source.Volume);
var transform = args.EntityManager.GetComponent<TransformComponent>(args.SolutionEntity);
var mapManager = IoCManager.Resolve<IMapManager>();
var mapSys = args.EntityManager.System<MapSystem>();
var sys = args.EntityManager.System<TransformSystem>();
var mapCoords = sys.GetMapCoordinates(args.SolutionEntity, xform: transform);
if (!mapManager.TryFindGridAt(mapCoords, out var gridUid, out var grid) ||
!mapSys.TryGetTileRef(gridUid, grid, transform.Coordinates, out var tileRef) ||
tileRef.Tile.IsSpace())
{
return;
}
var coords = mapSys.MapToGrid(gridUid, mapCoords);
var ent = args.EntityManager.SpawnEntity(_prototypeId, coords.SnapToGrid());
var smoke = args.EntityManager.System<SmokeSystem>();
smoke.StartSmoke(ent, splitSolution, _duration, spreadAmount);
var audio = args.EntityManager.System<SharedAudioSystem>();
audio.PlayPvs(_sound, args.SolutionEntity, AudioHelpers.WithVariation(0.125f));
}
}
}

View File

@@ -1,73 +0,0 @@
using Content.Server.Explosion.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using Content.Shared.Explosion;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using System.Text.Json.Serialization;
namespace Content.Server.Chemistry.ReactionEffects
{
[DataDefinition]
public sealed partial class ExplosionReactionEffect : ReagentEffect
{
/// <summary>
/// The type of explosion. Determines damage types and tile break chance scaling.
/// </summary>
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<ExplosionPrototype>))]
[JsonIgnore]
public string ExplosionType = default!;
/// <summary>
/// The max intensity the explosion can have at a given tile. Places an upper limit of damage and tile break
/// chance.
/// </summary>
[DataField]
[JsonIgnore]
public float MaxIntensity = 5;
/// <summary>
/// How quickly intensity drops off as you move away from the epicenter
/// </summary>
[DataField]
[JsonIgnore]
public float IntensitySlope = 1;
/// <summary>
/// The maximum total intensity that this chemical reaction can achieve. Basically here to prevent people
/// from creating a nuke by collecting enough potassium and water.
/// </summary>
/// <remarks>
/// A slope of 1 and MaxTotalIntensity of 100 corresponds to a radius of around 4.5 tiles.
/// </remarks>
[DataField]
[JsonIgnore]
public float MaxTotalIntensity = 100;
/// <summary>
/// The intensity of the explosion per unit reaction.
/// </summary>
[DataField]
[JsonIgnore]
public float IntensityPerUnit = 1;
public override bool ShouldLog => true;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-explosion-reaction-effect", ("chance", Probability));
public override LogImpact LogImpact => LogImpact.High;
public override void Effect(ReagentEffectArgs args)
{
var intensity = MathF.Min((float) args.Quantity * IntensityPerUnit, MaxTotalIntensity);
args.EntityManager.System<ExplosionSystem>()
.QueueExplosion(
args.SolutionEntity,
ExplosionType,
intensity,
IntensitySlope,
MaxIntensity);
}
}
}

View File

@@ -1,121 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReactionEffects
{
/// <summary>
/// Sets the temperature of the solution involved with the reaction to a new value.
/// </summary>
[DataDefinition]
public sealed partial class SetSolutionTemperatureEffect : ReagentEffect
{
/// <summary>
/// The temperature to set the solution to.
/// </summary>
[DataField("temperature", required: true)] private float _temperature;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-set-solution-temperature-effect",
("chance", Probability), ("temperature", _temperature));
public override void Effect(ReagentEffectArgs args)
{
var solution = args.Source;
if (solution == null)
return;
solution.Temperature = _temperature;
}
}
/// <summary>
/// Adjusts the temperature of the solution involved in the reaction.
/// </summary>
[DataDefinition]
public sealed partial class AdjustSolutionTemperatureEffect : ReagentEffect
{
/// <summary>
/// The change in temperature.
/// </summary>
[DataField("delta", required: true)] private float _delta;
/// <summary>
/// The minimum temperature this effect can reach.
/// </summary>
[DataField("minTemp")] private float _minTemp = 0.0f;
/// <summary>
/// The maximum temperature this effect can reach.
/// </summary>
[DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity;
/// <summary>
/// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
/// </summary>
[DataField("scaled")] private bool _scaled;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect",
("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp));
public override void Effect(ReagentEffectArgs args)
{
var solution = args.Source;
if (solution == null || solution.Volume == 0)
return;
var deltaT = _scaled ? _delta * (float) args.Quantity : _delta;
solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp);
}
}
/// <summary>
/// Adjusts the thermal energy of the solution involved in the reaction.
/// </summary>
public sealed partial class AdjustSolutionThermalEnergyEffect : ReagentEffect
{
/// <summary>
/// The change in energy.
/// </summary>
[DataField("delta", required: true)] private float _delta;
/// <summary>
/// The minimum temperature this effect can reach.
/// </summary>
[DataField("minTemp")] private float _minTemp = 0.0f;
/// <summary>
/// The maximum temperature this effect can reach.
/// </summary>
[DataField("maxTemp")] private float _maxTemp = float.PositiveInfinity;
/// <summary>
/// If true, then scale ranges by intensity. If not, the ranges are the same regardless of reactant amount.
/// </summary>
[DataField("scaled")] private bool _scaled;
public override void Effect(ReagentEffectArgs args)
{
var solution = args.Source;
if (solution == null || solution.Volume == 0)
return;
if (_delta > 0 && solution.Temperature >= _maxTemp)
return;
if (_delta < 0 && solution.Temperature <= _minTemp)
return;
var heatCap = solution.GetHeatCapacity(null);
var deltaT = _scaled
? _delta / heatCap * (float) args.Quantity
: _delta / heatCap;
solution.Temperature = Math.Clamp(solution.Temperature + deltaT, _minTemp, _maxTemp);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-adjust-solution-temperature-effect",
("chance", Probability), ("deltasign", MathF.Sign(_delta)), ("mintemp", _minTemp), ("maxtemp", _maxTemp));
}
}

View File

@@ -1,36 +0,0 @@
using Content.Server.Temperature.Components;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
/// <summary>
/// Requires the solution entity to be above or below a certain temperature.
/// Used for things like cryoxadone and pyroxadone.
/// </summary>
public sealed partial class Temperature : ReagentEffectCondition
{
[DataField]
public float Min = 0;
[DataField]
public float Max = float.PositiveInfinity;
public override bool Condition(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out TemperatureComponent? temp))
{
if (temp.CurrentTemperature > Min && temp.CurrentTemperature < Max)
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-body-temperature",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}
}

View File

@@ -1,49 +0,0 @@
using System.Linq;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Content.Shared.Localizations;
using Robust.Shared.Prototypes;
using Content.Shared.Mind;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Roles.Jobs;
using Content.Shared.Station;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Robust.Shared.IoC;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
public sealed partial class JobCondition : ReagentEffectCondition
{
[DataField(required: true)] public List<ProtoId<JobPrototype>> Job;
public override bool Condition(ReagentEffectArgs args)
{
args.EntityManager.TryGetComponent<MindContainerComponent>(args.SolutionEntity, out var mindContainer);
if (mindContainer != null && mindContainer.Mind != null)
{
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
if (args.EntityManager.TryGetComponent<JobComponent>(mindContainer?.Mind, out var comp) && prototypeManager.TryIndex(comp.Prototype, out var prototype))
{
foreach (var jobId in Job)
{
if (prototype.ID == jobId)
{
return true;
}
}
}
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
var localizedNames = Job.Select(jobId => prototype.Index(jobId).LocalizedName).ToList();
return Loc.GetString("reagent-effect-condition-guidebook-job-condition", ("job", ContentLocalizationManager.FormatListToOr(localizedNames)));
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Components;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
public sealed partial class MobStateCondition : ReagentEffectCondition
{
[DataField]
public MobState Mobstate = MobState.Alive;
public override bool Condition(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out MobStateComponent? mobState))
{
if (mobState.CurrentState == Mobstate)
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-mob-state-condition", ("state", Mobstate));
}
}
}

View File

@@ -1,48 +0,0 @@
using Content.Server.Body.Components;
using Content.Shared.Body.Prototypes;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
/// <summary>
/// Requires that the metabolizing organ is or is not tagged with a certain MetabolizerType
/// </summary>
public sealed partial class OrganType : ReagentEffectCondition
{
[DataField(required: true, customTypeSerializer: typeof(PrototypeIdSerializer<MetabolizerTypePrototype>))]
public string Type = default!;
/// <summary>
/// Does this condition pass when the organ has the type, or when it doesn't have the type?
/// </summary>
[DataField]
public bool ShouldHave = true;
public override bool Condition(ReagentEffectArgs args)
{
if (args.OrganEntity == null)
return false;
return Condition(args.OrganEntity.Value, args.EntityManager);
}
public bool Condition(Entity<MetabolizerComponent?> metabolizer, IEntityManager entMan)
{
metabolizer.Comp ??= entMan.GetComponentOrNull<MetabolizerComponent>(metabolizer.Owner);
if (metabolizer.Comp != null
&& metabolizer.Comp.MetabolizerTypes != null
&& metabolizer.Comp.MetabolizerTypes.Contains(Type))
return ShouldHave;
return !ShouldHave;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-organ-type",
("name", prototype.Index<MetabolizerTypePrototype>(Type).LocalizedName),
("shouldhave", ShouldHave));
}
}
}

View File

@@ -1,51 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
/// <summary>
/// Used for implementing reagent effects that require a certain amount of reagent before it should be applied.
/// For instance, overdoses.
///
/// This can also trigger on -other- reagents, not just the one metabolizing. By default, it uses the
/// one being metabolized.
/// </summary>
public sealed partial class ReagentThreshold : ReagentEffectCondition
{
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
// TODO use ReagentId
[DataField]
public string? Reagent;
public override bool Condition(ReagentEffectArgs args)
{
var reagent = Reagent ?? args.Reagent?.ID;
if (reagent == null)
return true; // No condition to apply.
var quant = FixedPoint2.Zero;
if (args.Source != null)
quant = args.Source.GetTotalPrototypeQuantity(reagent);
return quant >= Min && quant <= Max;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
ReagentPrototype? reagentProto = null;
if (Reagent is not null)
prototype.TryIndex(Reagent, out reagentProto);
return Loc.GetString("reagent-effect-condition-guidebook-reagent-threshold",
("reagent", reagentProto?.LocalizedName ?? Loc.GetString("reagent-effect-condition-guidebook-this-reagent")),
("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()),
("min", Min.Float()));
}
}
}

View File

@@ -1,36 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
/// <summary>
/// Requires the solution to be above or below a certain temperature.
/// Used for things like explosives.
/// </summary>
public sealed partial class SolutionTemperature : ReagentEffectCondition
{
[DataField]
public float Min = 0.0f;
[DataField]
public float Max = float.PositiveInfinity;
public override bool Condition(ReagentEffectArgs args)
{
if (args.Source == null)
return false;
if (args.Source.Temperature < Min)
return false;
if (args.Source.Temperature > Max)
return false;
return true;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-solution-temperature",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}
}

View File

@@ -1,35 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Damage;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
public sealed partial class TotalDamage : ReagentEffectCondition
{
[DataField]
public FixedPoint2 Max = FixedPoint2.MaxValue;
[DataField]
public FixedPoint2 Min = FixedPoint2.Zero;
public override bool Condition(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out DamageableComponent? damage))
{
var total = damage.TotalDamage;
if (total > Min && total < Max)
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-total-damage",
("max", Max == FixedPoint2.MaxValue ? (float) int.MaxValue : Max.Float()),
("min", Min.Float()));
}
}
}

View File

@@ -1,35 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Nutrition.Components;
using Content.Shared.FixedPoint;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffectConditions
{
public sealed partial class Hunger : ReagentEffectCondition
{
[DataField]
public float Max = float.PositiveInfinity;
[DataField]
public float Min = 0;
public override bool Condition(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent(args.SolutionEntity, out HungerComponent? hunger))
{
var total = hunger.CurrentHunger;
if (total > Min && total < Max)
return true;
}
return false;
}
public override string GuidebookExplanation(IPrototypeManager prototype)
{
return Loc.GetString("reagent-effect-condition-guidebook-total-hunger",
("max", float.IsPositiveInfinity(Max) ? (float) int.MaxValue : Max),
("min", Min));
}
}
}

View File

@@ -1,31 +0,0 @@
using Content.Server.Chemistry.Containers.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects
{
[UsedImplicitly]
public sealed partial class AddToSolutionReaction : ReagentEffect
{
[DataField("solution")]
private string _solution = "reagents";
public override void Effect(ReagentEffectArgs args)
{
if (args.Reagent == null)
return;
// TODO see if this is correct
var solutionContainerSystem = args.EntityManager.System<SolutionContainerSystem>();
if (!solutionContainerSystem.TryGetSolution(args.SolutionEntity, _solution, out var solutionContainer))
return;
if (solutionContainerSystem.TryAddReagent(solutionContainer.Value, args.Reagent.ID, args.Quantity, out var accepted))
args.Source?.RemoveReagent(args.Reagent.ID, accepted);
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys) =>
Loc.GetString("reagent-effect-guidebook-add-to-solution-reaction", ("chance", Probability));
}
}

View File

@@ -1,33 +0,0 @@
using Content.Server.Body.Systems;
using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReactionEffects
{
/// <summary>
/// Basically smoke and foam reactions.
/// </summary>
[UsedImplicitly]
public sealed partial class ChemCleanBloodstream : ReagentEffect
{
[DataField]
public float CleanseRate = 3.0f;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-chem-clean-bloodstream", ("chance", Probability));
public override void Effect(ReagentEffectArgs args)
{
if (args.Source == null || args.Reagent == null)
return;
var cleanseRate = CleanseRate;
cleanseRate *= args.Scale;
var bloodstreamSys = args.EntityManager.System<BloodstreamSystem>();
bloodstreamSys.FlushChemicals(args.SolutionEntity, args.Reagent.ID, cleanseRate);
}
}
}

View File

@@ -1,32 +0,0 @@
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Eye.Blinding;
using Content.Shared.Eye.Blinding.Systems;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects
{
/// <summary>
/// Heal or apply eye damage
/// </summary>
[UsedImplicitly]
public sealed partial class ChemHealEyeDamage : ReagentEffect
{
/// <summary>
/// How much eye damage to add.
/// </summary>
[DataField]
public int Amount = -1;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-cure-eye-damage", ("chance", Probability), ("deltasign", MathF.Sign(Amount)));
public override void Effect(ReagentEffectArgs args)
{
if (args.Scale != 1f) // huh?
return;
args.EntityManager.EntitySysManager.GetEntitySystem<BlindableSystem>().AdjustEyeDamage(args.SolutionEntity, Amount);
}
}
}

View File

@@ -1,31 +0,0 @@
using Content.Server.Electrocution;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects;
public sealed partial class Electrocute : ReagentEffect
{
[DataField] public int ElectrocuteTime = 2;
[DataField] public int ElectrocuteDamageScale = 5;
/// <remarks>
/// true - refresh electrocute time, false - accumulate electrocute time
/// </remarks>
[DataField] public bool Refresh = true;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-electrocute", ("chance", Probability), ("time", ElectrocuteTime));
public override bool ShouldLog => true;
public override void Effect(ReagentEffectArgs args)
{
args.EntityManager.System<ElectrocutionSystem>().TryDoElectrocution(args.SolutionEntity, null,
Math.Max((args.Quantity * ElectrocuteDamageScale).Int(), 1), TimeSpan.FromSeconds(ElectrocuteTime), Refresh, ignoreInsulation: true);
if (args.Reagent != null)
args.Source?.RemoveReagent(args.Reagent.ID, args.Quantity);
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects
{
[UsedImplicitly]
public sealed partial class ExtinguishReaction : ReagentEffect
{
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-extinguish-reaction", ("chance", Probability));
public override void Effect(ReagentEffectArgs args)
{
if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out FlammableComponent? flammable)) return;
var flammableSystem = args.EntityManager.System<FlammableSystem>();
flammableSystem.Extinguish(args.SolutionEntity, flammable);
flammableSystem.AdjustFireStacks(args.SolutionEntity, -1.5f * (float) args.Quantity, flammable);
}
}
}

View File

@@ -1,34 +0,0 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Database;
using JetBrains.Annotations;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects
{
[UsedImplicitly]
public sealed partial class FlammableReaction : ReagentEffect
{
[DataField]
public float Multiplier = 0.05f;
public override bool ShouldLog => true;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> Loc.GetString("reagent-effect-guidebook-flammable-reaction", ("chance", Probability));
public override LogImpact LogImpact => LogImpact.Medium;
public override void Effect(ReagentEffectArgs args)
{
if (!args.EntityManager.TryGetComponent(args.SolutionEntity, out FlammableComponent? flammable))
return;
args.EntityManager.System<FlammableSystem>().AdjustFireStacks(args.SolutionEntity, args.Quantity.Float() * Multiplier, flammable);
if (args.Reagent != null)
args.Source?.RemoveReagent(args.Reagent.ID, args.Quantity);
}
}
}

View File

@@ -1,30 +0,0 @@
using Content.Server.Body.Components;
using Content.Shared.Atmos;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects;
public sealed partial class ModifyLungGas : ReagentEffect
{
[DataField("ratios", required: true)]
private Dictionary<Gas, float> _ratios = default!;
// JUSTIFICATION: This is internal magic that players never directly interact with.
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> null;
public override void Effect(ReagentEffectArgs args)
{
if (!args.EntityManager.TryGetComponent<LungComponent>(args.OrganEntity, out var lung))
return;
foreach (var (gas, ratio) in _ratios)
{
var quantity = ratio * args.Quantity.Float() / Atmospherics.BreathMolesToReagentMultiplier;
if (quantity < 0)
quantity = Math.Max(quantity, -lung.Air[(int)gas]);
lung.Air.AdjustMoles(gas, quantity);
}
}
}

View File

@@ -1,75 +0,0 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Movement.Systems;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Server.Chemistry.ReagentEffects
{
/// <summary>
/// Default metabolism for stimulants and tranqs. Attempts to find a MovementSpeedModifier on the target,
/// adding one if not there and to change the movespeed
/// </summary>
public sealed partial class MovespeedModifier : ReagentEffect
{
/// <summary>
/// How much the entities' walk speed is multiplied by.
/// </summary>
[DataField]
public float WalkSpeedModifier { get; set; } = 1;
/// <summary>
/// How much the entities' run speed is multiplied by.
/// </summary>
[DataField]
public float SprintSpeedModifier { get; set; } = 1;
/// <summary>
/// How long the modifier applies (in seconds) when metabolized.
/// </summary>
[DataField]
public float StatusLifetime = 2f;
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
return Loc.GetString("reagent-effect-guidebook-movespeed-modifier",
("chance", Probability),
("walkspeed", WalkSpeedModifier),
("time", StatusLifetime));
}
/// <summary>
/// Remove reagent at set rate, changes the movespeed modifiers and adds a MovespeedModifierMetabolismComponent if not already there.
/// </summary>
public override void Effect(ReagentEffectArgs args)
{
var status = args.EntityManager.EnsureComponent<MovespeedModifierMetabolismComponent>(args.SolutionEntity);
// Only refresh movement if we need to.
var modified = !status.WalkSpeedModifier.Equals(WalkSpeedModifier) ||
!status.SprintSpeedModifier.Equals(SprintSpeedModifier);
status.WalkSpeedModifier = WalkSpeedModifier;
status.SprintSpeedModifier = SprintSpeedModifier;
// only going to scale application time
var statusLifetime = StatusLifetime;
statusLifetime *= args.Scale;
IncreaseTimer(status, statusLifetime);
if (modified)
args.EntityManager.System<MovementSpeedModifierSystem>().RefreshMovementSpeedModifiers(args.SolutionEntity);
}
public void IncreaseTimer(MovespeedModifierMetabolismComponent status, float time)
{
var gameTiming = IoCManager.Resolve<IGameTiming>();
var offsetTime = Math.Max(status.ModifierTimer.TotalSeconds, gameTiming.CurTime.TotalSeconds);
status.ModifierTimer = TimeSpan.FromSeconds(offsetTime + time);
status.Dirty();
}
}
}

View File

@@ -1,25 +0,0 @@
using Content.Server.Body.Components;
using Content.Server.Body.Systems;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.ReagentEffects;
public sealed partial class Oxygenate : ReagentEffect
{
[DataField]
public float Factor = 1f;
// JUSTIFICATION: This is internal magic that players never directly interact with.
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
=> null;
public override void Effect(ReagentEffectArgs args)
{
if (args.EntityManager.TryGetComponent<RespiratorComponent>(args.SolutionEntity, out var resp))
{
var respSys = args.EntityManager.System<RespiratorSystem>();
respSys.UpdateSaturation(args.SolutionEntity, args.Quantity.Float() * Factor, resp);
}
}
}

View File

@@ -1,62 +0,0 @@
using Content.Server.Botany.Components;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using System.Diagnostics.CodeAnalysis;
namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism
{
[ImplicitDataDefinitionForInheritors]
public abstract partial class PlantAdjustAttribute : ReagentEffect
{
[DataField]
public float Amount { get; protected set; } = 1;
/// <summary>
/// Localisation key for the name of the adjusted attribute. Used for guidebook descriptions.
/// </summary>
[DataField]
public abstract string GuidebookAttributeName { get; set; }
/// <summary>
/// Whether the attribute in question is a good thing. Used for guidebook descriptions to determine the color of the number.
/// </summary>
[DataField]
public virtual bool GuidebookIsAttributePositive { get; protected set; } = true;
/// <summary>
/// Checks if the plant holder can metabolize the reagent or not. Checks if it has an alive plant by default.
/// </summary>
/// <param name="plantHolder">The entity holding the plant</param>
/// <param name="plantHolderComponent">The plant holder component</param>
/// <param name="entityManager">The entity manager</param>
/// <param name="mustHaveAlivePlant">Whether to check if it has an alive plant or not</param>
/// <returns></returns>
public bool CanMetabolize(EntityUid plantHolder, [NotNullWhen(true)] out PlantHolderComponent? plantHolderComponent,
IEntityManager entityManager,
bool mustHaveAlivePlant = true)
{
plantHolderComponent = null;
if (!entityManager.TryGetComponent(plantHolder, out plantHolderComponent)
|| mustHaveAlivePlant && (plantHolderComponent.Seed == null || plantHolderComponent.Dead))
return false;
return true;
}
protected override string? ReagentEffectGuidebookText(IPrototypeManager prototype, IEntitySystemManager entSys)
{
string color;
if (GuidebookIsAttributePositive ^ Amount < 0.0)
{
color = "green";
}
else
{
color = "red";
}
return Loc.GetString("reagent-effect-guidebook-plant-attribute", ("attribute", Loc.GetString(GuidebookAttributeName)), ("amount", Amount.ToString("0.00")), ("colorName", color), ("chance", Probability));
}
}
}

View File

@@ -1,22 +0,0 @@
using Content.Server.Botany.Systems;
using Content.Shared.Chemistry.Reagent;
namespace Content.Server.Chemistry.ReagentEffects.PlantMetabolism
{
public sealed partial class PlantAdjustHealth : PlantAdjustAttribute
{
public override string GuidebookAttributeName { get; set; } = "plant-attribute-health";
public override void Effect(ReagentEffectArgs args)
{
if (!CanMetabolize(args.SolutionEntity, out var plantHolderComp, args.EntityManager))
return;
var plantHolder = args.EntityManager.System<PlantHolderSystem>();
plantHolderComp.Health += Amount;
plantHolder.CheckHealth(args.SolutionEntity, plantHolderComp);
}
}
}

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