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

@@ -12,6 +12,7 @@ using Content.Client.UserInterface.Systems.Actions.Widgets;
using Content.Client.UserInterface.Systems.Actions.Windows;
using Content.Client.UserInterface.Systems.Gameplay;
using Content.Shared.Actions;
using Content.Shared.Charges.Systems;
using Content.Shared.Input;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
@@ -42,7 +43,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
[Dependency] private readonly IOverlayManager _overlays = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntityManager _entMan = default!;
[Dependency] private readonly IInputManager _input = default!;
[UISystemDependency] private readonly ActionsSystem? _actionsSystem = default;
@@ -173,7 +173,6 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
// Is the action currently valid?
if (!action.Enabled
|| action is { Charges: 0, RenewCharges: false }
|| action.Cooldown.HasValue && action.Cooldown.Value.End > _timing.CurTime)
{
// The user is targeting with this action, but it is not valid. Maybe mark this click as
@@ -483,7 +482,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
continue;
}
var button = new ActionButton(_entMan, _spriteSystem, this) {Locked = true};
var button = new ActionButton(EntityManager, _spriteSystem, this) {Locked = true};
button.ActionPressed += OnWindowActionPressed;
button.ActionUnpressed += OnWindowActionUnPressed;
button.ActionFocusExited += OnWindowActionFocusExisted;
@@ -632,8 +631,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
return;
_menuDragHelper.MouseDown(action);
args.Handle();
HandleActionPressed(args, action);
}
private void OnWindowActionUnPressed(GUIBoundKeyEventArgs args, ActionButton dragged)
@@ -641,8 +639,7 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick && args.Function != EngineKeyFunctions.Use)
return;
DragAction();
args.Handle();
HandleActionUnpressed(args, dragged);
}
private void OnWindowActionFocusExisted(ActionButton button)
@@ -662,6 +659,11 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
if (args.Function != EngineKeyFunctions.UIClick)
return;
HandleActionPressed(args, button);
}
private void HandleActionPressed(GUIBoundKeyEventArgs args, ActionButton button)
{
args.Handle();
if (button.ActionId != null)
{
@@ -677,7 +679,15 @@ public sealed class ActionUIController : UIController, IOnStateChanged<GameplayS
private void OnActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (args.Function != EngineKeyFunctions.UIClick || _actionsSystem == null)
if (args.Function != EngineKeyFunctions.UIClick)
return;
HandleActionUnpressed(args, button);
}
private void HandleActionUnpressed(GUIBoundKeyEventArgs args, ActionButton button)
{
if (_actionsSystem == null)
return;
args.Handle();

View File

@@ -4,6 +4,8 @@ using Content.Client.Actions.UI;
using Content.Client.Cooldown;
using Content.Client.Stylesheets;
using Content.Shared.Actions;
using Content.Shared.Charges.Components;
using Content.Shared.Charges.Systems;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
@@ -22,6 +24,7 @@ public sealed class ActionButton : Control, IEntityControl
private IEntityManager _entities;
private SpriteSystem? _spriteSys;
private ActionUIController? _controller;
private SharedChargesSystem _sharedChargesSys;
private bool _beingHovered;
private bool _depressed;
private bool _toggled;
@@ -65,6 +68,7 @@ public sealed class ActionButton : Control, IEntityControl
_entities = entities;
_spriteSys = spriteSys;
_sharedChargesSys = _entities.System<SharedChargesSystem>();
_controller = controller;
MouseFilter = MouseFilterMode.Pass;
@@ -194,14 +198,22 @@ public sealed class ActionButton : Control, IEntityControl
var name = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityName));
var decr = FormattedMessage.FromMarkupPermissive(Loc.GetString(metadata.EntityDescription));
FormattedMessage? chargesText = null;
if (_action is { Charges: not null })
// TODO: Don't touch this use an event make callers able to add their own shit for actions or I kill you.
if (_entities.TryGetComponent(ActionId, out LimitedChargesComponent? actionCharges))
{
var charges = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {_action.Charges.Value.ToString()}/{_action.MaxCharges.ToString()}"));
return new ActionAlertTooltip(name, decr, charges: charges);
var charges = _sharedChargesSys.GetCurrentCharges((ActionId.Value, actionCharges, null));
chargesText = FormattedMessage.FromMarkupPermissive(Loc.GetString($"Charges: {charges.ToString()}/{actionCharges.MaxCharges}"));
if (_entities.TryGetComponent(ActionId, out AutoRechargeComponent? autoRecharge))
{
var chargeTimeRemaining = _sharedChargesSys.GetNextRechargeTime((ActionId.Value, actionCharges, autoRecharge));
chargesText.AddText(Loc.GetString($"{Environment.NewLine}Time Til Recharge: {chargeTimeRemaining}"));
}
}
return new ActionAlertTooltip(name, decr);
return new ActionAlertTooltip(name, decr, charges: chargesText);
}
protected override void ControlFocusExited()

View File

@@ -221,18 +221,11 @@ public sealed class CharacterUIController : UIController, IOnStateEntered<Gamepl
if (!_ent.TryGetComponent<MindComponent>(container.Mind.Value, out var mind))
return;
var roleText = Loc.GetString("role-type-crew-aligned-name");
var color = Color.White;
if (_prototypeManager.TryIndex(mind.RoleType, out var proto))
{
roleText = Loc.GetString(proto.Name);
color = proto.Color;
}
else
_sawmill.Error($"{_player.LocalEntity} has invalid Role Type '{mind.RoleType}'. Displaying '{roleText}' instead");
if (!_prototypeManager.TryIndex(mind.RoleType, out var proto))
_sawmill.Error($"Player '{_player.LocalSession}' has invalid Role Type '{mind.RoleType}'. Displaying default instead");
_window.RoleType.Text = roleText;
_window.RoleType.FontColorOverride = color;
_window.RoleType.Text = Loc.GetString(proto?.Name ?? "role-type-crew-aligned-name");
_window.RoleType.FontColorOverride = proto?.Color ?? Color.White;
}
private void CharacterDetached(EntityUid uid)

View File

@@ -0,0 +1,7 @@
<controls:FancyWindow xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:widgets="clr-namespace:Content.Client.UserInterface.Systems.Chat.Widgets"
Title="{Loc chat-window-title}"
MinSize="465 265">
<widgets:ChatBox Name="Chatbox"/>
</controls:FancyWindow>

View File

@@ -0,0 +1,44 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.UserInterface.Systems.Chat;
/// <summary>
/// Window which only holds a single chatbox, useful for monitoring multiple chats simultaneously.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class ChatWindow : FancyWindow
{
public ChatWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
// These are necessary for the controls inside the chatbox to initialize correctly:
Chatbox.Repopulate();
var controller = UserInterfaceManager.GetUIController<ChatUIController>();
controller.UpdateSelectedChannel(Chatbox);
}
/// <summary>
/// Helper method to configure this window to be useful for admins.
/// Sets incoming filters to only admin chats and output to admin channel
/// </summary>
public void ConfigureForAdminChat()
{
Chatbox.ChatInput.ChannelSelector.Select(ChatSelectChannel.Admin);
var filter = Chatbox.ChatInput.FilterButton.Popup;
foreach (var c in Enum.GetValues(typeof(ChatChannel)))
{
var channel = (ChatChannel)c;
var isAdminInterest = channel == ChatChannel.Admin
|| channel == ChatChannel.AdminChat
|| channel == ChatChannel.AdminAlert
|| channel == ChatChannel.AdminRelated;
filter.SetActive(channel, isAdminInterest);
}
}
}

View File

@@ -0,0 +1,35 @@
using JetBrains.Annotations;
using Robust.Shared.Console;
namespace Content.Client.UserInterface.Systems.Chat;
/// <summary>
/// Command which creates a window containing a chatbox
/// </summary>
[UsedImplicitly]
public sealed class ChatWindowCommand : LocalizedCommands
{
public override string Command => "chatwindow";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var window = new ChatWindow();
window.OpenCentered();
}
}
/// <summary>
/// Command which creates a window containing a chatbox configured for admin use
/// </summary>
[UsedImplicitly]
public sealed class AdminChatWindowCommand : LocalizedCommands
{
public override string Command => "achatwindow";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
var window = new ChatWindow();
window.ConfigureForAdminChat();
window.OpenCentered();
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Chat;
using Content.Shared.Chat;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@@ -40,6 +40,15 @@ public sealed partial class ChannelFilterPopup : Popup
return _filterStates.TryGetValue(channel, out var checkbox) && checkbox.Pressed;
}
public void SetActive(ChatChannel channel, bool isActive)
{
if (_filterStates.TryGetValue(channel, out var checkbox) && checkbox.Pressed != isActive)
{
checkbox.Pressed = isActive;
OnChannelFilter?.Invoke(checkbox.Channel, checkbox.Pressed);
}
}
public ChatChannel GetActive()
{
ChatChannel active = 0;

View File

@@ -10,7 +10,7 @@
<PanelContainer Name="ChatWindowPanel" Access="Public" HorizontalExpand="True" VerticalExpand="True"
StyleClasses="StyleNano.StyleClassChatPanel">
<BoxContainer Orientation="Vertical" SeparationOverride="4" HorizontalExpand="True" VerticalExpand="True">
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" />
<OutputPanel Name="Contents" HorizontalExpand="True" VerticalExpand="True" Margin="8 8 8 4" ShowScrollDownButton="True" />
<controls:ChatInputBox HorizontalExpand="True" Name="ChatInput" Access="Public" Margin="2"/>
</BoxContainer>
</PanelContainer>

View File

@@ -9,12 +9,32 @@
Orientation="Vertical"
HorizontalAlignment="Center">
<BoxContainer Orientation="Vertical">
<BoxContainer Name="StorageContainer"
<BoxContainer Name="SingleStorageContainer"
Access="Public"
HorizontalAlignment="Center"
HorizontalExpand="True"
Margin="10">
</BoxContainer>
<BoxContainer Name="DoubleStorageContainer"
Access="Public"
HorizontalAlignment="Stretch"
HorizontalExpand="True"
Margin="10">
<BoxContainer Name="LeftStorageContainer"
Access="Public"
HorizontalAlignment="Left"
HorizontalExpand="True"
VerticalAlignment="Bottom"
Margin="10">
</BoxContainer>
<BoxContainer Name="RightStorageContainer"
Access="Public"
HorizontalAlignment="Right"
HorizontalExpand="True"
VerticalAlignment="Bottom"
Margin="10">
</BoxContainer>
</BoxContainer>
<BoxContainer Orientation="Horizontal" Name="Hotbar" HorizontalAlignment="Center">
<inventory:ItemSlotButtonContainer
Name="SecondHotbar"

View File

@@ -38,6 +38,7 @@ public sealed partial class ItemStatusPanel : Control
StyleBox.Margin cutOut;
StyleBox.Margin flat;
Thickness contentMargin;
Thickness patchMargin;
switch (location)
{
@@ -61,15 +62,23 @@ public sealed partial class ItemStatusPanel : Control
Contents.Margin = contentMargin;
//Important to note for patchMargin!
//Because of hand ui flipping, left and right instead correspond to outside and inside respectively.
patchMargin = MarginFromThemeColor("_itemstatus_patch_margin");
var panel = (StyleBoxTexture) Panel.PanelOverride!;
panel.Texture = texture;
panel.SetPatchMargin(flat, 4);
panel.SetPatchMargin(cutOut, 7);
panel.SetPatchMargin(cutOut, patchMargin.Left);
panel.SetPatchMargin(flat, patchMargin.Right);
panel.SetPatchMargin(StyleBox.Margin.Top, patchMargin.Top);
panel.SetPatchMargin(StyleBox.Margin.Bottom, patchMargin.Bottom);
var panelHighlight = (StyleBoxTexture) HighlightPanel.PanelOverride!;
panelHighlight.Texture = textureHighlight;
panelHighlight.SetPatchMargin(flat, 4);
panelHighlight.SetPatchMargin(cutOut, 7);
panelHighlight.SetPatchMargin(cutOut, patchMargin.Left);
panelHighlight.SetPatchMargin(flat, patchMargin.Right);
panelHighlight.SetPatchMargin(StyleBox.Margin.Top, patchMargin.Top);
panelHighlight.SetPatchMargin(StyleBox.Margin.Bottom, patchMargin.Bottom);
_side = location;
}

View File

@@ -163,22 +163,25 @@ public sealed class ItemGridPiece : Control, IEntityControl
}
// typically you'd divide by two, but since the textures are half a tile, this is done implicitly
var iconOffset = Location.Rotation.RotateVec(itemComponent.StoredOffset) * 2 * UIScale;
var iconPosition = new Vector2(
(boundingGrid.Width + 1) * size.X + Location.Rotation.RotateVec(itemComponent.StoredOffset).X * 2,
(boundingGrid.Height + 1) * size.Y + Location.Rotation.RotateVec(itemComponent.StoredOffset).Y * 2);
(boundingGrid.Width + 1) * size.X + iconOffset.X,
(boundingGrid.Height + 1) * size.Y + iconOffset.Y);
var iconRotation = Location.Rotation + Angle.FromDegrees(itemComponent.StoredRotation);
if (itemComponent.StoredSprite is { } storageSprite)
{
var scale = 2 * UIScale;
var offset = (((Box2) boundingGrid).Size - Vector2.One) * size;
var sprite = _entityManager.System<SpriteSystem>().Frame0(storageSprite);
var sizeDifference = ((boundingGrid.Size + Vector2i.One) * _centerTexture.Size * 2 - sprite.Size) * UIScale;
var spriteBox = new Box2Rotated(new Box2(0f, sprite.Height * scale, sprite.Width * scale, 0f), -iconRotation, Vector2.Zero);
var root = spriteBox.CalcBoundingBox().BottomLeft;
var pos = PixelPosition * 2
+ (Parent?.GlobalPixelPosition ?? Vector2.Zero)
+ offset;
+ sizeDifference
+ iconOffset;
handle.SetTransform(pos, iconRotation);
var box = new UIBox2(root, root + sprite.Size * scale);

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Client.Examine;
using Content.Client.Hands.Systems;
@@ -48,6 +49,7 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
public Angle DraggingRotation = Angle.Zero;
public bool StaticStorageUIEnabled;
public bool OpaqueStorageWindow;
private int _openStorageLimit = -1;
public bool IsDragging => _menuDragHelper.IsDragging;
public ItemGridPiece? CurrentlyDragging => _menuDragHelper.Dragged;
@@ -66,6 +68,12 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
_configuration.OnValueChanged(CCVars.StaticStorageUI, OnStaticStorageChanged, true);
_configuration.OnValueChanged(CCVars.OpaqueStorageWindow, OnOpaqueWindowChanged, true);
_configuration.OnValueChanged(CCVars.StorageWindowTitle, OnStorageWindowTitle, true);
_configuration.OnValueChanged(CCVars.StorageLimit, OnStorageLimitChanged, true);
}
private void OnStorageLimitChanged(int obj)
{
_openStorageLimit = obj;
}
private void OnStorageWindowTitle(bool obj)
@@ -99,7 +107,49 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
if (StaticStorageUIEnabled)
{
UIManager.GetActiveUIWidgetOrNull<HotbarGui>()?.StorageContainer.AddChild(window);
var hotbar = UIManager.GetActiveUIWidgetOrNull<HotbarGui>();
// this lambda handles the nested storage case
// during nested storage, a parent window hides and a child window is
// immediately inserted to the end of the list
// we can reorder the newly inserted to the same index as the invisible
// window in order to prevent an invisible window from being replaced
// with a visible one in a different position
Action<Control?, Control> reorder = (parent, child) =>
{
if (parent is null)
return;
var parentChildren = parent.Children.ToList();
var invisibleIndex = parentChildren.FindIndex(c => c.Visible == false);
if (invisibleIndex == -1)
return;
child.SetPositionInParent(invisibleIndex);
};
if (hotbar != null)
{
hotbar.DoubleStorageContainer.Visible = _openStorageLimit == 2;
hotbar.SingleStorageContainer.Visible = _openStorageLimit != 2;
}
if (_openStorageLimit == 2)
{
if (hotbar?.LeftStorageContainer.Children.Any(c => c.Visible) == false) // we're comparing booleans because it's bool? and not bool from the optional chaining
{
hotbar?.LeftStorageContainer.AddChild(window);
reorder(hotbar?.LeftStorageContainer, window);
}
else
{
hotbar?.RightStorageContainer.AddChild(window);
reorder(hotbar?.RightStorageContainer, window);
}
}
else
{
hotbar?.SingleStorageContainer.AddChild(window);
reorder(hotbar?.SingleStorageContainer, window);
}
_closeRecentWindowUIController.SetMostRecentlyInteractedWindow(window);
}
else
@@ -269,12 +319,19 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
var position = targetStorage.GetMouseGridPieceLocation(dragEnt, dragLoc);
var newLocation = new ItemStorageLocation(DraggingRotation, position);
EntityManager.RaisePredictiveEvent(new StorageSetItemLocationEvent(
EntityManager.GetNetEntity(draggingGhost.Entity),
EntityManager.GetNetEntity(sourceStorage),
newLocation));
if (!_storage.ItemFitsInGridLocation(dragEnt, sourceStorage, newLocation))
{
window.Reclaim(control.Location, control);
}
else
{
EntityManager.RaisePredictiveEvent(new StorageSetItemLocationEvent(
EntityManager.GetNetEntity(draggingGhost.Entity),
EntityManager.GetNetEntity(sourceStorage),
newLocation));
window.Reclaim(newLocation, control);
window.Reclaim(newLocation, control);
}
}
// Dragging to new storage
else if (targetStorage?.StorageEntity != null && targetStorage != window)
@@ -336,6 +393,17 @@ public sealed class StorageUIController : UIController, IOnSystemChanged<Storage
if (DraggingGhost == null)
return false;
var player = _player.LocalEntity;
// If the attached storage is closed then stop dragging
if (player == null ||
!_storage.TryGetStorageLocation(DraggingGhost.Entity, out var container, out _, out _) ||
!_ui.IsUiOpen(container.Owner, StorageComponent.StorageUiKey.Key, player.Value))
{
DraggingGhost.Orphan();
return false;
}
SetDraggingRotation();
return true;
}