Merge remote-tracking branch 'upstream/stable' into ed-30-04-2025-upstream-sync

# Conflicts:
#	Content.Client/Parallax/ParallaxControl.cs
#	Content.Client/UserInterface/Systems/Storage/Controls/ItemGridPiece.cs
#	Content.IntegrationTests/Tests/PostMapInitTest.cs
#	Content.Server/Chat/Managers/ChatManager.cs
#	Content.Server/Fluids/EntitySystems/PuddleSystem.Evaporation.cs
#	Content.Server/Labels/Label/LabelSystem.cs
#	Content.Shared/Actions/SharedActionsSystem.cs
#	Content.Shared/Fluids/Components/EvaporationComponent.cs
#	Content.Shared/Labels/EntitySystems/SharedLabelSystem.cs
#	README.md
#	Resources/Prototypes/Entities/Mobs/Player/admin_ghost.yml
#	Resources/Prototypes/Maps/Pools/deathmatch.yml
#	Resources/Prototypes/Maps/arenas.yml
This commit is contained in:
Ed
2025-04-30 20:31:50 +03:00
2053 changed files with 113995 additions and 38376 deletions

View File

@@ -127,6 +127,7 @@ csharp_indent_braces = false
#csharp_indent_case_contents_when_block = true
#csharp_indent_labels = one_less_than_current
csharp_indent_switch_labels = true
xmldoc_indent_text = zeroindent
# Space preferences
csharp_space_after_cast = false

View File

@@ -12,7 +12,7 @@ jobs:
steps:
- uses: superbrothers/close-pull-request@v3
with:
comment: "Thank you for contributing to the Space Station 14 repository. Unfortunately, it looks like you submitted your pull request from the master branch. We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html) \n\n You can move your current work from the master branch to another branch by doing `git branch <branch_name` and resetting the master branch."
comment: "Thank you for your contribution! It appears you created a PR from your master branch, this is [something you should avoid doing](https://jmeridth.com/posts/do-not-issue-pull-requests-from-your-master-branch/), and thus this PR has been automatically closed. \n \n We suggest you follow [our git usage documentation](https://docs.spacestation14.com/en/general-development/setup/git-for-the-ss14-developer.html). \n \n You can move your current work from the master branch to another branch by following [these commands](https://ohshitgit.com/#accidental-commit-master). And then you may recreate your PR using the new branch."
# If you prefer to just comment on the pr and not close it, uncomment the bellow and comment the above

44
.vscode/tasks.json vendored
View File

@@ -32,6 +32,50 @@
"/consoleloggerparameters:'ForceNoAlign;NoSummary'"
],
"problemMatcher": "$msCompile"
},
{
"label": "test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.Tests/Content.Tests.csproj",
"--",
"NUnit.ConsoleOut=0"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
},
{
"label": "integration-test",
"command": "dotnet",
"type": "shell",
"args": [
"test",
"--no-build",
"--configuration",
"DebugOpt",
"Content.IntegrationTests/Content.IntegrationTests.csproj",
"--",
"NUnit.ConsoleOut=0",
"NUnit.MapWarningTo=Failed.ConsoleOut=0",
"NUnit.MapWarningTo=Failed"
],
"group": {
"kind": "test"
},
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}

View File

@@ -7,6 +7,7 @@ using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Server.Warps;
using Content.Shared.Warps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.EntitySerialization;

View File

@@ -174,7 +174,8 @@ namespace Content.Client.Access.UI
new List<ProtoId<AccessLevelPrototype>>());
var jobIndex = _jobPrototypeIds.IndexOf(state.TargetIdJobPrototype);
// If the job index is < 0 that means they don't have a job registered in the station records.
// If the job index is < 0 that means they don't have a job registered in the station records
// or the IdCardComponent's JobPrototype field.
// For example, a new ID from a box would have no job index.
if (jobIndex < 0)
{

View File

@@ -1,6 +1,7 @@
using System.IO;
using System.Linq;
using Content.Shared.Actions;
using Content.Shared.Charges.Systems;
using JetBrains.Annotations;
using Robust.Client.Player;
using Robust.Shared.ContentPack;
@@ -22,6 +23,7 @@ namespace Content.Client.Actions
{
public delegate void OnActionReplaced(EntityUid actionId);
[Dependency] private readonly SharedChargesSystem _sharedCharges = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IResourceManager _resources = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
@@ -51,29 +53,6 @@ namespace Content.Client.Actions
SubscribeLocalEvent<EntityWorldTargetActionComponent, ComponentHandleState>(OnEntityWorldTargetHandleState);
}
public override void FrameUpdate(float frameTime)
{
base.FrameUpdate(frameTime);
var worldActionQuery = EntityQueryEnumerator<WorldTargetActionComponent>();
while (worldActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var instantActionQuery = EntityQueryEnumerator<InstantActionComponent>();
while (instantActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
var entityActionQuery = EntityQueryEnumerator<EntityTargetActionComponent>();
while (entityActionQuery.MoveNext(out var uid, out var action))
{
UpdateAction(uid, action);
}
}
private void OnInstantHandleState(EntityUid uid, InstantActionComponent component, ref ComponentHandleState args)
{
if (args.Current is not InstantActionComponentState state)
@@ -127,9 +106,6 @@ namespace Content.Client.Actions
component.Toggled = state.Toggled;
component.Cooldown = state.Cooldown;
component.UseDelay = state.UseDelay;
component.Charges = state.Charges;
component.MaxCharges = state.MaxCharges;
component.RenewCharges = state.RenewCharges;
component.Container = EnsureEntity<T>(state.Container, uid);
component.EntityIcon = EnsureEntity<T>(state.EntityIcon, uid);
component.CheckCanInteract = state.CheckCanInteract;
@@ -152,7 +128,8 @@ namespace Content.Client.Actions
if (!ResolveActionData(actionId, ref action))
return;
action.IconColor = action.Charges < 1 ? action.DisabledIconColor : action.OriginalIconColor;
// TODO: Decouple this.
action.IconColor = _sharedCharges.GetCurrentCharges(actionId.Value) == 0 ? action.DisabledIconColor : action.OriginalIconColor;
base.UpdateAction(actionId, action);
if (_playerManager.LocalEntity != action.AttachedEntity)

View File

@@ -6,6 +6,7 @@ using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Ghost;
using Content.Shared.Mind;
using Content.Shared.Roles;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@@ -22,10 +23,11 @@ internal sealed class AdminNameOverlay : Overlay
private readonly IEyeManager _eyeManager;
private readonly EntityLookupSystem _entityLookup;
private readonly IUserInterfaceManager _userInterfaceManager;
private readonly SharedRoleSystem _roles;
private readonly Font _font;
private readonly Font _fontBold;
private bool _overlayClassic;
private bool _overlaySymbols;
private AdminOverlayAntagFormat _overlayFormat;
private AdminOverlayAntagSymbolStyle _overlaySymbolStyle;
private bool _overlayPlaytime;
private bool _overlayStartingJob;
private float _ghostFadeDistance;
@@ -33,9 +35,10 @@ internal sealed class AdminNameOverlay : Overlay
private int _overlayStackMax;
private float _overlayMergeDistance;
//TODO make this adjustable via GUI
//TODO make this adjustable via GUI?
private readonly ProtoId<RoleTypePrototype>[] _filter =
["SoloAntagonist", "TeamAntagonist", "SiliconAntagonist", "FreeAgent"];
private readonly string _antagLabelClassic = Loc.GetString("admin-overlay-antag-classic");
public AdminNameOverlay(
@@ -45,20 +48,22 @@ internal sealed class AdminNameOverlay : Overlay
IResourceCache resourceCache,
EntityLookupSystem entityLookup,
IUserInterfaceManager userInterfaceManager,
IConfigurationManager config)
IConfigurationManager config,
SharedRoleSystem roles)
{
_system = system;
_entityManager = entityManager;
_eyeManager = eyeManager;
_entityLookup = entityLookup;
_userInterfaceManager = userInterfaceManager;
_roles = roles;
ZIndex = 200;
// Setting these to a specific ttf would break the antag symbols
_font = resourceCache.NotoStack();
_fontBold = resourceCache.NotoStack(variation: "Bold");
config.OnValueChanged(CCVars.AdminOverlayClassic, (show) => { _overlayClassic = show; }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbols, (show) => { _overlaySymbols = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayAntagFormat, (show) => { _overlayFormat = UpdateOverlayFormat(show); }, true);
config.OnValueChanged(CCVars.AdminOverlaySymbolStyle, (show) => { _overlaySymbolStyle = UpdateOverlaySymbolStyle(show); }, true);
config.OnValueChanged(CCVars.AdminOverlayPlaytime, (show) => { _overlayPlaytime = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayStartingJob, (show) => { _overlayStartingJob = show; }, true);
config.OnValueChanged(CCVars.AdminOverlayGhostHideDistance, (f) => { _ghostHideDistance = f; }, true);
@@ -67,6 +72,22 @@ internal sealed class AdminNameOverlay : Overlay
config.OnValueChanged(CCVars.AdminOverlayMergeDistance, (f) => { _overlayMergeDistance = f; }, true);
}
private AdminOverlayAntagFormat UpdateOverlayFormat(string formatString)
{
if (!Enum.TryParse<AdminOverlayAntagFormat>(formatString, out var format))
format = AdminOverlayAntagFormat.Binary;
return format;
}
private AdminOverlayAntagSymbolStyle UpdateOverlaySymbolStyle(string symbolString)
{
if (!Enum.TryParse<AdminOverlayAntagSymbolStyle>(symbolString, out var symbolStyle))
symbolStyle = AdminOverlayAntagSymbolStyle.Off;
return symbolStyle;
}
public override OverlaySpace Space => OverlaySpace.ScreenSpace;
protected override void Draw(in OverlayDrawArgs args)
@@ -183,34 +204,56 @@ internal sealed class AdminNameOverlay : Overlay
currentOffset += lineoffset;
}
// Classic Antag Label
if (_overlayClassic && playerInfo.Antag)
// Determine antag symbol
string? symbol;
switch (_overlaySymbolStyle)
{
var symbol = _overlaySymbols ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol",
("symbol", symbol),
("name", _antagLabelClassic))
: _antagLabelClassic;
color = Color.OrangeRed;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
case AdminOverlayAntagSymbolStyle.Specific:
symbol = playerInfo.RoleProto.Symbol;
break;
case AdminOverlayAntagSymbolStyle.Basic:
symbol = Loc.GetString("player-tab-antag-prefix");
break;
default:
case AdminOverlayAntagSymbolStyle.Off:
symbol = string.Empty;
break;
}
// Role Type
else if (!_overlayClassic && _filter.Contains(playerInfo.RoleProto))
// Determine antag/role type name
string? text;
switch (_overlayFormat)
{
var symbol = _overlaySymbols && playerInfo.Antag ? playerInfo.RoleProto.Symbol : string.Empty;
var role = Loc.GetString(playerInfo.RoleProto.Name).ToUpper();
var label = _overlaySymbols
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", role))
: role;
color = playerInfo.RoleProto.Color;
color.A = alpha;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
case AdminOverlayAntagFormat.Roletype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? Loc.GetString(playerInfo.RoleProto.Name).ToUpper()
: string.Empty;
break;
case AdminOverlayAntagFormat.Subtype:
color = playerInfo.RoleProto.Color;
symbol = _filter.Contains(playerInfo.RoleProto) ? symbol : string.Empty;
text = _filter.Contains(playerInfo.RoleProto)
? _roles.GetRoleSubtypeLabel(playerInfo.RoleProto.Name, playerInfo.Subtype).ToUpper()
: string.Empty;
break;
default:
case AdminOverlayAntagFormat.Binary:
color = Color.OrangeRed;
symbol = playerInfo.Antag ? symbol : string.Empty;
text = playerInfo.Antag ? _antagLabelClassic : string.Empty;
break;
}
// Draw antag label
color.A = alpha;
var label = !string.IsNullOrEmpty(symbol)
? Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", text))
: text;
args.ScreenHandle.DrawString(_fontBold, screenCoordinates + currentOffset, label, uiScale, color);
currentOffset += lineoffset;
//Save the coordinates and size of the text block, for stack merge check
drawnOverlays.Add((screenCoordinatesCenter, currentOffset));
}

View File

@@ -0,0 +1,15 @@
namespace Content.Client.Administration;
public enum AdminOverlayAntagFormat
{
Binary,
Roletype,
Subtype
}
public enum AdminOverlayAntagSymbolStyle
{
Off,
Basic,
Specific
}

View File

@@ -1,4 +1,5 @@
using Content.Client.Administration.Managers;
using Content.Shared.Roles;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
@@ -15,6 +16,7 @@ namespace Content.Client.Administration.Systems
[Dependency] private readonly EntityLookupSystem _entityLookup = default!;
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly SharedRoleSystem _roles = default!;
private AdminNameOverlay _adminNameOverlay = default!;
@@ -30,7 +32,8 @@ namespace Content.Client.Administration.Systems
_resourceCache,
_entityLookup,
_userInterfaceManager,
_configurationManager);
_configurationManager,
_roles);
_adminManager.AdminStatusUpdated += OnAdminStatusUpdated;
}
@@ -46,7 +49,8 @@ namespace Content.Client.Administration.Systems
public void AdminOverlayOn()
{
if (_overlayManager.HasOverlay<AdminNameOverlay>()) return;
if (_overlayManager.HasOverlay<AdminNameOverlay>())
return;
_overlayManager.AddOverlay(_adminNameOverlay);
OverlayEnabled?.Invoke();
}

View File

@@ -32,7 +32,7 @@ namespace Content.Client.Administration.Systems
var verb = new VvVerb()
{
Text = Loc.GetString("view-variables"),
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Act = () => _clientConsoleHost.ExecuteCommand($"vv {GetNetEntity(args.Target)}"),
ClientExclusive = true // opening VV window is client-side. Don't ask server to run this verb.
};

View File

@@ -1,59 +0,0 @@
using System.Threading;
using Content.Client.Stylesheets;
using Robust.Client.UserInterface.Controls;
using Timer = Robust.Shared.Timing.Timer;
namespace Content.Client.Administration.UI;
public static class AdminUIHelpers
{
private static void ResetButton(Button button, ConfirmationData data)
{
data.Cancellation.Cancel();
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}
public static bool RemoveConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (confirmations.Remove(button, out var data))
{
ResetButton(button, data);
return true;
}
return false;
}
public static void RemoveAllConfirms(Dictionary<Button, ConfirmationData> confirmations)
{
foreach (var (button, confirmation) in confirmations)
{
ResetButton(button, confirmation);
}
confirmations.Clear();
}
public static bool TryConfirm(Button button, Dictionary<Button, ConfirmationData> confirmations)
{
if (RemoveConfirm(button, confirmations))
return true;
var data = new ConfirmationData(new CancellationTokenSource(), button.Text);
confirmations[button] = data;
Timer.Spawn(TimeSpan.FromSeconds(5), () =>
{
confirmations.Remove(button);
button.ModulateSelfOverride = null;
button.Text = data.OriginalText;
}, data.Cancellation.Token);
button.ModulateSelfOverride = StyleNano.ButtonColorCautionDefault;
button.Text = Loc.GetString("admin-player-actions-confirm");
return false;
}
}
public readonly record struct ConfirmationData(CancellationTokenSource Cancellation, string? OriginalText);

View File

@@ -1,6 +1,7 @@
<Control
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls">
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer StyleClasses="BackgroundDark">
<SplitContainer Orientation="Vertical" ResizeMode="NotResizable">
<SplitContainer Orientation="Horizontal" VerticalExpand="True">
@@ -18,9 +19,9 @@
<Control HorizontalExpand="True" />
<Button Visible="False" Name="Bans" Text="{Loc 'admin-player-actions-bans'}" StyleClasses="OpenRight" />
<Button Visible="False" Name="Notes" Text="{Loc 'admin-player-actions-notes'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Kick" Text="{Loc 'admin-player-actions-kick'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Ban" Text="{Loc 'admin-player-actions-ban'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" StyleClasses="OpenBoth" />
<controls:ConfirmButton Visible="False" Name="Respawn" Text="{Loc 'admin-player-actions-respawn'}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" StyleClasses="OpenBoth" />
<Button Visible="False" Name="Follow" Text="{Loc 'admin-player-actions-follow'}" StyleClasses="OpenLeft" />
</BoxContainer>
</SplitContainer>

View File

@@ -29,7 +29,6 @@ namespace Content.Client.Administration.UI.Bwoink
public AdminAHelpUIHandler AHelpHelper = default!;
private PlayerInfo? _currentPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public BwoinkControl()
{
@@ -178,11 +177,6 @@ namespace Content.Client.Administration.UI.Bwoink
Kick.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(Kick, _confirmations))
{
return;
}
// TODO: Reason field
if (_currentPlayer is not null)
_console.ExecuteCommand($"kick \"{_currentPlayer.Username}\"");
@@ -196,11 +190,6 @@ namespace Content.Client.Administration.UI.Bwoink
Respawn.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(Respawn, _confirmations))
{
return;
}
if (_currentPlayer is not null)
_console.ExecuteCommand($"respawn \"{_currentPlayer.Username}\"");
};

View File

@@ -1,5 +1,6 @@
<Popup xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client">
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<PanelContainer>
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BorderThickness="2" BorderColor="#18181B" BackgroundColor="#25252a"/>
@@ -19,7 +20,7 @@
<BoxContainer Orientation="Horizontal">
<Button Name="EditButton" Text="{Loc admin-notes-edit}"/>
<Control HorizontalExpand="True"/>
<Button Name="DeleteButton" Text="{Loc admin-notes-delete}" HorizontalAlignment="Right"/>
<controls:ConfirmButton Name="DeleteButton" Text="{Loc admin-notes-delete}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" HorizontalAlignment="Right"/>
</BoxContainer>
</BoxContainer>
</PanelContainer>

View File

@@ -19,12 +19,13 @@
<Label Name="SharedConnections"/>
<BoxContainer Align="Center">
<GridContainer Rows="5">
<GridContainer Rows="6">
<Button Name="NotesButton" Text="{Loc player-panel-show-notes}" SetWidth="136" Disabled="True"/>
<Button Name="AhelpButton" Text="{Loc player-panel-help}" Disabled="True"/>
<Button Name="FreezeButton" Text = "{Loc player-panel-freeze}" Disabled="True"/>
<controls:ConfirmButton Name="KickButton" Text="{Loc player-panel-kick}" Disabled="True"/>
<controls:ConfirmButton Name="DeleteButton" Text="{Loc player-panel-delete}" Disabled="True"/>
<Button Name="FollowButton" Text="{Loc player-panel-follow}"/>
<Button Name="ShowBansButton" Text="{Loc player-panel-show-bans}" SetWidth="136" Disabled="True"/>
<Button Name="LogsButton" Text="{Loc player-panel-logs}" Disabled="True"/>
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>

View File

@@ -2,7 +2,6 @@ using Content.Client.Administration.Managers;
using Content.Client.UserInterface.Controls;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Network;
using Robust.Shared.Utility;
@@ -21,6 +20,7 @@ public sealed partial class PlayerPanel : FancyWindow
public event Action<string?>? OnKick;
public event Action<NetUserId?>? OnOpenBanPanel;
public event Action<NetUserId?, bool>? OnWhitelistToggle;
public event Action? OnFollow;
public event Action? OnFreezeAndMuteToggle;
public event Action? OnFreeze;
public event Action? OnLogs;
@@ -36,7 +36,7 @@ public sealed partial class PlayerPanel : FancyWindow
RobustXamlLoader.Load(this);
_adminManager = adminManager;
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(PlayerName.Text ?? "");
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
@@ -47,6 +47,7 @@ public sealed partial class PlayerPanel : FancyWindow
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
SetWhitelisted(!_isWhitelisted);
};
FollowButton.OnPressed += _ => OnFollow?.Invoke();
FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke();

View File

@@ -38,6 +38,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage());
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());
}

View File

@@ -16,6 +16,7 @@
<cc:UICommandButton Command="callshuttle" Text="{Loc admin-player-actions-window-shuttle}" WindowType="{x:Type at:AdminShuttleWindow}"/>
<cc:CommandButton Command="adminlogs" Text="{Loc admin-player-actions-window-admin-logs}"/>
<cc:CommandButton Command="faxui" Text="{Loc admin-player-actions-window-admin-fax}"/>
<cc:CommandButton Command="achatwindow" Text="{Loc admin-player-actions-window-admin-chat}"/>
</GridContainer>
</BoxContainer>
</Control>

View File

@@ -1,6 +1,7 @@
<DefaultWindow
xmlns="https://spacestation14.io"
xmlns:cc="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc admin-player-actions-window-title}" MinSize="425 272">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal">
@@ -10,9 +11,9 @@
</BoxContainer>
<cc:PlayerListControl Name="PlayerList" VerticalExpand="True" />
<BoxContainer Orientation="Horizontal">
<Button Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" Disabled="True"/>
<controls:ConfirmButton Name="SubmitKickButton" Text="{Loc admin-player-actions-kick}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
<Button Name="SubmitAHelpButton" Text="{Loc admin-player-actions-ahelp}" Disabled="True"/>
<Button Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" Disabled="True"/>
<controls:ConfirmButton Name="SubmitRespawnButton" Text="{Loc admin-player-actions-respawn}" ConfirmationText="{Loc 'admin-player-actions-confirm'}" Disabled="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>

View File

@@ -14,7 +14,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
public sealed partial class PlayerActionsWindow : DefaultWindow
{
private PlayerInfo? _selectedPlayer;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public PlayerActionsWindow()
{
@@ -28,9 +27,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
private void OnListOnOnSelectionChanged(PlayerInfo? obj)
{
if (_selectedPlayer != obj)
AdminUIHelpers.RemoveAllConfirms(_confirmations);
_selectedPlayer = obj;
var disableButtons = _selectedPlayer == null;
SubmitKickButton.Disabled = disableButtons;
@@ -43,9 +39,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null)
return;
if (!AdminUIHelpers.TryConfirm(SubmitKickButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"kick \"{_selectedPlayer.Username}\" \"{CommandParsing.Escape(ReasonLine.Text)}\"");
}
@@ -64,9 +57,6 @@ namespace Content.Client.Administration.UI.Tabs.AdminTab
if (_selectedPlayer == null)
return;
if (!AdminUIHelpers.TryConfirm(SubmitRespawnButton, _confirmations))
return;
IoCManager.Resolve<IClientConsoleHost>().ExecuteCommand(
$"respawn \"{_selectedPlayer.Username}\"");
}

View File

@@ -13,7 +13,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
public Action<NetEntity>? OnTeleport;
public Action<NetEntity>? OnDelete;
private readonly Dictionary<Button, ConfirmationData> _confirmations = new();
public ObjectsTabEntry(IClientAdminManager manager, string name, NetEntity nent, StyleBox styleBox)
{
@@ -28,13 +27,6 @@ public sealed partial class ObjectsTabEntry : PanelContainer
DeleteButton.Disabled = !manager.CanCommand("delete");
TeleportButton.OnPressed += _ => OnTeleport?.Invoke(nent);
DeleteButton.OnPressed += _ =>
{
if (!AdminUIHelpers.TryConfirm(DeleteButton, _confirmations))
{
return;
}
OnDelete?.Invoke(nent);
};
DeleteButton.OnPressed += _ => OnDelete?.Invoke(nent);
}
}

View File

@@ -32,6 +32,10 @@ public sealed partial class PlayerTab : Control
private bool _ascending = true;
private bool _showDisconnected;
private AdminPlayerTabColorOption _playerTabColorSetting;
private AdminPlayerTabRoleTypeOption _playerTabRoleSetting;
private AdminPlayerTabSymbolOption _playerTabSymbolSetting;
public event Action<GUIBoundKeyEventArgs, ListData>? OnEntryKeyBindDown;
public PlayerTab()
@@ -44,9 +48,10 @@ public sealed partial class PlayerTab : Control
_adminSystem.OverlayEnabled += OverlayEnabled;
_adminSystem.OverlayDisabled += OverlayDisabled;
_config.OnValueChanged(CCVars.AdminPlayerlistSeparateSymbols, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerlistRoleTypeColor, PlayerListSettingsChanged);
_config.OnValueChanged(CCVars.AdminPlayerTabRoleSetting, RoleSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabColorSetting, ColorSettingChanged, true);
_config.OnValueChanged(CCVars.AdminPlayerTabSymbolSetting, SymbolSettingChanged, true);
OverlayButton.OnPressed += OverlayButtonPressed;
ShowDisconnectedButton.OnPressed += ShowDisconnectedPressed;
@@ -113,8 +118,27 @@ public sealed partial class PlayerTab : Control
#region ListContainer
private void PlayerListSettingsChanged(bool _)
private void RoleSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabRoleTypeOption>(s, out var format))
format = AdminPlayerTabRoleTypeOption.Subtype;
_playerTabRoleSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void ColorSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabColorOption>(s, out var format))
format = AdminPlayerTabColorOption.Both;
_playerTabColorSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
private void SymbolSettingChanged(string s)
{
if (!Enum.TryParse<AdminPlayerTabSymbolOption>(s, out var format))
format = AdminPlayerTabSymbolOption.Specific;
_playerTabSymbolSetting = format;
RefreshPlayerList(_adminSystem.PlayerList);
}
@@ -140,7 +164,12 @@ public sealed partial class PlayerTab : Control
if (data is not PlayerListData { Info: var player})
return;
var entry = new PlayerTabEntry(player, new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor));
var entry = new PlayerTabEntry(
player,
new StyleBoxFlat(button.Index % 2 == 0 ? _altColor : _defaultColor),
_playerTabColorSetting,
_playerTabRoleSetting,
_playerTabSymbolSetting);
button.AddChild(entry);
button.ToolTip = $"{player.Username}, {player.CharacterName}, {player.IdentityName}, {player.StartingJob}";
}

View File

@@ -1,42 +1,99 @@
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Content.Shared.Roles;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
[GenerateTypedNameReferences]
public sealed partial class PlayerTabEntry : PanelContainer
{
public NetEntity? PlayerEntity;
[Dependency] private readonly IEntityManager _entMan = default!;
public PlayerTabEntry(PlayerInfo player, StyleBoxFlat styleBoxFlat)
public PlayerTabEntry(
PlayerInfo player,
StyleBoxFlat styleBoxFlat,
AdminPlayerTabColorOption colorOption,
AdminPlayerTabRoleTypeOption roleSetting,
AdminPlayerTabSymbolOption symbolSetting)
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
var config = IoCManager.Resolve<IConfigurationManager>();
var roles = _entMan.System<SharedRoleSystem>();
UsernameLabel.Text = player.Username;
if (!player.Connected)
UsernameLabel.StyleClasses.Add("Disabled");
JobLabel.Text = player.StartingJob;
var separateAntagSymbols = config.GetCVar(CCVars.AdminPlayerlistSeparateSymbols);
var genericAntagSymbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
var roleSymbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
var symbol = separateAntagSymbols ? roleSymbol : genericAntagSymbol;
var colorAntags = false;
var colorRoles = false;
switch (colorOption)
{
case AdminPlayerTabColorOption.Off:
break;
case AdminPlayerTabColorOption.Character:
colorAntags = true;
break;
case AdminPlayerTabColorOption.Roletype:
colorRoles = true;
break;
default:
case AdminPlayerTabColorOption.Both:
colorAntags = true;
colorRoles = true;
break;
}
var symbol = string.Empty;
switch (symbolSetting)
{
case AdminPlayerTabSymbolOption.Off:
break;
case AdminPlayerTabSymbolOption.Basic:
symbol = player.Antag ? Loc.GetString("player-tab-antag-prefix") : string.Empty;
break;
default:
case AdminPlayerTabSymbolOption.Specific:
symbol = player.Antag ? player.RoleProto.Symbol : string.Empty;
break;
}
CharacterLabel.Text = Loc.GetString("player-tab-character-name-antag-symbol", ("symbol", symbol), ("name", player.CharacterName));
if (player.Antag && config.GetCVar(CCVars.AdminPlayerlistHighlightedCharacterColor))
if (player.Antag && colorAntags)
CharacterLabel.FontColorOverride = player.RoleProto.Color;
if (player.IdentityName != player.CharacterName)
CharacterLabel.Text += $" [{player.IdentityName}]";
RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
if (config.GetCVar(CCVars.AdminPlayerlistRoleTypeColor))
var roletype = RoleTypeLabel.Text = Loc.GetString(player.RoleProto.Name);
var subtype = roles.GetRoleSubtypeLabel(player.RoleProto.Name, player.Subtype);
switch (roleSetting)
{
case AdminPlayerTabRoleTypeOption.RoleTypeSubtype:
RoleTypeLabel.Text = roletype != subtype
? roletype + " - " +subtype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.SubtypeRoleType:
RoleTypeLabel.Text = roletype != subtype
? subtype + " - " + roletype
: roletype;
break;
case AdminPlayerTabRoleTypeOption.RoleType:
RoleTypeLabel.Text = roletype;
break;
default:
case AdminPlayerTabRoleTypeOption.Subtype:
RoleTypeLabel.Text = subtype;
break;
}
if (colorRoles)
RoleTypeLabel.FontColorOverride = player.RoleProto.Color;
BackgroundColorPanel.PanelOverride = styleBoxFlat;
OverallPlaytimeLabel.Text = player.PlaytimeString;
PlayerEntity = player.NetEntity;
}
}

View File

@@ -0,0 +1,24 @@
namespace Content.Client.Administration.UI.Tabs.PlayerTab;
public enum AdminPlayerTabColorOption
{
Off,
Character,
Roletype,
Both
}
public enum AdminPlayerTabRoleTypeOption
{
RoleType,
Subtype,
RoleTypeSubtype,
SubtypeRoleType
}
public enum AdminPlayerTabSymbolOption
{
Off,
Basic,
Specific
}

View File

@@ -0,0 +1,29 @@
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Atmos.Piping.Binary.Systems;
public sealed class GasVolumePumpSystem : SharedGasVolumePumpSystem
{
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<GasVolumePumpComponent, AfterAutoHandleStateEvent>(OnPumpState);
}
protected override void UpdateUi(Entity<GasVolumePumpComponent> entity)
{
if (_ui.TryGetOpenUi(entity.Owner, GasVolumePumpUiKey.Key, out var bui))
{
bui.Update();
}
}
private void OnPumpState(Entity<GasVolumePumpComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
}

View File

@@ -1,8 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
namespace Content.Client.Atmos.UI
@@ -14,7 +13,7 @@ namespace Content.Client.Atmos.UI
public sealed class GasVolumePumpBoundUserInterface : BoundUserInterface
{
[ViewVariables]
private const float MaxTransferRate = Atmospherics.MaxTransferRate;
private float _maxTransferRate;
[ViewVariables]
private GasVolumePumpWindow? _window;
@@ -29,38 +28,41 @@ namespace Content.Client.Atmos.UI
_window = this.CreateWindow<GasVolumePumpWindow>();
if (EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
{
_maxTransferRate = pump.MaxTransferRate;
}
_window.ToggleStatusButtonPressed += OnToggleStatusButtonPressed;
_window.PumpTransferRateChanged += OnPumpTransferRatePressed;
Update();
}
private void OnToggleStatusButtonPressed()
{
if (_window is null) return;
SendMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
SendPredictedMessage(new GasVolumePumpToggleStatusMessage(_window.PumpStatus));
}
private void OnPumpTransferRatePressed(string value)
{
var rate = UserInputParser.TryFloat(value, out var parsed) ? parsed : 0f;
if (rate > MaxTransferRate)
rate = MaxTransferRate;
rate = Math.Clamp(rate, 0f, _maxTransferRate);
SendMessage(new GasVolumePumpChangeTransferRateMessage(rate));
SendPredictedMessage(new GasVolumePumpChangeTransferRateMessage(rate));
}
/// <summary>
/// Update the UI state based on server-sent info
/// </summary>
/// <param name="state"></param>
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
if (_window == null || state is not GasVolumePumpBoundUserInterfaceState cast)
base.Update();
if (_window is null || !EntMan.TryGetComponent(Owner, out GasVolumePumpComponent? pump))
return;
_window.Title = cast.PumpLabel;
_window.SetPumpStatus(cast.Enabled);
_window.SetTransferRate(cast.TransferRate);
_window.Title = Identity.Name(Owner, EntMan);
_window.SetPumpStatus(pump.Enabled);
_window.SetTransferRate(pump.TransferRate);
}
}
}

View File

@@ -1,5 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
MinSize="200 120" Title="Volume Pump">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5" SeparationOverride="10">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
@@ -19,4 +20,4 @@
<Button Name="SetTransferRateButton" Text="{Loc comp-gas-pump-ui-pump-set-rate}" HorizontalAlignment="Right" Disabled="True"/>
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using Content.Client.Atmos.EntitySystems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Prototypes;
using Robust.Client.AutoGenerated;
@@ -16,7 +17,7 @@ namespace Content.Client.Atmos.UI
/// Client-side UI used to control a gas volume pump.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class GasVolumePumpWindow : DefaultWindow
public sealed partial class GasVolumePumpWindow : FancyWindow
{
public bool PumpStatus = true;

View File

@@ -1,6 +1,7 @@
using Content.Shared.Cargo;
using Content.Client.Cargo.UI;
using Content.Shared.Cargo.BUI;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Events;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.IdentityManagement;
@@ -14,6 +15,8 @@ namespace Content.Client.Cargo.BUI
{
public sealed class CargoOrderConsoleBoundUserInterface : BoundUserInterface
{
private readonly SharedCargoSystem _cargoSystem;
[ViewVariables]
private CargoConsoleMenu? _menu;
@@ -43,6 +46,7 @@ namespace Content.Client.Cargo.BUI
public CargoOrderConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
_cargoSystem = EntMan.System<SharedCargoSystem>();
}
protected override void Open()
@@ -57,7 +61,7 @@ namespace Content.Client.Cargo.BUI
string orderRequester;
if (EntMan.TryGetComponent<MetaDataComponent>(localPlayer, out var metadata))
if (EntMan.EntityExists(localPlayer))
orderRequester = Identity.Name(localPlayer.Value, EntMan);
else
orderRequester = string.Empty;
@@ -96,41 +100,54 @@ namespace Content.Client.Cargo.BUI
}
};
_menu.OnAccountAction += (account, amount) =>
{
SendMessage(new CargoConsoleWithdrawFundsMessage(account, amount));
};
_menu.OnToggleUnboundedLimit += _ =>
{
SendMessage(new CargoConsoleToggleLimitMessage());
};
_menu.OpenCentered();
}
private void Populate(List<CargoOrderData> orders)
{
if (_menu == null) return;
if (_menu == null)
return;
_menu.PopulateProducts();
_menu.PopulateCategories();
_menu.PopulateOrders(orders);
_menu.PopulateAccountActions();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not CargoConsoleInterfaceState cState)
if (state is not CargoConsoleInterfaceState cState || !EntMan.TryGetComponent<CargoOrderConsoleComponent>(Owner, out var orderConsole))
return;
var station = EntMan.GetEntity(cState.Station);
OrderCapacity = cState.Capacity;
OrderCount = cState.Count;
BankBalance = cState.Balance;
BankBalance = _cargoSystem.GetBalanceFromAccount(station, orderConsole.Account);
AccountName = cState.Name;
_menu?.UpdateStation(station);
Populate(cState.Orders);
_menu?.UpdateCargoCapacity(OrderCount, OrderCapacity);
_menu?.UpdateBankData(AccountName, BankBalance);
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing) return;
if (!disposing)
return;
_menu?.Dispose();
_orderMenu?.Dispose();
@@ -170,8 +187,6 @@ namespace Content.Client.Cargo.BUI
return;
SendMessage(new CargoConsoleApproveOrderMessage(row.Order.OrderId));
// Most of the UI isn't predicted anyway so.
// _menu?.UpdateCargoCapacity(OrderCount + row.Order.Amount, OrderCapacity);
}
}
}

View File

@@ -0,0 +1,35 @@
using Content.Client.Cargo.UI;
using Content.Shared.Cargo.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Cargo.BUI;
[UsedImplicitly]
public sealed class FundingAllocationConsoleBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private FundingAllocationMenu? _menu;
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<FundingAllocationMenu>();
_menu.OnSavePressed += (dicts, primary, lockbox) =>
{
SendMessage(new SetFundingAllocationBuiMessage(dicts, primary, lockbox));
};
}
protected override void UpdateState(BoundUserInterfaceState message)
{
base.UpdateState(message);
if (message is not FundingAllocationConsoleBuiState state)
return;
_menu?.Update(state);
}
}

View File

@@ -75,10 +75,9 @@ public sealed partial class CargoSystem
switch (state)
{
case CargoTelepadState.Teleporting:
if (_player.HasRunningAnimation(uid, TelepadBeamKey))
return;
_player.Stop(uid, player, TelepadIdleKey);
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
_player.Stop((uid, player), TelepadIdleKey);
if (!_player.HasRunningAnimation(uid, TelepadBeamKey))
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
break;
case CargoTelepadState.Unpowered:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);

View File

@@ -3,66 +3,83 @@
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
SetSize="600 600"
MinSize="600 600">
<BoxContainer Orientation="Vertical" Margin="5 0 5 0">
<BoxContainer Orientation="Vertical" Margin="15 5 15 10">
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-account-name-label'}"
StyleClasses="LabelKeyText" />
<Label Name="AccountNameLabel"
<RichTextLabel Name="AccountNameLabel"
Text="{Loc 'cargo-console-menu-account-name-none-text'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-points-label'}"
StyleClasses="LabelKeyText" />
<Label Name="PointsLabel"
<RichTextLabel Name="PointsLabel"
Text="$0" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'cargo-console-menu-order-capacity-label'}"
StyleClasses="LabelKeyText" />
<Label Name="ShuttleCapacityLabel"
Text="0/20" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="6">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="6">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
<BoxContainer Name="Requests"
Orientation="Vertical"
VerticalExpand="True">
<!-- Requests are added here by code -->
</BoxContainer>
<Label Text="{Loc 'cargo-console-menu-orders-label'}" />
<BoxContainer Name="Orders"
Orientation="Vertical"
StyleClasses="transparentItemList"
VerticalExpand="True">
<!-- Orders are added here by code -->
</BoxContainer>
<Control MinHeight="10"/>
<TabContainer Name="TabContainer" VerticalExpand="True">
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Orientation="Horizontal">
<OptionButton Name="Categories"
Prefix="{Loc 'cargo-console-menu-categories-label'}"
HorizontalExpand="True" />
<LineEdit Name="SearchBar"
PlaceHolder="{Loc 'cargo-console-menu-search-bar-placeholder'}"
HorizontalExpand="True" />
</BoxContainer>
</ScrollContainer>
</PanelContainer>
<TextureButton VerticalExpand="True" />
<Control MinHeight="5"/>
<ScrollContainer HorizontalExpand="True"
VerticalExpand="True"
SizeFlagsStretchRatio="2">
<BoxContainer Name="Products"
Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
<!-- Products get added here by code -->
</BoxContainer>
</ScrollContainer>
<Control MinHeight="5"/>
<PanelContainer VerticalExpand="True"
SizeFlagsStretchRatio="1">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#000000" />
</PanelContainer.PanelOverride>
<ScrollContainer VerticalExpand="True">
<BoxContainer Orientation="Vertical" Margin="5">
<Label Text="{Loc 'cargo-console-menu-requests-label'}" />
<BoxContainer Name="Requests"
Orientation="Vertical"
VerticalExpand="True">
<!-- Requests are added here by code -->
</BoxContainer>
</BoxContainer>
</ScrollContainer>
</PanelContainer>
</BoxContainer>
<!-- Funds tab -->
<BoxContainer Orientation="Vertical" Margin="15">
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="TransferLimitLabel" Margin="0 0 15 0"/>
<RichTextLabel Name="UnlimitedNotifier" Text="{Loc 'cargo-console-menu-account-action-transfer-limit-unlimited-notifier'}"/>
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Text="{Loc 'cargo-console-menu-account-action-select'}" Margin="0 0 10 0"/>
<OptionButton Name="ActionOptions"/>
</BoxContainer>
<Control MinHeight="5"/>
<BoxContainer Orientation="Horizontal">
<RichTextLabel Name="AmountText" Text="{ Loc 'cargo-console-menu-account-action-amount'}"/>
<SpinBox Name="TransferSpinBox" MinWidth="100" Value="10"/>
</BoxContainer>
<Control MinHeight="15"/>
<BoxContainer HorizontalAlignment="Center">
<Button Name="AccountActionButton" Text="{ Loc 'cargo-console-menu-account-action-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
<Control VerticalExpand="True"/>
<BoxContainer VerticalAlignment="Bottom" HorizontalAlignment="Center">
<Button Name="AccountLimitToggleButton" Text="{ Loc 'cargo-console-menu-toggle-account-lock-button'}" MinHeight="45" MinWidth="120"/>
</BoxContainer>
</BoxContainer>
</TabContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -1,4 +1,5 @@
using System.Linq;
using Content.Client.Cargo.Systems;
using Content.Client.UserInterface.Controls;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
@@ -8,6 +9,7 @@ using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using static Robust.Client.UserInterface.Controls.BaseButton;
namespace Content.Client.Cargo.UI
@@ -15,30 +17,83 @@ namespace Content.Client.Cargo.UI
[GenerateTypedNameReferences]
public sealed partial class CargoConsoleMenu : FancyWindow
{
private IEntityManager _entityManager;
private IPrototypeManager _protoManager;
private SpriteSystem _spriteSystem;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly IEntityManager _entityManager;
private readonly IPrototypeManager _protoManager;
private readonly CargoSystem _cargoSystem;
private readonly SpriteSystem _spriteSystem;
private EntityUid _owner;
private EntityUid? _station;
private readonly EntityQuery<CargoOrderConsoleComponent> _orderConsoleQuery;
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
public event Action<ButtonEventArgs>? OnItemSelected;
public event Action<ButtonEventArgs>? OnOrderApproved;
public event Action<ButtonEventArgs>? OnOrderCanceled;
public event Action<ProtoId<CargoAccountPrototype>?, int>? OnAccountAction;
public event Action<ButtonEventArgs>? OnToggleUnboundedLimit;
private readonly List<string> _categoryStrings = new();
private string? _category;
public CargoConsoleMenu(EntityUid owner, IEntityManager entMan, IPrototypeManager protoManager, SpriteSystem spriteSystem)
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_entityManager = entMan;
_protoManager = protoManager;
_cargoSystem = entMan.System<CargoSystem>();
_spriteSystem = spriteSystem;
_owner = owner;
Title = Loc.GetString("cargo-console-menu-title");
_orderConsoleQuery = _entityManager.GetEntityQuery<CargoOrderConsoleComponent>();
_bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
Title = entMan.GetComponent<MetaDataComponent>(owner).EntityName;
SearchBar.OnTextChanged += OnSearchBarTextChanged;
Categories.OnItemSelected += OnCategoryItemSelected;
if (entMan.TryGetComponent<CargoOrderConsoleComponent>(owner, out var orderConsole))
{
var accountProto = _protoManager.Index(orderConsole.Account);
AccountNameLabel.Text = Loc.GetString("cargo-console-menu-account-name-format",
("color", accountProto.Color),
("name", Loc.GetString(accountProto.Name)),
("code", Loc.GetString(accountProto.Code)));
}
TabContainer.SetTabTitle(0, Loc.GetString("cargo-console-menu-tab-title-orders"));
TabContainer.SetTabTitle(1, Loc.GetString("cargo-console-menu-tab-title-funds"));
ActionOptions.OnItemSelected += idx =>
{
ActionOptions.SelectId(idx.Id);
};
TransferSpinBox.IsValid = val =>
{
if (!_entityManager.TryGetComponent<CargoOrderConsoleComponent>(owner, out var console) ||
!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return true;
return val >= 0 && val <= (int) (console.TransferLimit * bank.Accounts[console.Account]);
};
AccountActionButton.OnPressed += _ =>
{
var account = (ProtoId<CargoAccountPrototype>?) ActionOptions.SelectedMetadata;
OnAccountAction?.Invoke(account, TransferSpinBox.Value);
};
AccountLimitToggleButton.OnPressed += a =>
{
OnToggleUnboundedLimit?.Invoke(a);
};
}
private void OnCategoryItemSelected(OptionButton.ItemSelectedEventArgs args)
@@ -144,11 +199,13 @@ namespace Content.Client.Cargo.UI
/// </summary>
public void PopulateOrders(IEnumerable<CargoOrderData> orders)
{
Orders.DisposeAllChildren();
Requests.DisposeAllChildren();
foreach (var order in orders)
{
if (order.Approved)
continue;
var product = _protoManager.Index<EntityPrototype>(order.ProductId);
var productName = product.Name;
@@ -164,35 +221,67 @@ namespace Content.Client.Cargo.UI
("orderAmount", order.OrderQuantity),
("orderRequester", order.Requester))
},
Description = {Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))}
Description =
{
Text = Loc.GetString("cargo-console-menu-order-reason-description",
("reason", order.Reason))
}
};
row.Cancel.OnPressed += (args) => { OnOrderCanceled?.Invoke(args); };
if (order.Approved)
{
row.Approve.Visible = false;
row.Cancel.Visible = false;
Orders.AddChild(row);
}
else
{
// TODO: Disable based on access.
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
Requests.AddChild(row);
}
// TODO: Disable based on access.
row.Approve.OnPressed += (args) => { OnOrderApproved?.Invoke(args); };
Requests.AddChild(row);
}
}
public void UpdateCargoCapacity(int count, int capacity)
public void PopulateAccountActions()
{
// TODO: Rename + Loc.
ShuttleCapacityLabel.Text = $"{count}/{capacity}";
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank) ||
!_entityManager.TryGetComponent<CargoOrderConsoleComponent>(_owner, out var console))
return;
var i = 0;
ActionOptions.Clear();
ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-withdraw"), i);
i++;
foreach (var account in bank.Accounts.Keys)
{
if (account == console.Account)
continue;
var accountProto = _protoManager.Index(account);
ActionOptions.AddItem(Loc.GetString("cargo-console-menu-account-action-option-transfer",
("code", Loc.GetString(accountProto.Code))),
i);
ActionOptions.SetItemMetadata(i, account);
i++;
}
}
public void UpdateBankData(string name, int points)
public void UpdateStation(EntityUid station)
{
AccountNameLabel.Text = name;
PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", points.ToString()));
_station = station;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_bankQuery.TryComp(_station, out var bankAccount) ||
!_orderConsoleQuery.TryComp(_owner, out var orderConsole))
{
return;
}
var balance = _cargoSystem.GetBalanceFromAccount((_station.Value, bankAccount), orderConsole.Account);
PointsLabel.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance));
TransferLimitLabel.Text = Loc.GetString("cargo-console-menu-account-action-transfer-limit",
("limit", (int) (balance * orderConsole.TransferLimit)));
UnlimitedNotifier.Visible = orderConsole.TransferUnbounded;
AccountActionButton.Disabled = TransferSpinBox.Value <= 0 ||
TransferSpinBox.Value > bankAccount.Accounts[orderConsole.Account] * orderConsole.TransferLimit ||
_timing.CurTime < orderConsole.NextAccountActionTime;
}
}
}

View File

@@ -1,11 +1,13 @@
<PanelContainer xmlns="https://spacestation14.io"
HorizontalExpand="True">
HorizontalExpand="True"
Margin="0 1">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<TextureRect Name="Icon"
Access="Public"
MinSize="32 32"
RectClipContent="True" />
<Control MinWidth="5"/>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True">
@@ -23,10 +25,10 @@
<Button Name="Approve"
Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-approve-button'}"
StyleClasses="LabelSubText" />
StyleClasses="OpenRight" />
<Button Name="Cancel"
Access="Public"
Text="{Loc 'cargo-console-menu-cargo-order-row-cancel-button'}"
StyleClasses="LabelSubText" />
StyleClasses="OpenLeft" />
</BoxContainer>
</PanelContainer>

View File

@@ -4,7 +4,8 @@
ToolTip=""
Access="Public"
HorizontalExpand="True"
VerticalExpand="True" />
VerticalExpand="True"
StyleClasses="OpenBoth"/>
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True">
<TextureRect Name="Icon"
@@ -18,7 +19,8 @@
<Label Name="PointCost"
Access="Public"
MinSize="52 32"
Align="Right" />
Align="Right"
Margin="0 0 5 0"/>
</PanelContainer>
</BoxContainer>
</PanelContainer>

View File

@@ -0,0 +1,32 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
Title="{Loc 'cargo-funding-alloc-console-menu-title'}">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
Margin="10 5 10 10">
<controls:TableContainer Columns="2" HorizontalExpand="True" VerticalExpand="True">
<RichTextLabel Name="PrimaryCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-primary-cut'}"/>
<SpinBox Name="PrimaryCut"/>
<RichTextLabel Name="LockboxCutLabel" Text="{Loc 'cargo-funding-alloc-console-label-lockbox-cut'}"/>
<SpinBox Name="LockboxCut"/>
</controls:TableContainer>
<Label Name="HelpLabel" HorizontalAlignment="Center" StyleClasses="LabelSubText" Margin="0 10"/>
<PanelContainer VerticalExpand="True" HorizontalExpand="True" VerticalAlignment="Top" MaxHeight="250">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E"/>
</PanelContainer.PanelOverride>
<controls:TableContainer Name="EntriesContainer" Columns="4" HorizontalExpand="True" VerticalExpand="True" Margin="5 0">
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-account'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-code'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-balance'}" HorizontalAlignment="Center"/>
<RichTextLabel Text="{Loc 'cargo-funding-alloc-console-label-cut'}" HorizontalAlignment="Center"/>
</controls:TableContainer>
</PanelContainer>
<BoxContainer Orientation="Horizontal" HorizontalExpand="True" Margin="5 0">
<Button Name="SaveButton" Text="{Loc 'cargo-funding-alloc-console-button-save'}" Disabled="True"/>
<RichTextLabel Name="SaveAlertLabel" HorizontalExpand="True" HorizontalAlignment="Right" Visible="False"/>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,229 @@
using System.Collections.Generic;
using System.Linq;
using Content.Client.Message;
using Content.Client.UserInterface.Controls;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Cargo.UI;
[GenerateTypedNameReferences]
public sealed partial class FundingAllocationMenu : FancyWindow
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly EntityQuery<StationBankAccountComponent> _bankQuery;
public event Action<Dictionary<ProtoId<CargoAccountPrototype>, int>, double, double>? OnSavePressed;
private EntityUid? _station;
private bool _allowPrimaryAccountAllocation;
private bool _allowPrimaryCutAdjustment;
private bool _lockboxCutEnabled;
private double _primaryCut;
private double _lockboxCut;
private readonly HashSet<Control> _addedControls = new();
private readonly List<SpinBox> _spinBoxes = new();
private readonly Dictionary<ProtoId<CargoAccountPrototype>, RichTextLabel> _balanceLabels = new();
public FundingAllocationMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_bankQuery = _entityManager.GetEntityQuery<StationBankAccountComponent>();
PrimaryCut.ValueChanged += args =>
{
_primaryCut = (double)args.Value / 100.0;
UpdateButtonDisabled();
};
LockboxCut.ValueChanged += args =>
{
_lockboxCut = 1.0 - (double)args.Value / 100.0;
UpdateButtonDisabled();
};
SaveButton.OnPressed += _ =>
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key).Select(p => p.Key).ToList();
var dicts = new Dictionary<ProtoId<CargoAccountPrototype>, int>();
for (var i = 0; i< accounts.Count; i++)
{
dicts.Add(accounts[i], _spinBoxes[i].Value);
}
OnSavePressed?.Invoke(dicts, _primaryCut, _lockboxCut);
SaveButton.Disabled = true;
};
_cfg.OnValueChanged(CCVars.AllowPrimaryAccountAllocation, enabled => { _allowPrimaryAccountAllocation = enabled; }, true);
_cfg.OnValueChanged(CCVars.AllowPrimaryCutAdjustment, enabled => { _allowPrimaryCutAdjustment = enabled; }, true);
_cfg.OnValueChanged(CCVars.LockboxCutEnabled, enabled => { _lockboxCutEnabled = enabled; }, true);
BuildEntries();
}
private IEnumerable<KeyValuePair<ProtoId<CargoAccountPrototype>, int>> EditableAccounts(StationBankAccountComponent bank)
{
foreach (var kvp in bank.Accounts)
{
if (_allowPrimaryAccountAllocation || kvp.Key != bank.PrimaryAccount)
{
yield return kvp;
}
}
}
private void BuildEntries()
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
if (_allowPrimaryCutAdjustment)
{
HelpLabel.Text = Loc.GetString("cargo-funding-alloc-console-label-help-adjustible");
}
else
{
HelpLabel.Text = Loc.GetString("cargo-funding-alloc-console-label-help-non-adjustible",
("percent", (int) (bank.PrimaryCut * 100)));
}
foreach (var ctrl in _addedControls)
{
ctrl.Orphan();
}
_addedControls.Clear();
_spinBoxes.Clear();
_balanceLabels.Clear();
_primaryCut = bank.PrimaryCut;
_lockboxCut = bank.LockboxCut;
LockboxCut.OverrideValue(100 - (int)(_lockboxCut * 100));
PrimaryCut.OverrideValue((int)(_primaryCut * 100));
LockboxCut.IsValid = val => val is >= 0 and <= 100;
PrimaryCut.IsValid = val => val is >= 0 and <= 100;
LockboxCut.Visible = _lockboxCutEnabled;
LockboxCutLabel.Visible = _lockboxCutEnabled;
PrimaryCut.Visible = _allowPrimaryCutAdjustment;
PrimaryCutLabel.Visible = _allowPrimaryCutAdjustment;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key);
foreach (var (account, balance) in accounts)
{
var accountProto = _prototypeManager.Index(account);
var accountNameLabel = new RichTextLabel
{
Modulate = accountProto.Color,
Margin = new Thickness(0, 0, 10, 0)
};
accountNameLabel.SetMarkup($"[bold]{Loc.GetString(accountProto.Name)}[/bold]");
EntriesContainer.AddChild(accountNameLabel);
var codeLabel = new RichTextLabel
{
Text = $"[font=\"Monospace\"]{Loc.GetString(accountProto.Code)}[/font]",
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(5, 0),
};
EntriesContainer.AddChild(codeLabel);
var balanceLabel = new RichTextLabel
{
Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", balance)),
HorizontalExpand = true,
HorizontalAlignment = HAlignment.Center,
Margin = new Thickness(5, 0),
};
EntriesContainer.AddChild(balanceLabel);
var box = new SpinBox
{
HorizontalAlignment = HAlignment.Center,
HorizontalExpand = true,
Value = (int) (bank.RevenueDistribution[account] * 100),
IsValid = val => val is >= 0 and <= 100,
};
box.ValueChanged += _ => UpdateButtonDisabled();
EntriesContainer.AddChild(box);
_spinBoxes.Add(box);
_balanceLabels.Add(account, balanceLabel);
_addedControls.Add(accountNameLabel);
_addedControls.Add(codeLabel);
_addedControls.Add(balanceLabel);
_addedControls.Add(box);
}
}
private void UpdateButtonDisabled()
{
if (!_entityManager.TryGetComponent<StationBankAccountComponent>(_station, out var bank))
return;
var sum = _spinBoxes.Sum(s => s.Value);
var incorrectSum = sum != 100;
var differs = false;
var accounts = EditableAccounts(bank).OrderBy(p => p.Key).Select(p => p.Key).ToList();
for (var i = 0; i < accounts.Count; i++)
{
var percent = _spinBoxes[i].Value;
if (percent != (int) Math.Round(bank.RevenueDistribution[accounts[i]] * 100))
{
differs = true;
break;
}
}
differs = differs || _primaryCut != bank.PrimaryCut || _lockboxCut != bank.LockboxCut;
SaveButton.Disabled = !differs || incorrectSum;
var diff = sum - 100;
SaveAlertLabel.Visible = incorrectSum;
SaveAlertLabel.SetMarkup(Loc.GetString("cargo-funding-alloc-console-label-save-fail",
("pos", Math.Sign(diff)),
("val", Math.Abs(diff))));
}
public void Update(FundingAllocationConsoleBuiState state)
{
_station = _entityManager.GetEntity(state.Station);
BuildEntries();
UpdateButtonDisabled();
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (!_bankQuery.TryComp(_station, out var bank))
return;
foreach (var (account, label) in _balanceLabels)
{
label.Text = Loc.GetString("cargo-console-menu-points-amount", ("amount", bank.Accounts[account]));
}
}
}

View File

@@ -90,10 +90,12 @@ public sealed partial class NanoTaskItemPopup : DefaultWindow
{
if (item is NanoTaskItem task)
{
var button = task.Priority switch {
var button = task.Priority switch
{
NanoTaskPriority.High => HighButton,
NanoTaskPriority.Medium => MediumButton,
NanoTaskPriority.Low => LowButton,
_ => throw new ArgumentException("Invalid priority"),
};
button.Pressed = true;
DescriptionInput.Text = task.Description;

View File

@@ -38,10 +38,12 @@ public sealed partial class NanoTaskUiFragment : BoxContainer
foreach (var task in tasks)
{
var container = task.Data.Priority switch {
var container = task.Data.Priority switch
{
NanoTaskPriority.High => HighContainer,
NanoTaskPriority.Medium => MediumContainer,
NanoTaskPriority.Low => LowContainer,
_ => throw new ArgumentException("Invalid priority"),
};
var control = new NanoTaskItemControl(task);
container.AddChild(control);

View File

@@ -0,0 +1,52 @@
using Content.Client.Actions;
using Content.Shared.Actions;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
namespace Content.Client.Charges;
public sealed class ChargesSystem : SharedChargesSystem
{
[Dependency] private readonly ActionsSystem _actions = default!;
private Dictionary<EntityUid, int> _lastCharges = new();
private Dictionary<EntityUid, int> _tempLastCharges = new();
public override void Update(float frameTime)
{
// Technically this should probably be in frameupdate but no one will ever notice a tick of delay on this.
base.Update(frameTime);
if (!_timing.IsFirstTimePredicted)
return;
// Update recharging actions. Server doesn't actually care about this and it's a waste of performance, actions are immediate.
var query = AllEntityQuery<AutoRechargeComponent, LimitedChargesComponent>();
while (query.MoveNext(out var uid, out var recharge, out var charges))
{
BaseActionComponent? actionComp = null;
if (!_actions.ResolveActionData(uid, ref actionComp, logError: false))
continue;
var current = GetCurrentCharges((uid, charges, recharge));
if (!_lastCharges.TryGetValue(uid, out var last) || current != last)
{
_actions.UpdateAction(uid, actionComp);
}
_tempLastCharges[uid] = current;
}
_lastCharges.Clear();
foreach (var (uid, value) in _tempLastCharges)
{
_lastCharges[uid] = value;
}
_tempLastCharges.Clear();
}
}

View File

@@ -1,5 +0,0 @@
using Content.Shared.Charges.Systems;
namespace Content.Client.Charges.Systems;
public sealed class ChargesSystem : SharedChargesSystem { }

View File

@@ -341,8 +341,11 @@ public sealed class ClientClothingSystem : ClothingSystem
if (layerData.State is not null && inventory.SpeciesId is not null && layerData.State.EndsWith(inventory.SpeciesId))
continue;
if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, revealedLayers))
if (_displacement.TryAddDisplacement(displacementData, sprite, index, key, out var displacementKey))
{
revealedLayers.Add(displacementKey);
index++;
}
}
}

View File

@@ -0,0 +1,25 @@
using Content.Client.Configurable.UI;
using Content.Shared.Configurable;
namespace Content.Client.Configurable;
public sealed class ConfigurationSystem : SharedConfigurationSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ConfigurationComponent, AfterAutoHandleStateEvent>(OnConfigurationState);
}
private void OnConfigurationState(Entity<ConfigurationComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (_uiSystem.TryGetOpenUi<ConfigurationBoundUserInterface>(ent.Owner,
ConfigurationComponent.ConfigurationUiKey.Key,
out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,6 +1,8 @@
using System.Text.RegularExpressions;
using Robust.Client.GameObjects;
using System.Numerics;
using System.Text.RegularExpressions;
using Content.Shared.Configurable;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Configurable.ConfigurationComponent;
namespace Content.Client.Configurable.UI
@@ -19,16 +21,53 @@ namespace Content.Client.Configurable.UI
base.Open();
_menu = this.CreateWindow<ConfigurationMenu>();
_menu.OnConfiguration += SendConfiguration;
if (EntMan.TryGetComponent(Owner, out ConfigurationComponent? component))
Refresh((Owner, component));
}
protected override void UpdateState(BoundUserInterfaceState state)
public void Refresh(Entity<ConfigurationComponent> entity)
{
base.UpdateState(state);
if (state is not ConfigurationBoundUserInterfaceState configurationState)
if (_menu == null)
return;
_menu?.Populate(configurationState);
_menu.Column.Children.Clear();
_menu.Inputs.Clear();
foreach (var field in entity.Comp.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = Control.VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = _menu.Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_menu.Inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = BoxContainer.LayoutOrientation.Horizontal
};
ConfigurationMenu.CopyProperties(_menu.Row, row);
row.AddChild(label);
row.AddChild(input);
_menu.Column.AddChild(row);
}
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)

View File

@@ -1,12 +1,8 @@
using System.Collections.Generic;
using System.Numerics;
using System.Numerics;
using System.Text.RegularExpressions;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using static Content.Shared.Configurable.ConfigurationComponent;
using static Robust.Client.UserInterface.Controls.BaseButton;
using static Robust.Client.UserInterface.Controls.BoxContainer;
@@ -14,10 +10,10 @@ namespace Content.Client.Configurable.UI
{
public sealed class ConfigurationMenu : DefaultWindow
{
private readonly BoxContainer _column;
private readonly BoxContainer _row;
public readonly BoxContainer Column;
public readonly BoxContainer Row;
private readonly List<(string name, LineEdit input)> _inputs;
public readonly List<(string name, LineEdit input)> Inputs;
[ViewVariables]
public Regex? Validation { get; internal set; }
@@ -28,7 +24,7 @@ namespace Content.Client.Configurable.UI
{
MinSize = SetSize = new Vector2(300, 250);
_inputs = new List<(string name, LineEdit input)>();
Inputs = new List<(string name, LineEdit input)>();
Title = Loc.GetString("configuration-menu-device-title");
@@ -39,14 +35,14 @@ namespace Content.Client.Configurable.UI
HorizontalExpand = true
};
_column = new BoxContainer
Column = new BoxContainer
{
Orientation = LayoutOrientation.Vertical,
Margin = new Thickness(8),
SeparationOverride = 16,
};
_row = new BoxContainer
Row = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
SeparationOverride = 16,
@@ -69,61 +65,20 @@ namespace Content.Client.Configurable.UI
ModulateSelfOverride = Color.FromHex("#202025")
};
outerColumn.AddChild(_column);
outerColumn.AddChild(Column);
baseContainer.AddChild(outerColumn);
baseContainer.AddChild(confirmButton);
Contents.AddChild(baseContainer);
}
public void Populate(ConfigurationBoundUserInterfaceState state)
{
_column.Children.Clear();
_inputs.Clear();
foreach (var field in state.Config)
{
var label = new Label
{
Margin = new Thickness(0, 0, 8, 0),
Name = field.Key,
Text = field.Key + ":",
VerticalAlignment = VAlignment.Center,
HorizontalExpand = true,
SizeFlagsStretchRatio = .2f,
MinSize = new Vector2(60, 0)
};
var input = new LineEdit
{
Name = field.Key + "-input",
Text = field.Value ?? "",
IsValid = Validate,
HorizontalExpand = true,
SizeFlagsStretchRatio = .8f
};
_inputs.Add((field.Key, input));
var row = new BoxContainer
{
Orientation = LayoutOrientation.Horizontal
};
CopyProperties(_row, row);
row.AddChild(label);
row.AddChild(input);
_column.AddChild(row);
}
}
private void OnConfirm(ButtonEventArgs args)
{
var config = GenerateDictionary(_inputs, "Text");
var config = GenerateDictionary(Inputs, "Text");
OnConfiguration?.Invoke(config);
Close();
}
private bool Validate(string value)
public bool Validate(string value)
{
return Validation?.IsMatch(value) != false;
}
@@ -140,7 +95,7 @@ namespace Content.Client.Configurable.UI
return dictionary;
}
private static void CopyProperties<T>(T from, T to) where T : Control
public static void CopyProperties<T>(T from, T to) where T : Control
{
foreach (var property in from.AllAttachedProperties)
{

View File

@@ -1,7 +1,7 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'credits-window-title'}"
SetSize="650 650" >
<TabContainer>
Title="{Loc 'credits-window-title'}"
SetSize="650 650">
<TabContainer Name="MasterTabContainer">
<ScrollContainer Name="Ss14ContributorsTab"
HScrollEnabled="False">
<BoxContainer Name="Ss14ContributorsContainer"
@@ -26,5 +26,11 @@
<!-- Licenses get added here by code -->
</BoxContainer>
</ScrollContainer>
<ScrollContainer Name="AttributionsTab"
HScrollEnabled="False">
<BoxContainer Name="AttributionsContainer"
Orientation="Vertical"
Margin="2 2 0 0" />
</ScrollContainer>
</TabContainer>
</DefaultWindow>

View File

@@ -1,184 +1,381 @@
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Threading.Tasks;
using Content.Client.Stylesheets;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.Credits;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Configuration;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Serialization.Markdown;
using Robust.Shared.Serialization.Markdown.Mapping;
using Robust.Shared.Serialization.Markdown.Sequence;
using Robust.Shared.Utility;
using YamlDotNet.RepresentationModel;
using static Robust.Client.UserInterface.Controls.BoxContainer;
namespace Content.Client.Credits
{
[GenerateTypedNameReferences]
public sealed partial class CreditsWindow : DefaultWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
namespace Content.Client.Credits;
private static readonly Dictionary<string, int> PatronTierPriority = new()
[GenerateTypedNameReferences]
public sealed partial class CreditsWindow : DefaultWindow
{
[Dependency] private readonly IResourceManager _resourceManager = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ISerializationManager _serialization = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!;
private static readonly Dictionary<string, int> PatronTierPriority = new()
{
["Nuclear Operative"] = 1,
["Syndicate Agent"] = 2,
["Revolutionary"] = 3,
};
private readonly List<FormattedMessage> _attributions = [];
private readonly ISawmill _sawmill = Logger.GetSawmill("Credits");
private const int AttributionsSourcesPerPage = 50;
public CreditsWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab"));
TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab"));
TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab"));
TabContainer.SetTabTitle(AttributionsTab, Loc.GetString("credits-window-attributions-tab"));
_protoManager.PrototypesReloaded += _ =>
{
["Nuclear Operative"] = 1,
["Syndicate Agent"] = 2,
["Revolutionary"] = 3
_attributions.Clear();
};
public CreditsWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
MasterTabContainer.OnTabChanged += OnTabChanged;
TabContainer.SetTabTitle(Ss14ContributorsTab, Loc.GetString("credits-window-ss14contributorslist-tab"));
TabContainer.SetTabTitle(PatronsTab, Loc.GetString("credits-window-patrons-tab"));
TabContainer.SetTabTitle(LicensesTab, Loc.GetString("credits-window-licenses-tab"));
PopulateContributors(Ss14ContributorsContainer);
}
/// <summary>
/// Only populates the tab when they are selected, which reduces lagspike when not looking at attributions.
/// </summary>
private void OnTabChanged(int tab)
{
if (tab == Ss14ContributorsTab.GetPositionInParent())
PopulateContributors(Ss14ContributorsContainer);
else if (tab == PatronsTab.GetPositionInParent())
PopulatePatrons(PatronsContainer);
else if (tab == LicensesTab.GetPositionInParent())
PopulateLicenses(LicensesContainer);
else if (tab == AttributionsTab.GetPositionInParent())
PopulateAttributions(AttributionsContainer, 0);
}
private async void PopulateAttributions(BoxContainer attributionsContainer, int count)
{
attributionsContainer.DisposeAllChildren();
if (_attributions.Count == 0)
{
var rsi = await CollectRSiAttributions();
var rga = await CollectRgaAttributions();
_attributions.AddRange(rsi);
_attributions.AddRange(rga);
}
private void PopulateLicenses(BoxContainer licensesContainer)
foreach (var message in _attributions.Skip(count).Take(AttributionsSourcesPerPage))
{
foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name))
{
licensesContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = entry.Name});
var rich = new RichTextLabel();
rich.SetMessage(message);
attributionsContainer.AddChild(rich);
}
// We split these line by line because otherwise
// the LGPL causes Clyde to go out of bounds in the rendering code.
foreach (var line in entry.License.Split("\n"))
var container = new BoxContainer { Orientation = LayoutOrientation.Horizontal };
var previousPageButton = new Button { Text = "Previous Page" };
previousPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count - AttributionsSourcesPerPage);
var nextPageButton = new Button { Text = "Next Page" };
nextPageButton.OnPressed +=
_ => PopulateAttributions(attributionsContainer, count + AttributionsSourcesPerPage);
if (count - AttributionsSourcesPerPage >= 0)
container.AddChild(previousPageButton);
if (count + AttributionsSourcesPerPage < _attributions.Count)
container.AddChild(nextPageButton);
attributionsContainer.AddChild(container);
}
private Task<List<FormattedMessage>> CollectRSiAttributions()
{
return Task.Run(() =>
{
var rsiStreams = _resourceManager.ContentFindFiles("/Textures/")
.Where(p => p.ToString().EndsWith(".rsi/meta.json"));
var attrs = new List<FormattedMessage>();
foreach (var stream in rsiStreams)
{
try
{
licensesContainer.AddChild(new Label {Text = line, FontColorOverride = new Color(200, 200, 200)});
var m = new FormattedMessage();
var yamlStream = _resourceManager.ContentFileReadYaml(stream);
if (yamlStream.Documents[0].RootNode.ToDataNode() is not MappingDataNode map)
throw new Exception("meta.json is not a mapping.");
if (!map.TryGet("copyright", out var copyrightNode))
throw new Exception("Missing the copyright field.");
if (!map.TryGet("states", out var statesNode))
throw new Exception("Missing the states field.");
if (statesNode is not SequenceDataNode states)
throw new Exception("Missing a list of states.");
var copyright = copyrightNode.ToString();
var files = states.Select(n => (MappingDataNode)n)
.Select(n => n.Get("name") + ".png");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory",
("directory", stream.Directory.ToString())));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files",
("files", string.Join(", ", files))));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright",
("copyright", copyright)));
m.AddText("\n");
attrs.Add(m);
}
catch (Exception e)
{
var m = new FormattedMessage();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed",
("file", stream.ToString())));
m.AddText("\n");
_sawmill.Error($"{stream.ToString()}\n{e}");
attrs.Add(m);
}
}
}
private void PopulatePatrons(BoxContainer patronsContainer)
return attrs;
});
}
private Task<List<FormattedMessage>> CollectRgaAttributions()
{
return Task.Run(() =>
{
var patrons = LoadPatrons();
var rgaStreams = _resourceManager.ContentFindFiles("/")
.Where(p => p.Filename == "attributions.yml");
// Do not show "become a patron" button on Steam builds
// since Patreon violates Valve's rules about alternative storefronts.
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon);
if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "")
var attrs = new List<FormattedMessage>();
foreach (var stream in rgaStreams)
{
Button patronButton;
patronsContainer.AddChild(patronButton = new Button
try
{
Text = Loc.GetString("credits-window-become-patron-button"),
HorizontalAlignment = HAlignment.Center
});
var yamlStream = _resourceManager.ContentFileReadYaml(stream);
patronButton.OnPressed +=
_ => IoCManager.Resolve<IUriOpener>().OpenUri(linkPatreon);
if (yamlStream.Documents[0].RootNode.ToDataNode() is not SequenceDataNode sequence)
throw new Exception("Attributions file is not a list of attributions.");
foreach (var attribution in sequence.Sequence)
{
var m = new FormattedMessage();
if (attribution is not MappingDataNode map)
throw new Exception("Attribution is not a mapping.");
if (!map.TryGet("files", out var filesNode))
throw new Exception("Attribution does not list files.");
if (!map.TryGet("copyright", out var copyrightNode))
throw new Exception("Attribution does not copyright.");
if (!map.TryGet("license", out var licenseNode))
throw new Exception("Attribution does not identify a license.");
if (!map.TryGet("source", out var sourceNode))
throw new Exception("Attribution does not identify a source.");
var files = _serialization.Read<string[]>(filesNode, notNullableOverride: true);
var copyright = copyrightNode.ToString();
var license = licenseNode.ToString();
var source = sourceNode.ToString();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-directory",
("directory", stream.Directory.ToString())));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-files",
("files", string.Join(", ", files))));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-copyright",
("copyright", copyright)));
m.AddText("\n");
m.AddMarkupPermissive(
_loc.GetString("credits-window-attributions-license", ("license", license)));
m.AddText("\n");
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-source", ("source", source)));
m.AddText("\n");
attrs.Add(m);
}
}
catch (Exception e)
{
var m = new FormattedMessage();
m.AddMarkupPermissive(_loc.GetString("credits-window-attributions-failed",
("file", stream.ToString())));
m.AddText("\n");
_sawmill.Error($"{stream.ToString()}\n{e}");
attrs.Add(m);
}
}
var first = true;
foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key]))
{
if (!first)
{
patronsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)});
}
return attrs;
});
}
first = false;
patronsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = $"{tier.Key}"});
var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name));
var label = new RichTextLabel();
label.SetMessage(msg);
patronsContainer.AddChild(label);
}
}
private IEnumerable<PatronEntry> LoadPatrons()
private void PopulateLicenses(BoxContainer licensesContainer)
{
foreach (var entry in CreditsManager.GetLicenses(_resourceManager).OrderBy(p => p.Name))
{
var yamlStream = _resourceManager.ContentFileReadYaml(new ("/Credits/Patrons.yml"));
var sequence = (YamlSequenceNode) yamlStream.Documents[0].RootNode;
licensesContainer.AddChild(new Label
{ StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = entry.Name });
return sequence
.Cast<YamlMappingNode>()
.Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString()));
}
private void PopulateContributors(BoxContainer ss14ContributorsContainer)
{
Button contributeButton;
ss14ContributorsContainer.AddChild(new BoxContainer
// We split these line by line because otherwise
// the LGPL causes Clyde to go out of bounds in the rendering code.
foreach (var line in entry.License.Split("\n"))
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
SeparationOverride = 20,
Children =
{
new Label {Text = Loc.GetString("credits-window-contributor-encouragement-label") },
(contributeButton = new Button {Text = Loc.GetString("credits-window-contribute-button")})
}
});
var first = true;
void AddSection(string title, string path, bool markup = false)
{
if (!first)
{
ss14ContributorsContainer.AddChild(new Control {MinSize = new Vector2(0, 10)});
}
first = false;
ss14ContributorsContainer.AddChild(new Label {StyleClasses = {StyleBase.StyleClassLabelHeading}, Text = title});
var label = new RichTextLabel();
var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}");
if (markup)
{
label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
}
else
{
label.SetMessage(text);
}
ss14ContributorsContainer.AddChild(label);
}
AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt");
AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt");
AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt");
AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true);
var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub);
contributeButton.OnPressed += _ =>
IoCManager.Resolve<IUriOpener>().OpenUri(linkGithub);
if (linkGithub == "")
contributeButton.Visible = false;
}
private sealed class PatronEntry
{
public string Name { get; }
public string Tier { get; }
public PatronEntry(string name, string tier)
{
Name = name;
Tier = tier;
licensesContainer.AddChild(new Label { Text = line, FontColorOverride = new Color(200, 200, 200) });
}
}
}
private void PopulatePatrons(BoxContainer patronsContainer)
{
var patrons = LoadPatrons();
// Do not show "become a patron" button on Steam builds
// since Patreon violates Valve's rules about alternative storefronts.
var linkPatreon = _cfg.GetCVar(CCVars.InfoLinksPatreon);
if (!_cfg.GetCVar(CCVars.BrandingSteam) && linkPatreon != "")
{
Button patronButton;
patronsContainer.AddChild(patronButton = new Button
{
Text = Loc.GetString("credits-window-become-patron-button"),
HorizontalAlignment = HAlignment.Center,
});
patronButton.OnPressed +=
_ => IoCManager.Resolve<IUriOpener>().OpenUri(linkPatreon);
}
var first = true;
foreach (var tier in patrons.GroupBy(p => p.Tier).OrderBy(p => PatronTierPriority[p.Key]))
{
if (!first)
patronsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) });
first = false;
patronsContainer.AddChild(new Label
{ StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = $"{tier.Key}" });
var msg = string.Join(", ", tier.OrderBy(p => p.Name).Select(p => p.Name));
var label = new RichTextLabel();
label.SetMessage(msg);
patronsContainer.AddChild(label);
}
}
private IEnumerable<PatronEntry> LoadPatrons()
{
var yamlStream = _resourceManager.ContentFileReadYaml(new ResPath("/Credits/Patrons.yml"));
var sequence = (YamlSequenceNode)yamlStream.Documents[0].RootNode;
return sequence
.Cast<YamlMappingNode>()
.Select(m => new PatronEntry(m["Name"].AsString(), m["Tier"].AsString()));
}
private void PopulateContributors(BoxContainer ss14ContributorsContainer)
{
Button contributeButton;
ss14ContributorsContainer.AddChild(new BoxContainer
{
Orientation = LayoutOrientation.Horizontal,
HorizontalAlignment = HAlignment.Center,
SeparationOverride = 20,
Children =
{
new Label { Text = Loc.GetString("credits-window-contributor-encouragement-label") },
(contributeButton = new Button { Text = Loc.GetString("credits-window-contribute-button") }),
},
});
var first = true;
void AddSection(string title, string path, bool markup = false)
{
if (!first)
ss14ContributorsContainer.AddChild(new Control { MinSize = new Vector2(0, 10) });
first = false;
ss14ContributorsContainer.AddChild(new Label
{ StyleClasses = { StyleBase.StyleClassLabelHeading }, Text = title });
var label = new RichTextLabel();
var text = _resourceManager.ContentFileReadAllText($"/Credits/{path}");
if (markup)
label.SetMessage(FormattedMessage.FromMarkupOrThrow(text.Trim()));
else
label.SetMessage(text);
ss14ContributorsContainer.AddChild(label);
}
AddSection(Loc.GetString("credits-window-contributors-section-title"), "GitHub.txt");
AddSection(Loc.GetString("credits-window-codebases-section-title"), "SpaceStation13.txt");
AddSection(Loc.GetString("credits-window-original-remake-team-section-title"), "OriginalRemake.txt");
AddSection(Loc.GetString("credits-window-special-thanks-section-title"), "SpecialThanks.txt", true);
var linkGithub = _cfg.GetCVar(CCVars.InfoLinksGithub);
contributeButton.OnPressed += _ =>
IoCManager.Resolve<IUriOpener>().OpenUri(linkGithub);
if (linkGithub == "")
contributeButton.Visible = false;
}
private sealed class PatronEntry
{
public string Name { get; }
public string Tier { get; }
public PatronEntry(string name, string tier)
{
Name = name;
Tier = tier;
}
}
}

View File

@@ -213,52 +213,14 @@ public sealed partial class CriminalRecordsConsoleWindow : FancyWindow
return;
}
var entries = listing.ToList();
entries.Sort((a, b) => string.Compare(a.Value, b.Value, StringComparison.Ordinal));
// `entries` now contains the definitive list of items which should be in
// our list of records and is in the order we want to present those items.
// Walk through the existing items in RecordListing and in the updated listing
// in parallel to synchronize the items in RecordListing with `entries`.
int i = RecordListing.Count - 1;
int j = entries.Count - 1;
while (i >= 0 && j >= 0)
{
var strcmp = string.Compare(RecordListing[i].Text, entries[j].Value, StringComparison.Ordinal);
if (strcmp == 0)
{
// This item exists in both RecordListing and `entries`. Nothing to do.
i--;
j--;
}
else if (strcmp > 0)
{
// Item exists in RecordListing, but not in `entries`. Remove it.
RecordListing.RemoveAt(i);
i--;
}
else if (strcmp < 0)
{
// A new entry which doesn't exist in RecordListing. Create it.
RecordListing.Insert(i + 1, new ItemList.Item(RecordListing){Text = entries[j].Value, Metadata = entries[j].Key});
j--;
}
}
// Any remaining items in RecordListing don't exist in `entries`, so remove them
while (i >= 0)
{
RecordListing.RemoveAt(i);
i--;
}
// And finally, any remaining items in `entries`, don't exist in RecordListing. Create them.
while (j >= 0)
{
RecordListing.Insert(0, new ItemList.Item(RecordListing){ Text = entries[j].Value, Metadata = entries[j].Key });
j--;
}
var entries = listing.Select(i => new ItemList.Item(RecordListing) {
Text = i.Value,
Metadata = i.Key
}).ToList();
entries.Sort((a, b) => string.Compare(a.Text, b.Text, StringComparison.Ordinal));
RecordListing.SetItems(entries, (a,b) => string.Compare(a.Text, b.Text));
}
private void PopulateRecordContainer(GeneralStationRecord stationRecord, CriminalRecord criminalRecord)
{
var specifier = new SpriteSpecifier.Rsi(new ResPath("Interface/Misc/job_icons.rsi"), "Unknown");

View File

@@ -13,7 +13,7 @@ namespace Content.Client.Decals
[Dependency] private readonly IOverlayManager _overlayManager = default!;
[Dependency] private readonly SpriteSystem _sprites = default!;
private DecalOverlay _overlay = default!;
private DecalOverlay? _overlay;
private HashSet<uint> _removedUids = new();
private readonly List<Vector2i> _removedChunks = new();
@@ -31,6 +31,9 @@ namespace Content.Client.Decals
public void ToggleOverlay()
{
if (_overlay == null)
return;
if (_overlayManager.HasOverlay<DecalOverlay>())
{
_overlayManager.RemoveOverlay(_overlay);
@@ -44,6 +47,10 @@ namespace Content.Client.Decals
public override void Shutdown()
{
base.Shutdown();
if (_overlay == null)
return;
_overlayManager.RemoveOverlay(_overlay);
}

View File

@@ -43,3 +43,9 @@ public enum DeliveryVisualLayers : byte
Breakage,
Trash,
}
public enum DeliverySpawnerVisualLayers : byte
{
Contents,
}

View File

@@ -0,0 +1,8 @@
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.DeviceNetwork.Systems;
public sealed class DeviceNetworkSystem : SharedDeviceNetworkSystem
{
}

View File

@@ -9,22 +9,36 @@ public sealed class DisplacementMapSystem : EntitySystem
{
[Dependency] private readonly ISerializationManager _serialization = default!;
public bool TryAddDisplacement(DisplacementData data, SpriteComponent sprite, int index, string key, HashSet<string> revealedLayers)
/// <summary>
/// Attempting to apply a displacement map to a specific layer of SpriteComponent
/// </summary>
/// <param name="data">Information package for applying the displacement map</param>
/// <param name="sprite">SpriteComponent</param>
/// <param name="index">Index of the layer where the new map layer will be added</param>
/// <param name="key">Unique layer key, which will determine which layer to apply displacement map to</param>
/// <param name="displacementKey">The key of the new displacement map layer added by this function.</param>
/// <returns></returns>
public bool TryAddDisplacement(DisplacementData data,
SpriteComponent sprite,
int index,
object key,
out string displacementKey)
{
displacementKey = $"{key}-displacement";
if (key.ToString() is null)
return false;
if (data.ShaderOverride != null)
sprite.LayerSetShader(index, data.ShaderOverride);
var displacementKey = $"{key}-displacement";
if (!revealedLayers.Add(displacementKey))
{
Log.Warning($"Duplicate key for DISPLACEMENT: {displacementKey}.");
return false;
}
if (sprite.LayerMapTryGet(displacementKey, out var oldIndex))
sprite.RemoveLayer(oldIndex);
//allows you not to write it every time in the YML
foreach (var pair in data.SizeMaps)
{
pair.Value.CopyToShaderParameters??= new()
pair.Value.CopyToShaderParameters ??= new()
{
LayerKey = "dummy",
ParameterTexture = "displacementMap",
@@ -45,21 +59,22 @@ public sealed class DisplacementMapSystem : EntitySystem
if (actualRSI is not null)
{
if (actualRSI.Size.X != actualRSI.Size.Y)
Log.Warning($"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked");
{
Log.Warning(
$"DISPLACEMENT: {displacementKey} has a resolution that is not 1:1, things can look crooked");
}
var layerSize = actualRSI.Size.X;
if (data.SizeMaps.ContainsKey(layerSize))
displacementDataLayer = data.SizeMaps[layerSize];
if (data.SizeMaps.TryGetValue(layerSize, out var map))
displacementDataLayer = map;
}
var displacementLayer = _serialization.CreateCopy(displacementDataLayer, notNullableOverride: true);
displacementLayer.CopyToShaderParameters!.LayerKey = key;
displacementLayer.CopyToShaderParameters!.LayerKey = key.ToString() ?? "this is impossible";
sprite.AddLayer(displacementLayer, index);
sprite.LayerMapSet(displacementKey, index);
revealedLayers.Add(displacementKey);
return true;
}
}

View File

@@ -1,9 +0,0 @@
using Content.Shared.Disposal.Components;
namespace Content.Client.Disposal;
[RegisterComponent]
public sealed partial class DisposalUnitComponent : SharedDisposalUnitComponent
{
}

View File

@@ -0,0 +1,79 @@
using Content.Client.Disposal.Unit;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
public MailingUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
MailingUnitWindow = this.CreateWindow<MailingUnitWindow>();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
if (EntMan.TryGetComponent(Owner, out MailingUnitComponent? component))
Refresh((Owner, component));
}
public void Refresh(Entity<MailingUnitComponent> entity)
{
if (MailingUnitWindow == null)
return;
// TODO: This should be decoupled from disposals
if (EntMan.TryGetComponent(entity.Owner, out DisposalUnitComponent? disposals))
{
var disposalSystem = EntMan.System<DisposalUnitSystem>();
var disposalState = disposalSystem.GetState(Owner, disposals);
var fullPressure = disposalSystem.EstimatedFullPressure(Owner, disposals);
MailingUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{disposalState}");
MailingUnitWindow.FullPressure = fullPressure;
MailingUnitWindow.PressureBar.UpdatePressure(fullPressure);
MailingUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
MailingUnitWindow.Engage.Pressed = disposals.Engaged;
}
MailingUnitWindow.Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", entity.Comp.Tag ?? " "));
//UnitTag.Text = state.Tag;
MailingUnitWindow.Target.Text = entity.Comp.Target;
MailingUnitWindow.TargetListContainer.Clear();
foreach (var target in entity.Comp.TargetList)
{
MailingUnitWindow.TargetListContainer.AddItem(target);
}
}
}

View File

@@ -0,0 +1,22 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Mailing;
namespace Content.Client.Disposal.Mailing;
public sealed class MailingUnitSystem : SharedMailingUnitSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MailingUnitComponent, AfterAutoHandleStateEvent>(OnMailingState);
}
private void OnMailingState(Entity<MailingUnitComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<MailingUnitBoundUserInterface>(ent.Owner, MailingUnitUiKey.Key, out var bui))
{
bui.Refresh(ent);
}
}
}

View File

@@ -1,12 +1,14 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI"
MinSize="300 400"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 400"
SetSize="300 400"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Horizontal" SeparationOverride="8">
<Label Text="{Loc 'ui-mailing-unit-target-label'}" />
<Label Name="Target"
Access="Public"
Text="" />
</BoxContainer>
<ItemList Name="TargetListContainer"
@@ -18,14 +20,15 @@
</ItemList>
<BoxContainer Orientation="Horizontal" SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState"
<Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar"
<disposal:PressureBar Name="PressureBar"
Access="Public"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -50,4 +53,4 @@
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -0,0 +1,27 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Mailing
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,9 +1,10 @@
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI;
namespace Content.Client.Disposal;
public sealed class PressureBar : ProgressBar
{

View File

@@ -1,187 +0,0 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using Content.Shared.DragDrop;
using Content.Shared.Emag.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Animations;
using Robust.Client.Graphics;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.GameStates;
using Robust.Shared.Physics.Events;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.Systems;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, ComponentHandleState>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, PreventCollideEvent>(OnPreventCollide);
SubscribeLocalEvent<DisposalUnitComponent, CanDropTargetEvent>(OnCanDragDropOn);
SubscribeLocalEvent<DisposalUnitComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<DisposalUnitComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref ComponentHandleState args)
{
if (args.Current is not DisposalUnitComponentState state)
return;
component.FlushSound = state.FlushSound;
component.State = state.State;
component.NextPressurized = state.NextPressurized;
component.AutomaticEngageTime = state.AutomaticEngageTime;
component.NextFlush = state.NextFlush;
component.Powered = state.Powered;
component.Engaged = state.Engaged;
component.RecentlyEjected.Clear();
component.RecentlyEjected.AddRange(EnsureEntityList<DisposalUnitComponent>(state.RecentlyEjected, uid));
}
public override bool HasDisposals(EntityUid? uid)
{
return HasComp<DisposalUnitComponent>(uid);
}
public override bool ResolveDisposals(EntityUid uid, [NotNullWhen(true)] ref SharedDisposalUnitComponent? component)
{
if (component != null)
return true;
TryComp<DisposalUnitComponent>(uid, out var storage);
component = storage;
return component != null;
}
public override void DoInsertDisposalUnit(EntityUid uid, EntityUid toInsert, EntityUid user, SharedDisposalUnitComponent? disposal = null)
{
return;
}
private void OnComponentInit(EntityUid uid, SharedDisposalUnitComponent sharedDisposalUnit, ComponentInit args)
{
if (!TryComp<SpriteComponent>(uid, out var sprite) || !TryComp<AppearanceComponent>(uid, out var appearance))
return;
UpdateState(uid, sharedDisposalUnit, sprite, appearance);
}
private void OnAppearanceChange(EntityUid uid, SharedDisposalUnitComponent unit, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(uid, unit, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(EntityUid uid, SharedDisposalUnitComponent unit, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<VisualState>(uid, Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state is VisualState.OverlayFlushing or VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(uid, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = unit.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
// Return to base state (though, depending on how the unit is
// configured we might get an appearance change event telling
// us to go to charging state)
new AnimationTrackSpriteFlick.KeyFrame(chargingState, (float) unit.FlushDelay.TotalSeconds)
}
},
}
};
if (unit.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(unit.FlushSound), 0)
}
});
}
_animationSystem.Play(uid, anim, AnimationKey);
}
}
else if (state == VisualState.OverlayCharging)
sprite.LayerSetState(DisposalUnitVisualLayers.OverlayFlush, chargingState);
else
_animationSystem.Stop(uid, AnimationKey);
if (!_appearanceSystem.TryGetData<HandleState>(uid, Visuals.Handle, out var handleState, appearance))
handleState = HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != HandleState.Normal);
if (!_appearanceSystem.TryGetData<LightStates>(uid, Visuals.Light, out var lightState, appearance))
lightState = LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Initializes a <see cref="DisposalRouterWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalRouterComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalRouterComponent"/>

View File

@@ -1,9 +1,8 @@
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Initializes a <see cref="DisposalTaggerWindow"/> and updates it when new server messages are received.

View File

@@ -1,11 +1,10 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using static Content.Shared.Disposal.Components.SharedDisposalTaggerComponent;
namespace Content.Client.Disposal.UI
namespace Content.Client.Disposal.Tube
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalTaggerComponent"/>

View File

@@ -0,0 +1,8 @@
using Content.Shared.Disposal.Unit;
namespace Content.Client.Disposal.Tube;
public sealed class DisposalTubeSystem : SharedDisposalTubeSystem
{
}

View File

@@ -1,103 +0,0 @@
using Content.Client.Disposal.Systems;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="DisposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
// What are you doing here
[ViewVariables]
public MailingUnitWindow? MailingUnitWindow;
[ViewVariables]
public DisposalUnitWindow? DisposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(UiButton button)
{
SendMessage(new UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
private void TargetSelected(ItemList.ItemListSelectedEventArgs args)
{
var item = args.ItemList[args.ItemIndex];
SendMessage(new TargetSelectedMessage(item.Text));
}
protected override void Open()
{
base.Open();
if (UiKey is MailingUnitUiKey)
{
MailingUnitWindow = new MailingUnitWindow();
MailingUnitWindow.OpenCenteredRight();
MailingUnitWindow.OnClose += Close;
MailingUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
MailingUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
MailingUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
MailingUnitWindow.TargetListContainer.OnItemSelected += TargetSelected;
}
else if (UiKey is DisposalUnitUiKey)
{
DisposalUnitWindow = new DisposalUnitWindow();
DisposalUnitWindow.OpenCenteredRight();
DisposalUnitWindow.OnClose += Close;
DisposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(UiButton.Eject);
DisposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(UiButton.Engage);
DisposalUnitWindow.Power.OnPressed += _ => ButtonPressed(UiButton.Power);
}
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not MailingUnitBoundUserInterfaceState && state is not DisposalUnitBoundUserInterfaceState)
{
return;
}
switch (state)
{
case MailingUnitBoundUserInterfaceState mailingUnitState:
MailingUnitWindow?.UpdateState(mailingUnitState);
break;
case DisposalUnitBoundUserInterfaceState disposalUnitState:
DisposalUnitWindow?.UpdateState(disposalUnitState);
break;
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (!disposing)
return;
MailingUnitWindow?.Dispose();
DisposalUnitWindow?.Dispose();
}
}
}

View File

@@ -1,43 +0,0 @@
using Content.Shared.Disposal.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
using static Content.Shared.Disposal.Components.SharedDisposalUnitComponent;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="SharedDisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public void UpdateState(DisposalUnitBoundUserInterfaceState state)
{
Title = state.UnitName;
UnitState.Text = state.UnitState;
Power.Pressed = state.Powered;
Engage.Pressed = state.Engaged;
FullPressure = state.FullPressureTime;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,55 +0,0 @@
using Content.Shared.Disposal;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.UI
{
/// <summary>
/// Client-side UI used to control a <see cref="MailingUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class MailingUnitWindow : DefaultWindow
{
public TimeSpan FullPressure;
public MailingUnitWindow()
{
RobustXamlLoader.Load(this);
}
/// <summary>
/// Update the interface state for the disposals window.
/// </summary>
/// <returns>true if we should stop updating every frame.</returns>
public bool UpdateState(MailingUnitBoundUserInterfaceState state)
{
var disposalState = state.DisposalState;
Title = Loc.GetString("ui-mailing-unit-window-title", ("tag", state.Tag ?? " "));
UnitState.Text = disposalState.UnitState;
FullPressure = disposalState.FullPressureTime;
var pressureReached = PressureBar.UpdatePressure(disposalState.FullPressureTime);
Power.Pressed = disposalState.Powered;
Engage.Pressed = disposalState.Engaged;
//UnitTag.Text = state.Tag;
Target.Text = state.Target;
TargetListContainer.Clear();
foreach (var target in state.TargetList)
{
TargetListContainer.AddItem(target);
}
return !disposalState.Powered || pressureReached;
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -0,0 +1,63 @@
using Content.Client.Disposal.Mailing;
using Content.Client.Power.EntitySystems;
using Content.Shared.Disposal.Components;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Initializes a <see cref="MailingUnitWindow"/> or a <see cref="_disposalUnitWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class DisposalUnitBoundUserInterface : BoundUserInterface
{
[ViewVariables] private DisposalUnitWindow? _disposalUnitWindow;
public DisposalUnitBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private void ButtonPressed(DisposalUnitComponent.UiButton button)
{
SendPredictedMessage(new DisposalUnitComponent.UiButtonPressedMessage(button));
// If we get client-side power stuff then we can predict the button presses but for now we won't as it stuffs
// the pressure lerp up.
}
protected override void Open()
{
base.Open();
_disposalUnitWindow = this.CreateWindow<DisposalUnitWindow>();
_disposalUnitWindow.OpenCenteredRight();
_disposalUnitWindow.Eject.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Eject);
_disposalUnitWindow.Engage.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Engage);
_disposalUnitWindow.Power.OnPressed += _ => ButtonPressed(DisposalUnitComponent.UiButton.Power);
if (EntMan.TryGetComponent(Owner, out DisposalUnitComponent? component))
{
Refresh((Owner, component));
}
}
public void Refresh(Entity<DisposalUnitComponent> entity)
{
if (_disposalUnitWindow == null)
return;
var disposalSystem = EntMan.System<DisposalUnitSystem>();
_disposalUnitWindow.Title = EntMan.GetComponent<MetaDataComponent>(entity.Owner).EntityName;
var state = disposalSystem.GetState(entity.Owner, entity.Comp);
_disposalUnitWindow.UnitState.Text = Loc.GetString($"disposal-unit-state-{state}");
_disposalUnitWindow.Power.Pressed = EntMan.System<PowerReceiverSystem>().IsPowered(Owner);
_disposalUnitWindow.Engage.Pressed = entity.Comp.Engaged;
_disposalUnitWindow.FullPressure = disposalSystem.EstimatedFullPressure(entity.Owner, entity.Comp);
}
}
}

View File

@@ -0,0 +1,151 @@
using Content.Shared.Disposal.Components;
using Content.Shared.Disposal.Unit;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Audio.Systems;
namespace Content.Client.Disposal.Unit;
public sealed class DisposalUnitSystem : SharedDisposalUnitSystem
{
[Dependency] private readonly AppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
private const string AnimationKey = "disposal_unit_animation";
private const string DefaultFlushState = "disposal-flush";
private const string DefaultChargeState = "disposal-charging";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DisposalUnitComponent, AfterAutoHandleStateEvent>(OnHandleState);
SubscribeLocalEvent<DisposalUnitComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnHandleState(EntityUid uid, DisposalUnitComponent component, ref AfterAutoHandleStateEvent args)
{
UpdateUI((uid, component));
}
protected override void UpdateUI(Entity<DisposalUnitComponent> entity)
{
if (_uiSystem.TryGetOpenUi<DisposalUnitBoundUserInterface>(entity.Owner, DisposalUnitComponent.DisposalUnitUiKey.Key, out var bui))
{
bui.Refresh(entity);
}
}
protected override void OnDisposalInit(Entity<DisposalUnitComponent> ent, ref ComponentInit args)
{
base.OnDisposalInit(ent, ref args);
if (!TryComp<SpriteComponent>(ent, out var sprite) || !TryComp<AppearanceComponent>(ent, out var appearance))
return;
UpdateState(ent, sprite, appearance);
}
private void OnAppearanceChange(Entity<DisposalUnitComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
UpdateState(ent, args.Sprite, args.Component);
}
/// <summary>
/// Update visuals and tick animation
/// </summary>
private void UpdateState(Entity<DisposalUnitComponent> ent, SpriteComponent sprite, AppearanceComponent appearance)
{
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.VisualState>(ent, DisposalUnitComponent.Visuals.VisualState, out var state, appearance))
return;
sprite.LayerSetVisible(DisposalUnitVisualLayers.Unanchored, state == DisposalUnitComponent.VisualState.UnAnchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.Base, state == DisposalUnitComponent.VisualState.Anchored);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFlush, state == DisposalUnitComponent.VisualState.OverlayFlushing);
sprite.LayerSetVisible(DisposalUnitVisualLayers.BaseCharging, state == DisposalUnitComponent.VisualState.OverlayCharging);
var chargingState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.BaseCharging, out var chargingLayer)
? sprite.LayerGetState(chargingLayer)
: new RSI.StateId(DefaultChargeState);
// This is a transient state so not too worried about replaying in range.
if (state == DisposalUnitComponent.VisualState.OverlayFlushing)
{
if (!_animationSystem.HasRunningAnimation(ent, AnimationKey))
{
var flushState = sprite.LayerMapTryGet(DisposalUnitVisualLayers.OverlayFlush, out var flushLayer)
? sprite.LayerGetState(flushLayer)
: new RSI.StateId(DefaultFlushState);
// Setup the flush animation to play
var anim = new Animation
{
Length = ent.Comp.FlushDelay,
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = DisposalUnitVisualLayers.OverlayFlush,
KeyFrames =
{
// Play the flush animation
new AnimationTrackSpriteFlick.KeyFrame(flushState, 0),
}
},
}
};
if (ent.Comp.FlushSound != null)
{
anim.AnimationTracks.Add(
new AnimationTrackPlaySound
{
KeyFrames =
{
new AnimationTrackPlaySound.KeyFrame(_audioSystem.ResolveSound(ent.Comp.FlushSound), 0)
}
});
}
_animationSystem.Play(ent, anim, AnimationKey);
}
}
else
_animationSystem.Stop(ent.Owner, AnimationKey);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.HandleState>(ent, DisposalUnitComponent.Visuals.Handle, out var handleState, appearance))
handleState = DisposalUnitComponent.HandleState.Normal;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayEngaged, handleState != DisposalUnitComponent.HandleState.Normal);
if (!_appearanceSystem.TryGetData<DisposalUnitComponent.LightStates>(ent, DisposalUnitComponent.Visuals.Light, out var lightState, appearance))
lightState = DisposalUnitComponent.LightStates.Off;
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayCharging,
(lightState & DisposalUnitComponent.LightStates.Charging) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayReady,
(lightState & DisposalUnitComponent.LightStates.Ready) != 0);
sprite.LayerSetVisible(DisposalUnitVisualLayers.OverlayFull,
(lightState & DisposalUnitComponent.LightStates.Full) != 0);
}
}
public enum DisposalUnitVisualLayers : byte
{
Unanchored,
Base,
BaseCharging,
OverlayFlush,
OverlayCharging,
OverlayReady,
OverlayFull,
OverlayEngaged
}

View File

@@ -1,20 +1,21 @@
<DefaultWindow xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.Disposal.UI"
MinSize="300 140"
SetSize="300 140"
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:disposal="clr-namespace:Content.Client.Disposal"
MinSize="300 140"
SetSize="300 160"
Resizable="False">
<BoxContainer Orientation="Vertical">
<BoxContainer Orientation="Vertical" Margin="10">
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-state'}" />
<Label Name="UnitState"
<Label Name="UnitState" Access="Public"
Text="{Loc 'ui-disposal-unit-label-status'}" />
</BoxContainer>
<Control MinSize="0 5" />
<BoxContainer Orientation="Horizontal"
SeparationOverride="4">
<Label Text="{Loc 'ui-disposal-unit-label-pressure'}" />
<ui:PressureBar Name="PressureBar"
<disposal:PressureBar Name="PressureBar"
MinSize="190 20"
HorizontalAlignment="Right"
MinValue="0"
@@ -23,7 +24,7 @@
Value="0.5" />
</BoxContainer>
<Control MinSize="0 10" />
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Horizontal" HorizontalExpand="True">
<Button Name="Engage"
Access="Public"
Text="{Loc 'ui-disposal-unit-button-flush'}"
@@ -39,4 +40,4 @@
StyleClasses="OpenLeft" />
</BoxContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -0,0 +1,28 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Disposal.Unit
{
/// <summary>
/// Client-side UI used to control a <see cref="Shared.Disposal.Components.DisposalUnitComponent"/>
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class DisposalUnitWindow : FancyWindow
{
public TimeSpan FullPressure;
public DisposalUnitWindow()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
PressureBar.UpdatePressure(FullPressure);
}
}
}

View File

@@ -1,5 +1,6 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
namespace Content.Client.Doors;
@@ -14,6 +15,30 @@ public sealed class FirelockSystem : SharedFirelockSystem
SubscribeLocalEvent<FirelockComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
protected override void OnComponentStartup(Entity<FirelockComponent> ent, ref ComponentStartup args)
{
base.OnComponentStartup(ent, ref args);
if(!TryComp<DoorComponent>(ent.Owner, out var door))
return;
door.ClosedSpriteStates.Add((DoorVisualLayers.BaseUnlit, ent.Comp.WarningLightSpriteState));
door.OpenSpriteStates.Add((DoorVisualLayers.BaseUnlit, ent.Comp.WarningLightSpriteState));
((Animation)door.OpeningAnimation).AnimationTracks.Add(new AnimationTrackSpriteFlick()
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.OpeningLightSpriteState, 0f) },
}
);
((Animation)door.ClosingAnimation).AnimationTracks.Add(new AnimationTrackSpriteFlick()
{
LayerKey = DoorVisualLayers.BaseUnlit,
KeyFrames = { new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.ClosingLightSpriteState, 0f) },
}
);
}
private void OnAppearanceChange(EntityUid uid, FirelockComponent comp, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)

View File

@@ -0,0 +1,75 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.Examine;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Content.Client.Doors;
/// <inheritdoc/>
public sealed class TurnstileSystem : SharedTurnstileSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationPlayer = default!;
private static EntProtoId _examineArrow = "TurnstileArrow";
private const string AnimationKey = "Turnstile";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<TurnstileComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<TurnstileComponent, ExaminedEvent>(OnExamined);
}
private void OnAnimationCompleted(Entity<TurnstileComponent> ent, ref AnimationCompletedEvent args)
{
if (args.Key != AnimationKey)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
sprite.LayerSetState(TurnstileVisualLayers.Base, new RSI.StateId(ent.Comp.DefaultState));
}
private void OnExamined(Entity<TurnstileComponent> ent, ref ExaminedEvent args)
{
Spawn(_examineArrow, new EntityCoordinates(ent, 0, 0));
}
protected override void PlayAnimation(EntityUid uid, string stateId)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var animation) || !TryComp<SpriteComponent>(uid, out var sprite))
return;
var ent = (uid, animation);
if (_animationPlayer.HasRunningAnimation(animation, AnimationKey))
_animationPlayer.Stop(ent, AnimationKey);
if (sprite.BaseRSI == null || !sprite.BaseRSI.TryGetState(stateId, out var state))
return;
var animLength = state.AnimationLength;
var anim = new Animation
{
AnimationTracks =
{
new AnimationTrackSpriteFlick
{
LayerKey = TurnstileVisualLayers.Base,
KeyFrames =
{
new AnimationTrackSpriteFlick.KeyFrame(state.StateId, 0f),
},
},
},
Length = TimeSpan.FromSeconds(animLength),
};
_animationPlayer.Play(ent, anim, AnimationKey);
}
}

View File

@@ -32,6 +32,8 @@ namespace Content.Client.Examine
[Dependency] private readonly VerbSystem _verbSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
private List<Verb> _verbList = new();
public const string StyleClassEntityTooltip = "entity-tooltip";
private EntityUid _examinedEntity;
@@ -158,13 +160,13 @@ namespace Content.Client.Examine
var entity = GetEntity(ev.EntityUid);
OpenTooltip(player.Value, entity, ev.CenterAtCursor, ev.OpenAtOldTooltip, ev.KnowTarget);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs);
UpdateTooltipInfo(player.Value, entity, ev.Message, ev.Verbs, getVerbs: false);
}
public override void SendExamineTooltip(EntityUid player, EntityUid target, FormattedMessage message, bool getVerbs, bool centerAtCursor)
{
OpenTooltip(player, target, centerAtCursor, false);
UpdateTooltipInfo(player, target, message);
OpenTooltip(player, target, centerAtCursor);
UpdateTooltipInfo(player, target, message, getVerbs: getVerbs);
}
/// <summary>
@@ -259,7 +261,7 @@ namespace Content.Client.Examine
/// <summary>
/// Fills the examine tooltip with a message and buttons if applicable.
/// </summary>
public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null)
public void UpdateTooltipInfo(EntityUid player, EntityUid target, FormattedMessage message, List<Verb>? verbs=null, bool getVerbs = true)
{
var vBox = _examineTooltipOpen?.GetChild(0).GetChild(0);
if (vBox == null)
@@ -283,9 +285,29 @@ namespace Content.Client.Examine
break;
}
verbs ??= new List<Verb>();
var totalVerbs = _verbSystem.GetLocalVerbs(target, player, typeof(ExamineVerb));
totalVerbs.UnionWith(verbs);
// We still need client-exclusive verbs even when the server sends its data in so if that's the case
// we remove any non-client-exclusive verbs.
if (!getVerbs)
{
_verbList.AddRange(totalVerbs);
foreach (var verb in _verbList)
{
if (!verb.ClientExclusive)
{
totalVerbs.Remove(verb);
}
}
_verbList.Clear();
}
if (verbs != null)
{
totalVerbs.UnionWith(verbs);
}
AddVerbsToTooltip(totalVerbs);
}

View File

@@ -348,14 +348,16 @@ namespace Content.Client.Hands.Systems
sprite.LayerSetData(index, layerData);
//Add displacement maps
if (hand.Location == HandLocation.Left && handComp.LeftHandDisplacement is not null)
_displacement.TryAddDisplacement(handComp.LeftHandDisplacement, sprite, index, key, revealedLayers);
else if (hand.Location == HandLocation.Right && handComp.RightHandDisplacement is not null)
_displacement.TryAddDisplacement(handComp.RightHandDisplacement, sprite, index, key, revealedLayers);
//Fallback to default displacement map
else if (handComp.HandDisplacement is not null)
_displacement.TryAddDisplacement(handComp.HandDisplacement, sprite, index, key, revealedLayers);
// Add displacement maps
var displacement = hand.Location switch
{
HandLocation.Left => handComp.LeftHandDisplacement,
HandLocation.Right => handComp.RightHandDisplacement,
_ => handComp.HandDisplacement
};
if (displacement is not null && _displacement.TryAddDisplacement(displacement, sprite, index, key, out var displacementKey))
revealedLayers.Add(displacementKey);
}
RaiseLocalEvent(held, new HeldVisualsUpdatedEvent(uid, revealedLayers), true);

View File

@@ -1,3 +1,4 @@
using Content.Client.DisplacementMap;
using Content.Shared.CCVar;
using Content.Shared.Humanoid;
using Content.Shared.Humanoid.Markings;
@@ -16,6 +17,7 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MarkingManager _markingManager = default!;
[Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly DisplacementMapSystem _displacement = default!;
public override void Initialize()
{
@@ -380,6 +382,11 @@ public sealed class HumanoidAppearanceSystem : SharedHumanoidAppearanceSystem
{
sprite.LayerSetColor(layerId, Color.White);
}
if (humanoid.MarkingsDisplacement.TryGetValue(markingPrototype.BodyPart, out var displacementData) && markingPrototype.CanBeDisplaced)
{
_displacement.TryAddDisplacement(displacementData, sprite, targetLayer + j + 1, layerId, out _);
}
}
}

View File

@@ -3,7 +3,7 @@
xmlns:info="clr-namespace:Content.Client.Info"
VerticalExpand="True" HorizontalExpand="True"
MouseFilter="Stop">
<parallax:ParallaxControl />
<parallax:ParallaxControl SpeedX="20"/>
<Control VerticalExpand="True"
MaxWidth="800"
MaxHeight="900">

View File

@@ -1,15 +0,0 @@
using Content.Shared.Hands;
using Content.Shared.Item;
namespace Content.Client.Items.Systems;
public sealed class MultiHandedItemSystem : SharedMultiHandedItemSystem
{
protected override void OnEquipped(EntityUid uid, MultiHandedItemComponent component, GotEquippedHandEvent args)
{
}
protected override void OnUnequipped(EntityUid uid, MultiHandedItemComponent component, GotUnequippedHandEvent args)
{
}
}

View File

@@ -1,7 +0,0 @@
using Content.Shared.Labels.EntitySystems;
namespace Content.Client.Labels;
public sealed partial class LabelSystem : SharedLabelSystem
{
}

View File

@@ -127,12 +127,17 @@
HorizontalExpand="True"
Orientation="Vertical">
<Label Text="{Loc 'lathe-menu-materials-title'}" Margin="5 5 5 5" HorizontalAlignment="Center"/>
<BoxContainer
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
</BoxContainer>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<ui:MaterialStorageControl Name="MaterialsList" SizeFlagsStretchRatio="8"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>

View File

@@ -3,7 +3,7 @@
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:parallax="clr-namespace:Content.Client.Parallax"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls">
<parallax:ParallaxControl />
<parallax:ParallaxControl SpeedX="20"/>
<Control HorizontalAlignment="Center" VerticalAlignment="Center">
<PanelContainer StyleClasses="AngleRect" />
<BoxContainer Orientation="Vertical" MinSize="300 200">

View File

@@ -1,7 +1,12 @@
<Control xmlns="https://spacestation14.io"
xmlns:pllax="clr-namespace:Content.Client.Parallax"
xmlns:clog="clr-namespace:Content.Client.Changelog">
<pllax:ParallaxControl />
<pllax:ParallaxControl
ParallaxPrototype="TrainStation"
SpeedX="5"
SpeedY="20"
ScaleX="3"
ScaleY="3"/>
<BoxContainer Name="VBox"
Orientation="Vertical"
HorizontalAlignment="Center"

View File

@@ -0,0 +1,6 @@
using Content.Shared.Materials.OreSilo;
namespace Content.Client.Materials;
/// <inheritdoc/>
public sealed class OreSiloSystem : SharedOreSiloSystem;

View File

@@ -2,7 +2,10 @@
SizeFlagsStretchRatio="8"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="MaterialList" Orientation="Vertical">
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" Align="Center"/>
<BoxContainer Orientation="Vertical" VerticalExpand="True">
<BoxContainer Name="MaterialList" Orientation="Vertical" VerticalExpand="True">
<Label Name="NoMatsLabel" Text="{Loc 'lathe-menu-no-materials-message'}" HorizontalAlignment="Center" VerticalAlignment="Center" VerticalExpand="True"/>
</BoxContainer>
<Label Name="SiloLinkedLabel" Text="{Loc 'lathe-menu-silo-linked-message'}" StyleClasses="LabelSubText" Visible="False" HorizontalAlignment="Center"/>
</BoxContainer>
</ScrollContainer>

View File

@@ -1,8 +1,10 @@
using System.Linq;
using Content.Shared.Materials;
using Content.Shared.Materials.OreSilo;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Client.Materials.UI;
@@ -14,15 +16,18 @@ namespace Content.Client.Materials.UI;
public sealed partial class MaterialStorageControl : ScrollContainer
{
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly MaterialStorageSystem _materialStorage;
private EntityUid? _owner;
private Dictionary<string, int> _currentMaterials = new();
private Dictionary<ProtoId<MaterialPrototype>, int> _currentMaterials = new();
public MaterialStorageControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_materialStorage = _entityManager.System<MaterialStorageSystem>();
}
public void SetOwner(EntityUid owner)
@@ -44,7 +49,8 @@ public sealed partial class MaterialStorageControl : ScrollContainer
}
var canEject = materialStorage.CanEjectStoredMaterials;
var mats = materialStorage.Storage.Select(pair => (pair.Key.Id, pair.Value)).ToDictionary();
var mats = _materialStorage.GetStoredMaterials((_owner.Value, materialStorage));
if (_currentMaterials.Equals(mats))
return;
@@ -88,5 +94,6 @@ public sealed partial class MaterialStorageControl : ScrollContainer
_currentMaterials = mats;
NoMatsLabel.Visible = MaterialList.ChildCount == 1;
SiloLinkedLabel.Visible = _entityManager.TryGetComponent<OreSiloClientComponent>(_owner.Value, out var client) && client.Silo != null;
}
}

View File

@@ -0,0 +1,34 @@
using Content.Shared.Materials.OreSilo;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
namespace Content.Client.Materials.UI;
[UsedImplicitly]
public sealed class OreSiloBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private OreSiloMenu? _menu;
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<OreSiloMenu>();
_menu.SetEntity(Owner);
_menu.OnClientEntryPressed += netEnt =>
{
SendPredictedMessage(new ToggleOreSiloClientMessage(netEnt));
};
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not OreSiloBuiState msg)
return;
_menu?.Update(msg);
}
}

View File

@@ -0,0 +1,42 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
xmlns:ui="clr-namespace:Content.Client.Materials.UI"
Title="{Loc 'ore-silo-ui-title'}"
MinSize="400 260"
SetSize="400 460">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
Margin="10 10 10 10">
<BoxContainer VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
SizeFlagsStretchRatio="3">
<Label Text="{Loc 'ore-silo-ui-label-clients'}" Margin="5 5 5 5" HorizontalAlignment="Center" StyleClasses="LabelKeyText"/>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<ItemList Name="ClientList" SelectMode="Button" VerticalExpand="True"/>
</PanelContainer>
</BoxContainer>
<BoxContainer VerticalExpand="True"
HorizontalExpand="True"
Orientation="Vertical"
SizeFlagsStretchRatio="2">
<Label Text="{Loc 'ore-silo-ui-label-mats'}" Margin="5 5 5 5" HorizontalAlignment="Center" StyleClasses="LabelKeyText"/>
<PanelContainer VerticalExpand="True">
<PanelContainer.PanelOverride>
<graphics:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<BoxContainer
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
<ui:MaterialStorageControl Name="Materials"/>
</BoxContainer>
</PanelContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,64 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Materials.OreSilo;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Materials.UI;
[GenerateTypedNameReferences]
public sealed partial class OreSiloMenu : FancyWindow
{
public event Action<NetEntity>? OnClientEntryPressed;
public OreSiloMenu()
{
RobustXamlLoader.Load(this);
ClientList.OnItemSelected += args =>
{
var item = ClientList[args.ItemIndex];
// a little bit of null suppression makes me feel great! :-)
OnClientEntryPressed?.Invoke((NetEntity) item.Metadata!);
};
}
public void SetEntity(EntityUid uid)
{
Materials.SetOwner(uid);
}
public void Update(OreSiloBuiState state)
{
var items = new List<ItemList.Item>();
var orderedClients = state.Clients.OrderBy(t => t.Item3).ThenBy(t => t.Item1.Id);
foreach (var (ent, _, _) in orderedClients)
{
items.Add(new ItemList.Item(ClientList)
{
Metadata = ent
});
}
ClientList.SetItems(items,
(item1, item2) =>
{
var ent1 = (NetEntity) item1.Metadata!;
var ent2 = (NetEntity) item2.Metadata!;
return ent1.CompareTo(ent2);
});
var entTextDict = state.Clients.Select(t => (t.Item1, t.Item2)).ToDictionary();
using var enumerator = ClientList.GetEnumerator();
while (enumerator.MoveNext())
{
if (enumerator.Current.Metadata is not NetEntity ent)
continue;
if (entTextDict.TryGetValue(ent, out var text))
enumerator.Current.Text = text;
}
}
}

View File

@@ -79,10 +79,28 @@ public sealed partial class CrewMonitoringWindow : FancyWindow
NoServerLabel.Visible = false;
// Collect one status per user, using the sensor with the most data available.
Dictionary<NetEntity, SuitSensorStatus> uniqueSensorsMap = new();
foreach (var sensor in sensors)
{
if (uniqueSensorsMap.TryGetValue(sensor.OwnerUid, out var existingSensor))
{
// Skip if we already have a sensor with more data for this mob.
if (existingSensor.Coordinates != null && sensor.Coordinates == null)
continue;
if (existingSensor.DamagePercentage != null && sensor.DamagePercentage == null)
continue;
}
uniqueSensorsMap[sensor.OwnerUid] = sensor;
}
var uniqueSensors = uniqueSensorsMap.Values.ToList();
// Order sensor data
var orderedSensors = sensors.OrderBy(n => n.Name).OrderBy(j => j.Job);
var orderedSensors = uniqueSensors.OrderBy(n => n.Name).OrderBy(j => j.Job);
var assignedSensors = new HashSet<SuitSensorStatus>();
var departments = sensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n);
var departments = uniqueSensors.SelectMany(d => d.JobDepartments).Distinct().OrderBy(n => n);
// Create department labels and populate lists
foreach (var department in departments)

View File

@@ -1,4 +1,3 @@
using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Systems;
namespace Content.Client.NetworkConfigurator.Systems;

View File

@@ -6,18 +6,18 @@
<BoxContainer Orientation="Vertical" Margin="8">
<Label Text="{Loc 'ui-options-admin-player-panel'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="PlayerlistSeparateSymbolsCheckBox" Text="{Loc 'ui-options-admin-playerlist-separate-symbols'}" />
<CheckBox Name="PlayerlistCharacterColorCheckBox" Text="{Loc 'ui-options-admin-playerlist-character-color'}" />
<CheckBox Name="PlayerlistRoleTypeColorCheckBox" Text="{Loc 'ui-options-admin-playerlist-roletype-color'}" />
<ui:OptionDropDown Name="DropDownPlayerTabSymbolSetting" Title="{Loc 'ui-options-admin-player-tab-symbol-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabRoleSetting" Title="{Loc 'ui-options-admin-player-tab-role-setting'}" />
<ui:OptionDropDown Name="DropDownPlayerTabColorSetting" Title="{Loc 'ui-options-admin-player-tab-color-setting'}" />
<Label Text="{Loc 'ui-options-admin-overlay-title'}"
StyleClasses="LabelKeyText"/>
<CheckBox Name="EnableClassicOverlayCheckBox" Text="{Loc 'ui-options-enable-classic-overlay'}" />
<CheckBox Name="EnableOverlaySymbolsCheckBox" Text="{Loc 'ui-options-enable-overlay-symbols'}" />
<CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-enable-overlay-playtime'}" />
<CheckBox Name="EnableOverlayStartingJobCheckBox" Text="{Loc 'ui-options-enable-overlay-starting-job'}" />
<ui:OptionSlider Name="OverlayMergeDistanceSlider" Title="{Loc 'ui-options-overlay-merge-distance'}"/>
<ui:OptionSlider Name="OverlayGhostFadeSlider" Title="{Loc 'ui-options-overlay-ghost-fade-distance'}"/>
<ui:OptionSlider Name="OverlayGhostHideSlider" Title="{Loc 'ui-options-overlay-ghost-hide-distance'}"/>
<ui:OptionDropDown Name="DropDownOverlayAntagFormat" Title="{Loc 'ui-options-admin-overlay-antag-format'}" />
<ui:OptionDropDown Name="DropDownOverlayAntagSymbol" Title="{Loc 'ui-options-admin-overlay-antag-symbol'}" />
<CheckBox Name="EnableOverlayPlaytimeCheckBox" Text="{Loc 'ui-options-admin-enable-overlay-playtime'}" />
<CheckBox Name="EnableOverlayStartingJobCheckBox" Text="{Loc 'ui-options-admin-enable-overlay-starting-job'}" />
<ui:OptionSlider Name="OverlayMergeDistanceSlider" Title="{Loc 'ui-options-admin-overlay-merge-distance'}"/>
<ui:OptionSlider Name="OverlayGhostFadeSlider" Title="{Loc 'ui-options-admin-overlay-ghost-fade-distance'}"/>
<ui:OptionSlider Name="OverlayGhostHideSlider" Title="{Loc 'ui-options-admin-overlay-ghost-hide-distance'}"/>
</BoxContainer>
</ScrollContainer>
<ui:OptionsTabControlRow Name="Control" Access="Public" />

View File

@@ -1,3 +1,5 @@
using Content.Client.Administration;
using Content.Client.Administration.UI.Tabs.PlayerTab;
using Content.Shared.CCVar;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
@@ -19,12 +21,43 @@ public sealed partial class AdminOptionsTab : Control
{
RobustXamlLoader.Load(this);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistSeparateSymbols, PlayerlistSeparateSymbolsCheckBox);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistHighlightedCharacterColor, PlayerlistCharacterColorCheckBox);
Control.AddOptionCheckBox(CCVars.AdminPlayerlistRoleTypeColor, PlayerlistRoleTypeColorCheckBox);
var antagFormats = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var format in Enum.GetValues(typeof(AdminOverlayAntagFormat)))
{
antagFormats.Add(new OptionDropDownCVar<string>.ValueOption(format.ToString()!, Loc.GetString($"ui-options-admin-overlay-antag-format-{format.ToString()!.ToLower()}")));
}
var antagSymbolStyles = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var symbol in Enum.GetValues(typeof(AdminOverlayAntagSymbolStyle)))
{
antagSymbolStyles.Add(new OptionDropDownCVar<string>.ValueOption(symbol.ToString()!, Loc.GetString($"ui-options-admin-overlay-antag-symbol-{symbol.ToString()!.ToLower()}")));
}
var playerTabColorSettings = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var setting in Enum.GetValues(typeof(AdminPlayerTabColorOption)))
{
playerTabColorSettings.Add(new OptionDropDownCVar<string>.ValueOption(setting.ToString()!, Loc.GetString($"ui-options-admin-player-tab-color-setting-{setting.ToString()!.ToLower()}")));
}
var playerTabRoleSettings = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var setting in Enum.GetValues(typeof(AdminPlayerTabRoleTypeOption)))
{
playerTabRoleSettings.Add(new OptionDropDownCVar<string>.ValueOption(setting.ToString()!, Loc.GetString($"ui-options-admin-player-tab-role-setting-{setting.ToString()!.ToLower()}")));
}
var playerTabSymbolSettings = new List<OptionDropDownCVar<string>.ValueOption>();
foreach (var setting in Enum.GetValues(typeof(AdminPlayerTabSymbolOption)))
{
playerTabSymbolSettings.Add(new OptionDropDownCVar<string>.ValueOption(setting.ToString()!, Loc.GetString($"ui-options-admin-player-tab-symbol-setting-{setting.ToString()!.ToLower()}")));
}
Control.AddOptionDropDown(CCVars.AdminPlayerTabSymbolSetting, DropDownPlayerTabSymbolSetting, playerTabSymbolSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabRoleSetting, DropDownPlayerTabRoleSetting, playerTabRoleSettings);
Control.AddOptionDropDown(CCVars.AdminPlayerTabColorSetting, DropDownPlayerTabColorSetting, playerTabColorSettings);
Control.AddOptionDropDown(CCVars.AdminOverlayAntagFormat, DropDownOverlayAntagFormat, antagFormats);
Control.AddOptionDropDown(CCVars.AdminOverlaySymbolStyle, DropDownOverlayAntagSymbol, antagSymbolStyles);
Control.AddOptionCheckBox(CCVars.AdminOverlayClassic, EnableClassicOverlayCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlaySymbols, EnableOverlaySymbolsCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlayPlaytime, EnableOverlayPlaytimeCheckBox);
Control.AddOptionCheckBox(CCVars.AdminOverlayStartingJob, EnableOverlayStartingJobCheckBox);

View File

@@ -7,40 +7,21 @@ using Robust.Shared.Timing;
namespace Content.Client.PDA.Ringer
{
[UsedImplicitly]
public sealed class RingerBoundUserInterface : BoundUserInterface
public sealed class RingerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private RingtoneMenu? _menu;
public RingerBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<RingtoneMenu>();
_menu.OpenToLeft();
_menu.TestRingerButton.OnPressed += _ =>
{
SendMessage(new RingerPlayRingtoneMessage());
};
_menu.TestRingtoneButtonPressed += OnTestRingtoneButtonPressed;
_menu.SetRingtoneButtonPressed += OnSetRingtoneButtonPressed;
_menu.SetRingerButton.OnPressed += _ =>
{
if (!TryGetRingtone(out var ringtone))
return;
SendMessage(new RingerSetRingtoneMessage(ringtone));
_menu.SetRingerButton.Disabled = true;
Timer.Spawn(333, () =>
{
if (_menu is { Disposed: false, SetRingerButton: { Disposed: false } ringer})
ringer.Disabled = false;
});
};
Update();
}
private bool TryGetRingtone(out Note[] ringtone)
@@ -63,36 +44,59 @@ namespace Content.Client.PDA.Ringer
return true;
}
protected override void UpdateState(BoundUserInterfaceState state)
public override void Update()
{
base.UpdateState(state);
base.Update();
if (_menu == null || state is not RingerUpdateState msg)
if (_menu == null)
return;
for (int i = 0; i < _menu.RingerNoteInputs.Length; i++)
if (!EntMan.TryGetComponent(Owner, out RingerComponent? ringer))
return;
for (var i = 0; i < _menu.RingerNoteInputs.Length; i++)
{
var note = ringer.Ringtone[i].ToString();
var note = msg.Ringtone[i].ToString();
if (RingtoneMenu.IsNote(note))
{
_menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
_menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
}
if (!RingtoneMenu.IsNote(note))
continue;
_menu.PreviousNoteInputs[i] = note.Replace("sharp", "#");
_menu.RingerNoteInputs[i].Text = _menu.PreviousNoteInputs[i];
}
_menu.TestRingerButton.Disabled = msg.IsPlaying;
_menu.TestRingerButton.Disabled = ringer.Active;
}
protected override void Dispose(bool disposing)
private void OnTestRingtoneButtonPressed()
{
base.Dispose(disposing);
if (!disposing)
if (_menu is null)
return;
_menu?.Dispose();
SendPredictedMessage(new RingerPlayRingtoneMessage());
// We disable it instantly to remove the delay before the client receives the next compstate
// Makes the UI feel responsive, will be re-enabled by ringer.Active once it gets an update.
_menu.TestRingerButton.Disabled = true;
}
private void OnSetRingtoneButtonPressed()
{
if (_menu is null)
return;
if (!TryGetRingtone(out var ringtone))
return;
SendPredictedMessage(new RingerSetRingtoneMessage(ringtone));
_menu.SetRingerButton.Disabled = true;
Timer.Spawn(333,
() =>
{
if (_menu is { Disposed: false, SetRingerButton: { Disposed: false } ringer} )
ringer.Disabled = false;
});
}
}
}

View File

@@ -0,0 +1,36 @@
using Content.Shared.PDA;
using Content.Shared.PDA.Ringer;
using Content.Shared.Store.Components;
namespace Content.Client.PDA.Ringer;
/// <summary>
/// Handles the client-side logic for <see cref="SharedRingerSystem"/>.
/// </summary>
public sealed class RingerSystem : SharedRingerSystem
{
/// <inheritdoc/>
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<RingerComponent, AfterAutoHandleStateEvent>(OnRingerUpdate);
}
/// <summary>
/// Updates the UI whenever we get a new component state from the server.
/// </summary>
private void OnRingerUpdate(Entity<RingerComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateRingerUi(ent);
}
/// <inheritdoc/>
protected override void UpdateRingerUi(Entity<RingerComponent> ent)
{
if (UI.TryGetOpenUi(ent.Owner, RingerUiKey.Key, out var bui))
{
bui.Update();
}
}
}

View File

@@ -1,7 +1,8 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc 'comp-ringer-ui-menu-title'}"
MinSize="320 128"
SetSize="320 128">
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Title="{Loc 'comp-ringer-ui-menu-title'}"
MinSize="320 100"
SetSize="320 100">
<BoxContainer Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True"
@@ -90,4 +91,4 @@
</BoxContainer>
</PanelContainer>
</BoxContainer>
</DefaultWindow>
</controls:FancyWindow>

View File

@@ -1,6 +1,7 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Content.Shared.PDA;
using Robust.Client.UserInterface.Controls;
@@ -8,49 +9,68 @@ using Robust.Client.UserInterface.Controls;
namespace Content.Client.PDA.Ringer
{
[GenerateTypedNameReferences]
public sealed partial class RingtoneMenu : DefaultWindow
public sealed partial class RingtoneMenu : FancyWindow
{
public string[] PreviousNoteInputs = new[] { "A", "A", "A", "A", "A", "A" };
public LineEdit[] RingerNoteInputs = default!;
public LineEdit[] RingerNoteInputs;
public event Action? SetRingtoneButtonPressed;
public event Action? TestRingtoneButtonPressed;
public RingtoneMenu()
{
RobustXamlLoader.Load(this);
SetRingerButton.OnPressed += _ => SetRingtoneButtonPressed?.Invoke();
TestRingerButton.OnPressed += _ => TestRingtoneButtonPressed?.Invoke();
RingerNoteInputs = new[] { RingerNoteOneInput, RingerNoteTwoInput, RingerNoteThreeInput, RingerNoteFourInput, RingerNoteFiveInput, RingerNoteSixInput };
for (var i = 0; i < RingerNoteInputs.Length; ++i)
{
var input = RingerNoteInputs[i];
var index = i;
var foo = () => // Prevents unauthorized characters from being entered into the LineEdit
{
input.Text = input.Text.ToUpper();
input.OnTextChanged += args =>
{
if (input.Text.Length <= 0)
return;
input.Text = args.Text.ToUpper();
var isValid = IsNote(input.Text);
if (!isValid)
{
input.Text = PreviousNoteInputs[index];
input.AddStyleClass("Caution");
}
else
{
PreviousNoteInputs[index] = input.Text;
input.RemoveStyleClass("Caution");
}
input.CursorPosition = input.Text.Length;
};
input.OnFocusExit += _ =>
{
if (!IsNote(input.Text))
{
input.Text = PreviousNoteInputs[index];
input.RemoveStyleClass("Caution");
}
else
PreviousNoteInputs[index] = input.Text;
input.RemoveStyleClass("Caution");
};
input.OnFocusExit += _ => foo();
input.OnTextEntered += _ =>
{
foo();
input.CursorPosition = input.Text.Length; // Resets caret position to the end of the typed input
};
input.OnTextChanged += _ =>
{
input.Text = input.Text.ToUpper();
if (!IsNote(input.Text))
input.AddStyleClass("Caution");
else
{
input.Text = PreviousNoteInputs[index];
input.RemoveStyleClass("Caution");
}
input.CursorPosition = input.Text.Length;
};
}
}
@@ -66,6 +86,9 @@ namespace Content.Client.PDA.Ringer
/// </summary>
public static bool IsNote(string input)
{
if (input.Any(char.IsDigit))
return false;
input = input.Replace("#", "sharp");
return Enum.TryParse(input, true, out Note _);

View File

@@ -1,4 +1,4 @@
using System.Numerics;
using System.Numerics;
using Content.Client.Parallax.Data;
using Content.Client.Parallax.Managers;
using Robust.Client.Graphics;

View File

@@ -131,7 +131,7 @@ namespace Content.Client.Parallax
{
private readonly Color InnerColor = Color.White;
private readonly Color OuterColor = Color.Black;
private readonly NoiseGenerator.NoiseType NoiseType = NoiseGenerator.NoiseType.Fbm;
private readonly FastNoiseLite.FractalType NoiseType = FastNoiseLite.FractalType.FBm;
private readonly uint Seed = 1234;
private readonly float Persistence = 0.5f;
private readonly float Lacunarity = (float) (Math.PI / 3);
@@ -204,10 +204,10 @@ namespace Content.Client.Parallax
switch (((TomlValue<string>) tomlObject).Value)
{
case "fbm":
NoiseType = NoiseGenerator.NoiseType.Fbm;
NoiseType = FastNoiseLite.FractalType.FBm;
break;
case "ridged":
NoiseType = NoiseGenerator.NoiseType.Ridged;
NoiseType = FastNoiseLite.FractalType.Ridged;
break;
default:
throw new InvalidOperationException();
@@ -217,14 +217,11 @@ namespace Content.Client.Parallax
public override void Apply(Image<Rgba32> bitmap)
{
var noise = new NoiseGenerator(NoiseType);
noise.SetSeed(Seed);
var noise = new FastNoiseLite((int)Seed);
noise.SetFractalType(NoiseType);
noise.SetFrequency(Frequency);
noise.SetPersistence(Persistence);
noise.SetLacunarity(Lacunarity);
noise.SetOctaves(Octaves);
noise.SetPeriodX(bitmap.Width);
noise.SetPeriodY(bitmap.Height);
noise.SetFractalLacunarity(Lacunarity);
noise.SetFractalOctaves((int)Octaves);
var threshVal = 1 / (1 - Threshold);
var powFactor = 1 / Power;
@@ -268,7 +265,7 @@ namespace Content.Client.Parallax
// Noise mask stuff.
private readonly bool Masked;
private readonly NoiseGenerator.NoiseType MaskNoiseType = NoiseGenerator.NoiseType.Fbm;
private readonly FastNoiseLite.FractalType MaskNoiseType = FastNoiseLite.FractalType.FBm;
private readonly uint MaskSeed = 1234;
private readonly float MaskPersistence = 0.5f;
private readonly float MaskLacunarity = (float) (Math.PI * 2 / 3);
@@ -357,10 +354,10 @@ namespace Content.Client.Parallax
switch (((TomlValue<string>) tomlObject).Value)
{
case "fbm":
MaskNoiseType = NoiseGenerator.NoiseType.Fbm;
MaskNoiseType = FastNoiseLite.FractalType.FBm;
break;
case "ridged":
MaskNoiseType = NoiseGenerator.NoiseType.Ridged;
MaskNoiseType = FastNoiseLite.FractalType.Ridged;
break;
default:
throw new InvalidOperationException();
@@ -439,14 +436,10 @@ namespace Content.Client.Parallax
{
var o = PointSize - 1;
var random = new Random(Seed);
var noise = new NoiseGenerator(MaskNoiseType);
noise.SetSeed(MaskSeed);
noise.SetFrequency(MaskFrequency);
noise.SetPersistence(MaskPersistence);
noise.SetLacunarity(MaskLacunarity);
noise.SetOctaves(MaskOctaves);
noise.SetPeriodX(buffer.Width);
noise.SetPeriodY(buffer.Height);
var noise = new FastNoiseLite((int)MaskSeed);
noise.SetFractalType(MaskNoiseType);
noise.SetFractalLacunarity(MaskLacunarity);
noise.SetFractalOctaves((int)MaskOctaves);
var threshVal = 1 / (1 - MaskThreshold);
var powFactor = 1 / MaskPower;

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