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

# Conflicts:
#	Content.Server/Chat/Managers/ChatSanitizationManager.cs
#	Content.Shared/Damage/Systems/SharedStaminaSystem.cs
#	Content.Shared/Eye/VisibilityFlags.cs
#	Content.Shared/Lock/LockSystem.cs
#	Content.Shared/StatusEffectNew/StatusEffectSystem.Relay.cs
#	Resources/Prototypes/Recipes/Reactions/chemicals.yml
#	Tools/actions_changelogs_since_last_run.py
This commit is contained in:
Ed
2025-08-04 12:44:29 +03:00
507 changed files with 12042 additions and 3123 deletions

View File

@@ -334,7 +334,12 @@ namespace Content.Client.Actions
private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args) private void OnEntityTargetAttempt(Entity<EntityTargetActionComponent> ent, ref ActionTargetAttemptEvent args)
{ {
if (args.Handled || args.Input.EntityUid is not { Valid: true } entity) if (args.Handled)
return;
args.Handled = true;
if (args.Input.EntityUid is not { Valid: true } entity)
return; return;
// let world target component handle it // let world target component handle it
@@ -345,8 +350,6 @@ namespace Content.Client.Actions
return; return;
} }
args.Handled = true;
var action = args.Action; var action = args.Action;
var user = args.User; var user = args.User;

View File

@@ -0,0 +1,20 @@
<Control
xmlns="https://spacestation14.io"
xmlns:viewport="clr-namespace:Content.Client.Viewport"
MouseFilter="Stop">
<PanelContainer StyleClasses="BackgroundDark" Name="AdminCameraWindowRoot" Access="Public">
<BoxContainer Orientation="Vertical" Access="Public">
<!-- Camera -->
<Control VerticalExpand="True" Name="CameraViewBox">
<viewport:ScalingViewport Name="CameraView"
MinSize="100 100"
MouseFilter="Ignore" />
</Control>
<!-- Controller buttons -->
<BoxContainer Orientation="Horizontal" Margin="5 5 5 5">
<Button StyleClasses="OpenRight" Name="FollowButton" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-follow'}" />
<Button StyleClasses="OpenLeft" Name="PopControl" HorizontalExpand="True" Access="Public" Text="{Loc 'admin-camera-window-pop-out'}" />
</BoxContainer>
</BoxContainer>
</PanelContainer>
</Control>

View File

@@ -0,0 +1,101 @@
using System.Numerics;
using Content.Client.Eye;
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.Timing;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Administration.UI.AdminCamera;
[GenerateTypedNameReferences]
public sealed partial class AdminCameraControl : Control
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IClientGameTiming _timing = default!;
public event Action? OnFollow;
public event Action? OnPopoutControl;
private readonly EyeLerpingSystem _eyeLerpingSystem;
private readonly FixedEye _defaultEye = new();
private AdminCameraEuiState? _nextState;
private const float MinimumZoom = 0.1f;
private const float MaximumZoom = 2.0f;
public EntityUid? CurrentCamera;
public float Zoom = 1.0f;
public bool IsPoppedOut;
public AdminCameraControl()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_eyeLerpingSystem = _entManager.System<EyeLerpingSystem>();
CameraView.Eye = _defaultEye;
FollowButton.OnPressed += _ => OnFollow?.Invoke();
PopControl.OnPressed += _ => OnPopoutControl?.Invoke();
CameraView.OnResized += OnResized;
}
private new void OnResized()
{
var width = Math.Max(CameraView.PixelWidth, (int)Math.Floor(CameraView.MinWidth));
var height = Math.Max(CameraView.PixelHeight, (int)Math.Floor(CameraView.MinHeight));
CameraView.ViewportSize = new Vector2i(width, height);
}
protected override void MouseWheel(GUIMouseWheelEventArgs args)
{
base.MouseWheel(args);
if (CameraView.Eye == null)
return;
Zoom = Math.Clamp(Zoom - args.Delta.Y * 0.15f * Zoom, MinimumZoom, MaximumZoom);
CameraView.Eye.Zoom = new Vector2(Zoom, Zoom);
args.Handle();
}
public void SetState(AdminCameraEuiState state)
{
_nextState = state;
}
// I know that this is awful, but I copied this from the solution editor anyways.
// This is needed because EUIs update before the gamestate is applied, which means it will fail to get the uid from the net entity.
// The suggestion from the comment in the solution editor saying to use a BUI is not ideal either:
// - We would need to bind the UI to an entity, but with how BUIs currently work we cannot open it in the same tick as we spawn that entity on the server.
// - We want the UI opened by the user session, not by their currently attached entity. Otherwise it would close in cases where admins move from one entity to another, for example when ghosting.
protected override void FrameUpdate(FrameEventArgs args)
{
if (_nextState == null || _timing.LastRealTick < _nextState.Tick) // make sure the last gamestate has been applied
return;
if (!_entManager.TryGetEntity(_nextState.Camera, out var cameraUid))
return;
if (CurrentCamera == null)
{
_eyeLerpingSystem.AddEye(cameraUid.Value);
CurrentCamera = cameraUid;
}
else if (CurrentCamera != cameraUid)
{
_eyeLerpingSystem.RemoveEye(CurrentCamera.Value);
_eyeLerpingSystem.AddEye(cameraUid.Value);
CurrentCamera = cameraUid;
}
if (_entManager.TryGetComponent<EyeComponent>(CurrentCamera, out var eye))
CameraView.Eye = eye.Eye ?? _defaultEye;
}
}

View File

@@ -0,0 +1,117 @@
using System.Numerics;
using Content.Client.Eui;
using Content.Shared.Administration;
using Content.Shared.Eui;
using JetBrains.Annotations;
using Robust.Client.UserInterface.Controls;
namespace Content.Client.Administration.UI.AdminCamera;
/// <summary>
/// Admin Eui for opening a viewport window to observe entities.
/// Use the "Open Camera" admin verb or the "camera" command to open.
/// </summary>
[UsedImplicitly]
public sealed partial class AdminCameraEui : BaseEui
{
private readonly AdminCameraWindow _window;
private readonly AdminCameraControl _control;
// If not null the camera is in "popped out" mode and is in an external window.
private OSWindow? _OSWindow;
// The last location the window was located at in game.
// Is used for getting knowing where to "pop in" external windows.
private Vector2 _lastLocation;
public AdminCameraEui()
{
_window = new AdminCameraWindow();
_control = new AdminCameraControl();
_window.Contents.AddChild(_control);
_control.OnFollow += () => SendMessage(new AdminCameraFollowMessage());
_window.OnClose += () =>
{
if (!_control.IsPoppedOut)
SendMessage(new CloseEuiMessage());
};
_control.OnPopoutControl += () =>
{
if (_control.IsPoppedOut)
PopIn();
else
PopOut();
};
}
// Pop the window out into an external OS window
private void PopOut()
{
_lastLocation = _window.Position;
// TODO: When there is a way to have a minimum window size, enforce something!
_OSWindow = new OSWindow
{
SetSize = _window.Size,
Title = _window.Title ?? Loc.GetString("admin-camera-window-title-placeholder"),
};
_OSWindow.Show();
if (_OSWindow.Root == null)
return;
_control.Orphan();
_OSWindow.Root.AddChild(_control);
_OSWindow.Closed += () =>
{
if (_control.IsPoppedOut)
SendMessage(new CloseEuiMessage());
};
_control.IsPoppedOut = true;
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-in");
_window.Close();
}
// Pop the window back into the in game window.
private void PopIn()
{
_control.Orphan();
_window.Contents.AddChild(_control);
_window.Open(_lastLocation);
_control.IsPoppedOut = false;
_control.PopControl.Text = Loc.GetString("admin-camera-window-pop-out");
_OSWindow?.Close();
_OSWindow = null;
}
public override void Opened()
{
base.Opened();
_window.OpenCentered();
}
public override void Closed()
{
base.Closed();
_window.Close();
}
public override void HandleState(EuiStateBase baseState)
{
if (baseState is not AdminCameraEuiState state)
return;
_window.SetState(state);
_control.SetState(state);
}
}

View File

@@ -0,0 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
Title="{Loc admin-camera-window-title-placeholder}"
SetSize="425 550"
MinSize="200 225"
Name="Window">
</DefaultWindow>

View File

@@ -0,0 +1,23 @@
using Content.Shared.Administration;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Administration.UI.AdminCamera;
[GenerateTypedNameReferences]
public sealed partial class AdminCameraWindow : DefaultWindow
{
public AdminCameraWindow()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
ContentsContainer.Margin = new Thickness(5, 0, 5, 0);
}
public void SetState(AdminCameraEuiState state)
{
Title = Loc.GetString("admin-camera-window-title", ("name", state.Name));
}
}

View File

@@ -31,6 +31,7 @@
<Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/> <Button Name="FreezeAndMuteToggleButton" Text="{Loc player-panel-freeze-and-mute}" Disabled="True"/>
<Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/> <Button Name="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
<controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/> <controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
<Button Name="CameraButton" Text="{Loc player-panel-camera}" Disabled="True"/>
</GridContainer> </GridContainer>
</BoxContainer> </BoxContainer>
</BoxContainer> </BoxContainer>

View File

@@ -18,6 +18,7 @@ public sealed partial class PlayerPanel : FancyWindow
public event Action<NetUserId?>? OnOpenBans; public event Action<NetUserId?>? OnOpenBans;
public event Action<NetUserId?>? OnAhelp; public event Action<NetUserId?>? OnAhelp;
public event Action<string?>? OnKick; public event Action<string?>? OnKick;
public event Action<string?>? OnCamera;
public event Action<NetUserId?>? OnOpenBanPanel; public event Action<NetUserId?>? OnOpenBanPanel;
public event Action<NetUserId?, bool>? OnWhitelistToggle; public event Action<NetUserId?, bool>? OnWhitelistToggle;
public event Action? OnFollow; public event Action? OnFollow;
@@ -33,26 +34,27 @@ public sealed partial class PlayerPanel : FancyWindow
public PlayerPanel(IClientAdminManager adminManager) public PlayerPanel(IClientAdminManager adminManager)
{ {
RobustXamlLoader.Load(this); RobustXamlLoader.Load(this);
_adminManager = adminManager; _adminManager = adminManager;
UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? ""); UsernameCopyButton.OnPressed += _ => OnUsernameCopy?.Invoke(TargetUsername ?? "");
BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer); BanButton.OnPressed += _ => OnOpenBanPanel?.Invoke(TargetPlayer);
KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername); KickButton.OnPressed += _ => OnKick?.Invoke(TargetUsername);
NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer); CameraButton.OnPressed += _ => OnCamera?.Invoke(TargetUsername);
ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer); NotesButton.OnPressed += _ => OnOpenNotes?.Invoke(TargetPlayer);
AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer); ShowBansButton.OnPressed += _ => OnOpenBans?.Invoke(TargetPlayer);
WhitelistToggle.OnPressed += _ => AhelpButton.OnPressed += _ => OnAhelp?.Invoke(TargetPlayer);
{ WhitelistToggle.OnPressed += _ =>
OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted); {
SetWhitelisted(!_isWhitelisted); OnWhitelistToggle?.Invoke(TargetPlayer, _isWhitelisted);
}; SetWhitelisted(!_isWhitelisted);
FollowButton.OnPressed += _ => OnFollow?.Invoke(); };
FreezeButton.OnPressed += _ => OnFreeze?.Invoke(); FollowButton.OnPressed += _ => OnFollow?.Invoke();
FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke(); FreezeButton.OnPressed += _ => OnFreeze?.Invoke();
LogsButton.OnPressed += _ => OnLogs?.Invoke(); FreezeAndMuteToggleButton.OnPressed += _ => OnFreezeAndMuteToggle?.Invoke();
DeleteButton.OnPressed += _ => OnDelete?.Invoke(); LogsButton.OnPressed += _ => OnLogs?.Invoke();
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke(); DeleteButton.OnPressed += _ => OnDelete?.Invoke();
RejuvenateButton.OnPressed += _ => OnRejuvenate?.Invoke();
} }
public void SetUsername(string player) public void SetUsername(string player)
@@ -122,6 +124,7 @@ public sealed partial class PlayerPanel : FancyWindow
{ {
BanButton.Disabled = !_adminManager.CanCommand("banpanel"); BanButton.Disabled = !_adminManager.CanCommand("banpanel");
KickButton.Disabled = !_adminManager.CanCommand("kick"); KickButton.Disabled = !_adminManager.CanCommand("kick");
CameraButton.Disabled = !_adminManager.CanCommand("camera");
NotesButton.Disabled = !_adminManager.CanCommand("adminnotes"); NotesButton.Disabled = !_adminManager.CanCommand("adminnotes");
ShowBansButton.Disabled = !_adminManager.CanCommand("banlist"); ShowBansButton.Disabled = !_adminManager.CanCommand("banlist");
WhitelistToggle.Disabled = WhitelistToggle.Disabled =

View File

@@ -15,7 +15,7 @@ public sealed class PlayerPanelEui : BaseEui
[Dependency] private readonly IClientAdminManager _admin = default!; [Dependency] private readonly IClientAdminManager _admin = default!;
[Dependency] private readonly IClipboardManager _clipboard = default!; [Dependency] private readonly IClipboardManager _clipboard = default!;
private PlayerPanel PlayerPanel { get; } private PlayerPanel PlayerPanel { get; }
public PlayerPanelEui() public PlayerPanelEui()
{ {
@@ -25,6 +25,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\""); PlayerPanel.OnOpenNotes += id => _console.ExecuteCommand($"adminnotes \"{id}\"");
// Kick command does not support GUIDs // Kick command does not support GUIDs
PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\""); PlayerPanel.OnKick += username => _console.ExecuteCommand($"kick \"{username}\"");
PlayerPanel.OnCamera += username => _console.ExecuteCommand($"camera \"{username}\"");
PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\""); PlayerPanel.OnOpenBanPanel += id => _console.ExecuteCommand($"banpanel \"{id}\"");
PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\""); PlayerPanel.OnOpenBans += id => _console.ExecuteCommand($"banlist \"{id}\"");
PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\""); PlayerPanel.OnAhelp += id => _console.ExecuteCommand($"openahelp \"{id}\"");
@@ -37,7 +38,7 @@ public sealed class PlayerPanelEui : BaseEui
PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage()); PlayerPanel.OnFreeze += () => SendMessage(new PlayerPanelFreezeMessage());
PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage()); PlayerPanel.OnLogs += () => SendMessage(new PlayerPanelLogsMessage());
PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage()); PlayerPanel.OnRejuvenate += () => SendMessage(new PlayerPanelRejuvenationMessage());
PlayerPanel.OnDelete+= () => SendMessage(new PlayerPanelDeleteMessage()); PlayerPanel.OnDelete += () => SendMessage(new PlayerPanelDeleteMessage());
PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage()); PlayerPanel.OnFollow += () => SendMessage(new PlayerPanelFollowMessage());
PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage()); PlayerPanel.OnClose += () => SendMessage(new CloseEuiMessage());

View File

@@ -162,10 +162,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
{ {
var list = new List<AtmosMonitoringConsoleLine>(); var list = new List<AtmosMonitoringConsoleLine>();
foreach (var ((netId, layer, hexColor), atmosPipeData) in chunk.AtmosPipeData) foreach (var ((netId, layer, pipeColor), atmosPipeData) in chunk.AtmosPipeData)
{ {
// Determine the correct coloration for the pipe // Determine the correct coloration for the pipe
var color = Color.FromHex(hexColor) * _basePipeNetColor; var color = pipeColor * _basePipeNetColor;
if (FocusNetId != null && FocusNetId != netId) if (FocusNetId != null && FocusNetId != netId)
color *= _unfocusedPipeNetColor; color *= _unfocusedPipeNetColor;

View File

@@ -1,11 +1,46 @@
using Content.Client.Atmos.Components; using Content.Client.Atmos.Components;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Shared.Atmos.Piping; using Content.Shared.Atmos.Piping;
using Content.Shared.Hands;
using Content.Shared.Atmos.Components;
using Content.Shared.Item;
namespace Content.Client.Atmos.EntitySystems; namespace Content.Client.Atmos.EntitySystems;
public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisualsComponent> public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisualsComponent>
{ {
[Dependency] private readonly SharedItemSystem _itemSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<PipeColorVisualsComponent, GetInhandVisualsEvent>(OnGetVisuals);
SubscribeLocalEvent<PipeColorVisualsComponent, BeforeRenderInGridEvent>(OnDrawInGrid);
}
/// <summary>
/// This method is used to display the color changes of the pipe on the screen..
/// </summary>
private void OnGetVisuals(Entity<PipeColorVisualsComponent> item, ref GetInhandVisualsEvent args)
{
foreach (var (_, layerData) in args.Layers)
{
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
layerData.Color = pipeColor.Color;
}
}
/// <summary>
/// This method is used to change the pipe's color in a container grid.
/// </summary>
private void OnDrawInGrid(Entity<PipeColorVisualsComponent> item, ref BeforeRenderInGridEvent args)
{
if (TryComp(item.Owner, out AtmosPipeColorComponent? pipeColor))
args.Color = pipeColor.Color;
}
protected override void OnAppearanceChange(EntityUid uid, PipeColorVisualsComponent component, ref AppearanceChangeEvent args) protected override void OnAppearanceChange(EntityUid uid, PipeColorVisualsComponent component, ref AppearanceChangeEvent args)
{ {
if (TryComp<SpriteComponent>(uid, out var sprite) if (TryComp<SpriteComponent>(uid, out var sprite)
@@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisual
var layer = sprite[PipeVisualLayers.Pipe]; var layer = sprite[PipeVisualLayers.Pipe];
layer.Color = color.WithAlpha(layer.Color.A); layer.Color = color.WithAlpha(layer.Color.A);
} }
_itemSystem.VisualsChanged(uid);
} }
} }

View File

@@ -2,7 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2007/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2007/xaml"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls" xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client" xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
MinSize="500 500" Resizable="True" Title="Air Alarm"> MinSize="500 500" Resizable="True" Title="{Loc air-alarm-ui-title}">
<BoxContainer Orientation="Vertical" Margin="5 5 5 5"> <BoxContainer Orientation="Vertical" Margin="5 5 5 5">
<!-- Status (pressure, temperature, alarm state, device total, address, etc) --> <!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2"> <BoxContainer Orientation="Horizontal" Margin="0 0 0 2">

View File

@@ -64,7 +64,7 @@ public sealed class ClientClothingSystem : ClothingSystem
base.Initialize(); base.Initialize();
SubscribeLocalEvent<ClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals); SubscribeLocalEvent<ClothingComponent, GetEquipmentVisualsEvent>(OnGetVisuals);
SubscribeLocalEvent<ClothingComponent, InventoryTemplateUpdated>(OnInventoryTemplateUpdated); SubscribeLocalEvent<InventoryComponent, InventoryTemplateUpdated>(OnInventoryTemplateUpdated);
SubscribeLocalEvent<InventoryComponent, VisualsChangedEvent>(OnVisualsChanged); SubscribeLocalEvent<InventoryComponent, VisualsChangedEvent>(OnVisualsChanged);
SubscribeLocalEvent<SpriteComponent, DidUnequipEvent>(OnDidUnequip); SubscribeLocalEvent<SpriteComponent, DidUnequipEvent>(OnDidUnequip);
@@ -87,20 +87,19 @@ public sealed class ClientClothingSystem : ClothingSystem
} }
} }
private void OnInventoryTemplateUpdated(Entity<ClothingComponent> ent, ref InventoryTemplateUpdated args) private void OnInventoryTemplateUpdated(Entity<InventoryComponent> ent, ref InventoryTemplateUpdated args)
{ {
UpdateAllSlots(ent.Owner, clothing: ent.Comp); UpdateAllSlots(ent.Owner, ent.Comp);
} }
private void UpdateAllSlots( private void UpdateAllSlots(
EntityUid uid, EntityUid uid,
InventoryComponent? inventoryComponent = null, InventoryComponent? inventoryComponent = null)
ClothingComponent? clothing = null)
{ {
var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent)); var enumerator = _inventorySystem.GetSlotEnumerator((uid, inventoryComponent));
while (enumerator.NextItem(out var item, out var slot)) while (enumerator.NextItem(out var item, out var slot))
{ {
RenderEquipment(uid, item, slot.Name, inventoryComponent, clothingComponent: clothing); RenderEquipment(uid, item, slot.Name, inventoryComponent);
} }
} }

View File

@@ -1,53 +1,42 @@
using Content.Client.Markers; using Content.Client.Markers;
using Content.Client.Popups; using Content.Client.Popups;
using Content.Client.SubFloor; using Content.Client.SubFloor;
using Content.Shared.SubFloor;
using Robust.Client.GameObjects;
using Robust.Shared.Console; using Robust.Shared.Console;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Commands; namespace Content.Client.Commands;
internal sealed class ShowMarkersCommand : LocalizedCommands internal sealed class ShowMarkersCommand : LocalizedEntityCommands
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly MarkerSystem _markerSystem = default!;
public override string Command => "showmarkers"; public override string Command => "showmarkers";
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args) public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible ^= true; _markerSystem.MarkersVisible ^= true;
} }
} }
internal sealed class ShowSubFloor : LocalizedCommands internal sealed class ShowSubFloor : LocalizedEntityCommands
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly SubFloorHideSystem _subfloorSystem = default!;
public override string Command => "showsubfloor"; public override string Command => "showsubfloor";
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args) public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
_entitySystemManager.GetEntitySystem<SubFloorHideSystem>().ShowAll ^= true; _subfloorSystem.ShowAll ^= true;
} }
} }
internal sealed class NotifyCommand : LocalizedCommands internal sealed class NotifyCommand : LocalizedEntityCommands
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; [Dependency] private readonly PopupSystem _popupSystem = default!;
public override string Command => "notify"; public override string Command => "notify";
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args) public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
var message = args[0]; _popupSystem.PopupCursor(args[0]);
_entitySystemManager.GetEntitySystem<PopupSystem>().PopupCursor(message);
} }
} }

View File

@@ -1,32 +1,29 @@
using Content.Client.Actions; using Content.Client.Actions;
using Content.Client.Mapping;
using Content.Client.Markers; using Content.Client.Markers;
using JetBrains.Annotations; using Content.Client.SubFloor;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.State;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Client.Commands; namespace Content.Client.Commands;
[UsedImplicitly] internal sealed class MappingClientSideSetupCommand : LocalizedEntityCommands
internal sealed class MappingClientSideSetupCommand : LocalizedCommands
{ {
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly ILightManager _lightManager = default!; [Dependency] private readonly ILightManager _lightManager = default!;
[Dependency] private readonly ActionsSystem _actionSystem = default!;
[Dependency] private readonly MarkerSystem _markerSystem = default!;
[Dependency] private readonly SubFloorHideSystem _subfloorSystem = default!;
public override string Command => "mappingclientsidesetup"; public override string Command => "mappingclientsidesetup";
public override string Help => LocalizationManager.GetString($"cmd-{Command}-help", ("command", Command));
public override void Execute(IConsoleShell shell, string argStr, string[] args) public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
if (!_lightManager.LockConsoleAccess) if (_lightManager.LockConsoleAccess)
{ return;
_entitySystemManager.GetEntitySystem<MarkerSystem>().MarkersVisible = true;
_lightManager.Enabled = false; _markerSystem.MarkersVisible = true;
shell.ExecuteCommand("showsubfloor"); _lightManager.Enabled = false;
_entitySystemManager.GetEntitySystem<ActionsSystem>().LoadActionAssignments("/mapping_actions.yml", false); _subfloorSystem.ShowAll = true;
} _actionSystem.LoadActionAssignments("/mapping_actions.yml", false);
} }
} }

View File

@@ -1,4 +1,5 @@
using Robust.Client.Graphics; using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface; using Robust.Client.UserInterface;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
@@ -36,7 +37,7 @@ namespace Content.Client.Cooldown
if (Progress >= 0f) if (Progress >= 0f)
{ {
var hue = (5f / 18f) * lerp; var hue = (5f / 18f) * lerp;
color = Color.FromHsv((hue, 0.75f, 0.75f, 0.50f)); color = Color.FromHsv(new Vector4(hue, 0.75f, 0.75f, 0.50f));
} }
else else
{ {

View File

@@ -4,6 +4,7 @@ using Content.Shared.Damage.Systems;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Utility;
namespace Content.Client.Damage.Systems; namespace Content.Client.Damage.Systems;
@@ -104,6 +105,8 @@ public sealed partial class StaminaSystem : SharedStaminaSystem
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity) private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
{ {
DebugTools.Assert(entity.Comp1.CritThreshold > entity.Comp1.AnimationThreshold, $"Animation threshold on {ToPrettyString(entity)} was not less than the crit threshold. This will cause errors, animation has been cancelled.");
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) / var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold), (entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
0f, 0f,

View File

@@ -1,4 +1,5 @@
using Content.Shared.Disposal; using System.Numerics;
using Content.Shared.Disposal;
using Content.Shared.Disposal.Unit; using Content.Shared.Disposal.Unit;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls; using Robust.Client.UserInterface.Controls;

View File

@@ -5,6 +5,7 @@ using Robust.Client.Graphics;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Linq; using System.Linq;
using System.Numerics;
using DrawDepth = Content.Shared.DrawDepth.DrawDepth; using DrawDepth = Content.Shared.DrawDepth.DrawDepth;
namespace Content.Client.Holopad; namespace Content.Client.Holopad;

View File

@@ -55,6 +55,7 @@ namespace Content.Client.Input
human.AddFunction(EngineKeyFunctions.MoveLeft); human.AddFunction(EngineKeyFunctions.MoveLeft);
human.AddFunction(EngineKeyFunctions.MoveRight); human.AddFunction(EngineKeyFunctions.MoveRight);
human.AddFunction(EngineKeyFunctions.Walk); human.AddFunction(EngineKeyFunctions.Walk);
human.AddFunction(ContentKeyFunctions.ToggleKnockdown);
human.AddFunction(ContentKeyFunctions.SwapHands); human.AddFunction(ContentKeyFunctions.SwapHands);
human.AddFunction(ContentKeyFunctions.SwapHandsReverse); human.AddFunction(ContentKeyFunctions.SwapHandsReverse);
human.AddFunction(ContentKeyFunctions.Drop); human.AddFunction(ContentKeyFunctions.Drop);

View File

@@ -1,3 +1,4 @@
using System.Linq;
using Content.Client.Clothing; using Content.Client.Clothing;
using Content.Client.Examine; using Content.Client.Examine;
using Content.Client.Verbs.UI; using Content.Client.Verbs.UI;
@@ -11,6 +12,7 @@ using Robust.Client.UserInterface;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.Input.Binding; using Robust.Shared.Input.Binding;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Client.Inventory namespace Content.Client.Inventory
{ {
@@ -19,7 +21,7 @@ namespace Content.Client.Inventory
{ {
[Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IUserInterfaceManager _ui = default!; [Dependency] private readonly IUserInterfaceManager _ui = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!; [Dependency] private readonly ClientClothingSystem _clothingVisualsSystem = default!;
[Dependency] private readonly ExamineSystem _examine = default!; [Dependency] private readonly ExamineSystem _examine = default!;
@@ -91,6 +93,14 @@ namespace Content.Client.Inventory
private void OnShutdown(EntityUid uid, InventoryComponent component, ComponentShutdown args) private void OnShutdown(EntityUid uid, InventoryComponent component, ComponentShutdown args)
{ {
if (TryComp(uid, out InventorySlotsComponent? inventorySlots))
{
foreach (var slot in component.Slots)
{
TryRemoveSlotData((uid, inventorySlots), (SlotData)slot);
}
}
if (uid == _playerManager.LocalEntity) if (uid == _playerManager.LocalEntity)
OnUnlinkInventory?.Invoke(); OnUnlinkInventory?.Invoke();
} }
@@ -102,23 +112,6 @@ namespace Content.Client.Inventory
private void OnPlayerAttached(EntityUid uid, InventorySlotsComponent component, LocalPlayerAttachedEvent args) private void OnPlayerAttached(EntityUid uid, InventorySlotsComponent component, LocalPlayerAttachedEvent args)
{ {
if (TryGetSlots(uid, out var definitions))
{
foreach (var definition in definitions)
{
if (!TryGetSlotContainer(uid, definition.Name, out var container, out _))
continue;
if (!component.SlotData.TryGetValue(definition.Name, out var data))
{
data = new SlotData(definition);
component.SlotData[definition.Name] = data;
}
data.Container = container;
}
}
OnLinkInventorySlots?.Invoke(uid, component); OnLinkInventorySlots?.Invoke(uid, component);
} }
@@ -128,20 +121,6 @@ namespace Content.Client.Inventory
base.Shutdown(); base.Shutdown();
} }
protected override void OnInit(EntityUid uid, InventoryComponent component, ComponentInit args)
{
base.OnInit(uid, component, args);
_clothingVisualsSystem.InitClothing(uid, component);
if (!TryComp(uid, out InventorySlotsComponent? inventorySlots))
return;
foreach (var slot in component.Slots)
{
TryAddSlotDef(uid, inventorySlots, slot);
}
}
public void ReloadInventory(InventorySlotsComponent? component = null) public void ReloadInventory(InventorySlotsComponent? component = null)
{ {
var player = _playerManager.LocalEntity; var player = _playerManager.LocalEntity;
@@ -165,7 +144,10 @@ namespace Content.Client.Inventory
public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, string slotName, public void UpdateSlot(EntityUid owner, InventorySlotsComponent component, string slotName,
bool? blocked = null, bool? highlight = null) bool? blocked = null, bool? highlight = null)
{ {
var oldData = component.SlotData[slotName]; // The slot might have been removed when changing templates, which can cause items to be dropped.
if (!component.SlotData.TryGetValue(slotName, out var oldData))
return;
var newHighlight = oldData.Highlighted; var newHighlight = oldData.Highlighted;
var newBlocked = oldData.Blocked; var newBlocked = oldData.Blocked;
@@ -181,14 +163,28 @@ namespace Content.Client.Inventory
EntitySlotUpdate?.Invoke(newData); EntitySlotUpdate?.Invoke(newData);
} }
public bool TryAddSlotDef(EntityUid owner, InventorySlotsComponent component, SlotDefinition newSlotDef) public bool TryAddSlotData(Entity<InventorySlotsComponent> ent, SlotData newSlotData)
{ {
SlotData newSlotData = newSlotDef; //convert to slotData if (!ent.Comp.SlotData.TryAdd(newSlotData.SlotName, newSlotData))
if (!component.SlotData.TryAdd(newSlotDef.Name, newSlotData))
return false; return false;
if (owner == _playerManager.LocalEntity) if (TryGetSlotContainer(ent.Owner, newSlotData.SlotName, out var newContainer, out _))
ent.Comp.SlotData[newSlotData.SlotName].Container = newContainer;
if (ent.Owner == _playerManager.LocalEntity)
OnSlotAdded?.Invoke(newSlotData); OnSlotAdded?.Invoke(newSlotData);
return true;
}
public bool TryRemoveSlotData(Entity<InventorySlotsComponent> ent, SlotData removedSlotData)
{
if (!ent.Comp.SlotData.Remove(removedSlotData.SlotName))
return false;
if (ent.Owner == _playerManager.LocalEntity)
OnSlotRemoved?.Invoke(removedSlotData);
return true; return true;
} }
@@ -239,33 +235,52 @@ namespace Content.Client.Inventory
{ {
base.UpdateInventoryTemplate(ent); base.UpdateInventoryTemplate(ent);
if (TryComp(ent, out InventorySlotsComponent? inventorySlots)) if (!TryComp<InventorySlotsComponent>(ent, out var inventorySlots))
return;
List<SlotData> slotDataToRemove = new(); // don't modify dict while iterating
foreach (var slotData in inventorySlots.SlotData.Values)
{ {
foreach (var slot in ent.Comp.Slots) if (!ent.Comp.Slots.Any(s => s.Name == slotData.SlotName))
{ slotDataToRemove.Add(slotData);
if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData))
slotData.SlotDef = slot;
}
} }
// remove slots that are no longer in the new template
foreach (var slotData in slotDataToRemove)
{
TryRemoveSlotData((ent.Owner, inventorySlots), slotData);
}
// update existing slots or add them if they don't exist yet
foreach (var slot in ent.Comp.Slots)
{
if (inventorySlots.SlotData.TryGetValue(slot.Name, out var slotData))
slotData.SlotDef = slot;
else
TryAddSlotData((ent.Owner, inventorySlots), (SlotData)slot);
}
_clothingVisualsSystem.InitClothing(ent, ent.Comp);
if (ent.Owner == _playerManager.LocalEntity)
ReloadInventory(inventorySlots);
} }
public sealed class SlotData public sealed class SlotData
{ {
public SlotDefinition SlotDef; [ViewVariables] public SlotDefinition SlotDef;
public EntityUid? HeldEntity => Container?.ContainedEntity; [ViewVariables] public EntityUid? HeldEntity => Container?.ContainedEntity;
public bool Blocked; [ViewVariables] public bool Blocked;
public bool Highlighted; [ViewVariables] public bool Highlighted;
[ViewVariables] public ContainerSlot? Container;
[ViewVariables] [ViewVariables] public bool HasSlotGroup => SlotDef.SlotGroup != "Default";
public ContainerSlot? Container; [ViewVariables] public Vector2i ButtonOffset => SlotDef.UIWindowPosition;
public bool HasSlotGroup => SlotDef.SlotGroup != "Default"; [ViewVariables] public string SlotName => SlotDef.Name;
public Vector2i ButtonOffset => SlotDef.UIWindowPosition; [ViewVariables] public bool ShowInWindow => SlotDef.ShowInWindow;
public string SlotName => SlotDef.Name; [ViewVariables] public string SlotGroup => SlotDef.SlotGroup;
public bool ShowInWindow => SlotDef.ShowInWindow; [ViewVariables] public string SlotDisplayName => SlotDef.DisplayName;
public string SlotGroup => SlotDef.SlotGroup; [ViewVariables] public string TextureName => "Slots/" + SlotDef.TextureName;
public string SlotDisplayName => SlotDef.DisplayName; [ViewVariables] public string FullTextureName => SlotDef.FullTextureName;
public string TextureName => "Slots/" + SlotDef.TextureName;
public string FullTextureName => SlotDef.FullTextureName;
public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false, public SlotData(SlotDefinition slotDef, ContainerSlot? container = null, bool highlighted = false,
bool blocked = false) bool blocked = false)

View File

@@ -1,4 +1,5 @@
using System.Linq; using System.Linq;
using System.Numerics;
using Content.Client.Items.Systems; using Content.Client.Items.Systems;
using Content.Shared.Clothing; using Content.Shared.Clothing;
using Content.Shared.Hands; using Content.Shared.Hands;

View File

@@ -57,7 +57,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
rotation += 2 * Math.PI; rotation += 2 * Math.PI;
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
{ {
Rotation = rotation Rotation = rotation,
User = GetNetEntity(player)
}); });
return; return;
@@ -77,7 +78,8 @@ public sealed class MouseRotatorSystem : SharedMouseRotatorSystem
RaisePredictiveEvent(new RequestMouseRotatorRotationEvent RaisePredictiveEvent(new RequestMouseRotatorRotationEvent
{ {
Rotation = angle Rotation = angle,
User = GetNetEntity(player)
}); });
} }
} }

View File

@@ -25,6 +25,7 @@
<CheckBox Name="IntegerScalingCheckBox" <CheckBox Name="IntegerScalingCheckBox"
Text="{Loc 'ui-options-vp-integer-scaling'}" Text="{Loc 'ui-options-vp-integer-scaling'}"
ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" /> ToolTip="{Loc 'ui-options-vp-integer-scaling-tooltip'}" />
<ui:OptionDropDown Name="DropDownFilterMode" Title="{Loc 'ui-options-filter-label'}" />
<CheckBox Name="ViewportVerticalFitCheckBox" <CheckBox Name="ViewportVerticalFitCheckBox"
Text="{Loc 'ui-options-vp-vertical-fit'}" Text="{Loc 'ui-options-vp-vertical-fit'}"
ToolTip="{Loc 'ui-options-vp-vertical-fit-tooltip'}" /> ToolTip="{Loc 'ui-options-vp-vertical-fit-tooltip'}" />

View File

@@ -39,6 +39,14 @@ public sealed partial class GraphicsTab : Control
new OptionDropDownCVar<float>.ValueOption(2.00f, Loc.GetString("ui-options-scale-200")), new OptionDropDownCVar<float>.ValueOption(2.00f, Loc.GetString("ui-options-scale-200")),
]); ]);
Control.AddOptionDropDown(
CCVars.ViewportScalingFilterMode,
DropDownFilterMode,
[
new OptionDropDownCVar<string>.ValueOption("nearest", Loc.GetString("ui-options-filter-nearest")),
new OptionDropDownCVar<string>.ValueOption("bilinear", Loc.GetString("ui-options-filter-bilinear")),
]);
var vpStretch = Control.AddOptionCheckBox(CCVars.ViewportStretch, ViewportStretchCheckBox); var vpStretch = Control.AddOptionCheckBox(CCVars.ViewportStretch, ViewportStretchCheckBox);
var vpVertFit = Control.AddOptionCheckBox(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox); var vpVertFit = Control.AddOptionCheckBox(CCVars.ViewportVerticalFit, ViewportVerticalFitCheckBox);
Control.AddOptionSlider( Control.AddOptionSlider(
@@ -50,6 +58,7 @@ public sealed partial class GraphicsTab : Control
vpStretch.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility(); vpStretch.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility();
vpVertFit.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility(); vpVertFit.ImmediateValueChanged += _ => UpdateViewportSettingsVisibility();
IntegerScalingCheckBox.OnToggled += _ => UpdateViewportSettingsVisibility();
Control.AddOptionSlider( Control.AddOptionSlider(
CCVars.ViewportWidth, CCVars.ViewportWidth,
@@ -77,6 +86,7 @@ public sealed partial class GraphicsTab : Control
IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed; IntegerScalingCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed; ViewportVerticalFitCheckBox.Visible = ViewportStretchCheckBox.Pressed;
ViewportWidthSlider.Visible = !ViewportStretchCheckBox.Pressed || !ViewportVerticalFitCheckBox.Pressed; ViewportWidthSlider.Visible = !ViewportStretchCheckBox.Pressed || !ViewportVerticalFitCheckBox.Pressed;
DropDownFilterMode.Visible = !IntegerScalingCheckBox.Pressed && ViewportStretchCheckBox.Pressed;
} }
private void UpdateViewportWidthRange() private void UpdateViewportWidthRange()

View File

@@ -163,6 +163,7 @@ namespace Content.Client.Options.UI.Tabs
AddButton(EngineKeyFunctions.Walk); AddButton(EngineKeyFunctions.Walk);
AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk); AddCheckBox("ui-options-hotkey-toggle-walk", _cfg.GetCVar(CCVars.ToggleWalk), HandleToggleWalk);
InitToggleWalk(); InitToggleWalk();
AddButton(ContentKeyFunctions.ToggleKnockdown);
AddHeader("ui-options-header-camera"); AddHeader("ui-options-header-camera");
AddButton(EngineKeyFunctions.CameraRotateLeft); AddButton(EngineKeyFunctions.CameraRotateLeft);

View File

@@ -3,6 +3,7 @@ using Robust.Client.UserInterface.XAML;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using System; using System;
using System.Numerics;
using Content.Client.Stylesheets; using Content.Client.Stylesheets;
using Content.Shared.APC; using Content.Shared.APC;
using Robust.Client.Graphics; using Robust.Client.Graphics;

View File

@@ -6,7 +6,6 @@ using Robust.Shared.Utility;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Vector4 = Robust.Shared.Maths.Vector4;
namespace Content.Client.Power; namespace Content.Client.Power;

View File

@@ -10,7 +10,6 @@ using Robust.Shared.Physics;
using Robust.Shared.Threading; using Robust.Shared.Threading;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Vector2 = System.Numerics.Vector2;
namespace Content.Client.Shuttles.UI; namespace Content.Client.Shuttles.UI;

View File

@@ -220,9 +220,9 @@ public sealed partial class ShuttleNavControl : BaseShuttleControl
var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView); var gridCentre = Vector2.Transform(gridBody.LocalCenter, curGridToView);
var distance = gridCentre.Length(); var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName), var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}")); ("distance", $"{gridDistance:0.0}"));
var mapCoords = _transform.GetWorldPosition(gUid); var mapCoords = _transform.GetWorldPosition(gUid);
var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})"; var coordsText = $"({mapCoords.X:0.0}, {mapCoords.Y:0.0})";

View File

@@ -0,0 +1,43 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.SmartFridge;
using Robust.Client.UserInterface;
using Robust.Shared.Input;
namespace Content.Client.SmartFridge;
public sealed class SmartFridgeBoundUserInterface : BoundUserInterface
{
private SmartFridgeMenu? _menu;
public SmartFridgeBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<SmartFridgeMenu>();
_menu.OnItemSelected += OnItemSelected;
Refresh();
}
public void Refresh()
{
if (_menu is not {} menu || !EntMan.TryGetComponent(Owner, out SmartFridgeComponent? fridge))
return;
menu.SetFlavorText(Loc.GetString(fridge.FlavorText));
menu.Populate((Owner, fridge));
}
private void OnItemSelected(GUIBoundKeyEventArgs args, ListData data)
{
if (args.Function != EngineKeyFunctions.UIClick)
return;
if (data is not SmartFridgeListData entry)
return;
SendPredictedMessage(new SmartFridgeDispenseItemMessage(entry.Entry));
}
}

View File

@@ -0,0 +1,16 @@
<BoxContainer xmlns="https://spacestation14.io"
Orientation="Horizontal"
HorizontalExpand="True"
SeparationOverride="4">
<SpriteView
Name="EntityView"
Margin="4 0 0 0"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MinSize="32 32"
/>
<Label Name="NameLabel"
SizeFlagsStretchRatio="3"
HorizontalExpand="True"
ClipText="True"/>
</BoxContainer>

View File

@@ -0,0 +1,18 @@
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.SmartFridge;
[GenerateTypedNameReferences]
public sealed partial class SmartFridgeItem : BoxContainer
{
public SmartFridgeItem(EntityUid uid, string text)
{
RobustXamlLoader.Load(this);
EntityView.SetEntity(uid);
NameLabel.Text = text;
}
}

View File

@@ -0,0 +1,24 @@
<controls:FancyWindow
xmlns="https://spacestation14.io"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:co="clr-namespace:Content.Client.UserInterface.Controls"
MinHeight="450"
MinWidth="350"
Title="{Loc 'smart-fridge-component-title'}">
<BoxContainer Name="MainContainer" Orientation="Vertical">
<LineEdit Name="SearchBar" PlaceHolder="{Loc 'smart-fridge-component-search-filter'}" HorizontalExpand="True" Margin ="4 4"/>
<co:SearchListContainer Name="VendingContents" VerticalExpand="True" Margin="4 4"/>
<!-- Footer -->
<BoxContainer Orientation="Vertical">
<PanelContainer StyleClasses="LowDivider" />
<BoxContainer Orientation="Horizontal" Margin="10 2 5 0" VerticalAlignment="Bottom">
<Label Name="LeftFlavorLabel" StyleClasses="WindowFooterText" />
<Label Text="{Loc 'vending-machine-flavor-right'}" StyleClasses="WindowFooterText"
HorizontalAlignment="Right" HorizontalExpand="True" Margin="0 0 5 0" />
<TextureRect StyleClasses="NTLogoDark" Stretch="KeepAspectCentered"
VerticalAlignment="Center" HorizontalAlignment="Right" SetSize="19 19"/>
</BoxContainer>
</BoxContainer>
</BoxContainer>
</controls:FancyWindow>

View File

@@ -0,0 +1,81 @@
using System.Linq;
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.SmartFridge;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.XAML;
using Robust.Client.UserInterface;
namespace Content.Client.SmartFridge;
public record SmartFridgeListData(EntityUid Representative, SmartFridgeEntry Entry, int Amount) : ListData;
[GenerateTypedNameReferences]
public sealed partial class SmartFridgeMenu : FancyWindow
{
[Dependency] private readonly IEntityManager _entityManager = default!;
public event Action<GUIBoundKeyEventArgs, ListData>? OnItemSelected;
private readonly StyleBoxFlat _styleBox = new() { BackgroundColor = new Color(70, 73, 102) };
public SmartFridgeMenu()
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
VendingContents.SearchBar = SearchBar;
VendingContents.DataFilterCondition += DataFilterCondition;
VendingContents.GenerateItem += GenerateButton;
VendingContents.ItemKeyBindDown += (args, data) => OnItemSelected?.Invoke(args, data);
}
private bool DataFilterCondition(string filter, ListData data)
{
if (data is not SmartFridgeListData entry)
return false;
if (string.IsNullOrEmpty(filter))
return true;
return entry.Entry.Name.Contains(filter, StringComparison.CurrentCultureIgnoreCase);
}
private void GenerateButton(ListData data, ListContainerButton button)
{
if (data is not SmartFridgeListData entry)
return;
var label = Loc.GetString("smart-fridge-list-item", ("item", entry.Entry.Name), ("amount", entry.Amount));
button.AddChild(new SmartFridgeItem(entry.Representative, label));
button.ToolTip = label;
button.StyleBoxOverride = _styleBox;
}
public void Populate(Entity<SmartFridgeComponent> ent)
{
var listData = new List<ListData>();
foreach (var item in ent.Comp.Entries)
{
if (!ent.Comp.ContainedEntries.TryGetValue(item, out var items) || items.Count == 0)
{
listData.Add(new SmartFridgeListData(EntityUid.Invalid, item, 0));
}
else
{
var representative = _entityManager.GetEntity(items.First());
listData.Add(new SmartFridgeListData(representative, item, items.Count));
}
}
VendingContents.PopulateList(listData);
}
public void SetFlavorText(string flavor)
{
LeftFlavorLabel.Text = flavor;
}
}

View File

@@ -0,0 +1,24 @@
using Content.Shared.SmartFridge;
using Robust.Shared.Analyzers;
namespace Content.Client.SmartFridge;
public sealed class SmartFridgeUISystem : EntitySystem
{
[Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<SmartFridgeComponent, AfterAutoHandleStateEvent>(OnSmartFridgeAfterState);
}
private void OnSmartFridgeAfterState(Entity<SmartFridgeComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (!_uiSystem.TryGetOpenUi<SmartFridgeBoundUserInterface>(ent.Owner, SmartFridgeUiKey.Key, out var bui))
return;
bui.Refresh();
}
}

View File

@@ -1,17 +1,19 @@
using System.Numerics; using System.Numerics;
using Content.Shared.Mobs; using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Stunnable; using Content.Shared.Stunnable;
using Robust.Client.Animations; using Robust.Client.Animations;
using Robust.Client.GameObjects; using Robust.Client.GameObjects;
using Robust.Shared.Animations; using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.Stunnable; namespace Content.Client.Stunnable;
public sealed class StunSystem : SharedStunSystem public sealed class StunSystem : SharedStunSystem
{ {
[Dependency] private readonly SharedCombatModeSystem _combat = default!;
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!; [Dependency] private readonly SpriteSystem _spriteSystem = default!;
@@ -23,6 +25,22 @@ public sealed class StunSystem : SharedStunSystem
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit); SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged); SubscribeLocalEvent<StunVisualsComponent, AppearanceChangeEvent>(OnAppearanceChanged);
CommandBinds.Builder
.BindAfter(EngineKeyFunctions.UseSecondary, new PointerInputCmdHandler(OnUseSecondary, true, true), typeof(SharedInteractionSystem))
.Register<StunSystem>();
}
private bool OnUseSecondary(in PointerInputCmdHandler.PointerInputCmdArgs args)
{
if (args.Session?.AttachedEntity is not {Valid: true} uid)
return false;
if (args.EntityUid != uid || !HasComp<KnockedDownComponent>(uid) || !_combat.IsInCombatMode(uid))
return false;
RaisePredictiveEvent(new ForceStandUpEvent());
return true;
} }
/// <summary> /// <summary>

View File

@@ -30,6 +30,8 @@ namespace Content.Client.UserInterface.Controls
}; };
AddChild(Viewport); AddChild(Viewport);
_cfg.OnValueChanged(CCVars.ViewportScalingFilterMode, _ => UpdateCfg(), true);
} }
protected override void EnteredTree() protected override void EnteredTree()
@@ -52,6 +54,7 @@ namespace Content.Client.UserInterface.Controls
var renderScaleUp = _cfg.GetCVar(CCVars.ViewportScaleRender); var renderScaleUp = _cfg.GetCVar(CCVars.ViewportScaleRender);
var fixedFactor = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor); var fixedFactor = _cfg.GetCVar(CCVars.ViewportFixedScaleFactor);
var verticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit); var verticalFit = _cfg.GetCVar(CCVars.ViewportVerticalFit);
var filterMode = _cfg.GetCVar(CCVars.ViewportScalingFilterMode);
if (stretch) if (stretch)
{ {
@@ -60,7 +63,11 @@ namespace Content.Client.UserInterface.Controls
{ {
// Did not find a snap, enable stretching. // Did not find a snap, enable stretching.
Viewport.FixedStretchSize = null; Viewport.FixedStretchSize = null;
Viewport.StretchMode = ScalingViewportStretchMode.Bilinear; Viewport.StretchMode = filterMode switch
{
"nearest" => ScalingViewportStretchMode.Nearest,
"bilinear" => ScalingViewportStretchMode.Bilinear
};
Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None; Viewport.IgnoreDimension = verticalFit ? ScalingViewportIgnoreDimension.Horizontal : ScalingViewportIgnoreDimension.None;
if (renderScaleUp) if (renderScaleUp)

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.Mobs; using Content.Shared.Mobs;
using Robust.Client.Graphics; using Robust.Client.Graphics;
using Robust.Client.Player; using Robust.Client.Player;

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
// lerp // lerp
var hue = 5f / 18f * progress; var hue = 5f / 18f * progress;
return Color.FromHsv((hue, 1f, 0.75f, 1f)); return Color.FromHsv(new Vector4(hue, 1f, 0.75f, 1f));
} }
return InterpolateColorGaussian(Plasma, progress); return InterpolateColorGaussian(Plasma, progress);

View File

@@ -185,7 +185,12 @@ public sealed class ItemGridPiece : Control, IEntityControl
handle.SetTransform(pos, iconRotation); handle.SetTransform(pos, iconRotation);
var box = new UIBox2(root, root + sprite.Size * scale); var box = new UIBox2(root, root + sprite.Size * scale);
handle.DrawTextureRect(sprite, box);
var ev = new BeforeRenderInGridEvent(new Color(255, 255, 255));
_entityManager.EventBus.RaiseLocalEvent(Entity, ev);
handle.DrawTextureRect(sprite, box, ev.Color);
handle.SetTransform(GlobalPixelPosition, Angle.Zero); handle.SetTransform(GlobalPixelPosition, Angle.Zero);
} }
else else
@@ -298,6 +303,19 @@ public sealed class ItemGridPiece : Control, IEntityControl
public EntityUid? UiEntity => Entity; public EntityUid? UiEntity => Entity;
} }
/// <summary>
/// This event gets raised before a sprite gets drawn in a grid and lets to change the sprite color for several gameobjects that have special sprites to render in containers.
/// </summary>
public sealed class BeforeRenderInGridEvent : EntityEventArgs
{
public Color Color { get; set; }
public BeforeRenderInGridEvent(Color color)
{
Color = color;
}
}
public enum ItemGridPieceMarks public enum ItemGridPieceMarks
{ {
First, First,

View File

@@ -621,7 +621,7 @@ public sealed class StorageWindow : BaseWindow
{ {
marked.Add(cell); marked.Add(cell);
cell.ModulateSelfOverride = spotFree cell.ModulateSelfOverride = spotFree
? Color.FromHsv((0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f)) ? Color.FromHsv(new Vector4(0.18f, 1 / spot, 0.5f / spot + 0.5f, 1f))
: Color.FromHex("#2222CC"); : Color.FromHex("#2222CC");
} }
} }

View File

@@ -58,7 +58,7 @@ namespace Content.IntegrationTests.Tests
} }
}); });
await server.WaitRunTicks(15); await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() => await server.WaitPost(() =>
{ {
@@ -116,7 +116,7 @@ namespace Content.IntegrationTests.Tests
entityMan.SpawnEntity(protoId, map.GridCoords); entityMan.SpawnEntity(protoId, map.GridCoords);
} }
}); });
await server.WaitRunTicks(15); await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() => await server.WaitPost(() =>
{ {
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan) static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
@@ -274,7 +274,7 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3); await pair.RunTicksSync(3);
// We consider only non-audio entities, as some entities will just play sounds when they spawn. // We consider only non-audio entities, as some entities will just play sounds when they spawn.
int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>(); int Count(IEntityManager ent) => ent.EntityCount - ent.Count<AudioComponent>();
IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>); IEnumerable<EntityUid> Entities(IEntityManager entMan) => entMan.GetEntities().Where(entMan.HasComponent<AudioComponent>);
await Assert.MultipleAsync(async () => await Assert.MultipleAsync(async () =>

View File

@@ -1,4 +1,4 @@
using Content.Server.Stunnable; using Content.Server.Stunnable;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
@@ -17,9 +17,7 @@ namespace Content.IntegrationTests.Tests
components: components:
- type: Inventory - type: Inventory
- type: ContainerContainer - type: ContainerContainer
- type: StatusEffects - type: MobState
allowed:
- Stun
- type: entity - type: entity
name: InventoryJumpsuitJanitorDummy name: InventoryJumpsuitJanitorDummy
@@ -70,7 +68,7 @@ namespace Content.IntegrationTests.Tests
}); });
#pragma warning restore NUnit2045 #pragma warning restore NUnit2045
systemMan.GetEntitySystem<StunSystem>().TryStun(human, TimeSpan.FromSeconds(1f), true); systemMan.GetEntitySystem<StunSystem>().TryUpdateStunDuration(human, TimeSpan.FromSeconds(1f));
#pragma warning disable NUnit2045 #pragma warning disable NUnit2045
// Since the mob is stunned, they can't equip this. // Since the mob is stunned, they can't equip this.

View File

@@ -2,7 +2,7 @@
using System.Linq; using System.Linq;
using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles;
using Content.Server.Ghost.Roles.Components; using Content.Server.Ghost.Roles.Components;
using Content.Server.Mind.Commands; using Content.Server.Mind;
using Content.Server.Roles; using Content.Server.Roles;
using Content.Shared.Damage; using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes; using Content.Shared.Damage.Prototypes;
@@ -339,7 +339,7 @@ public sealed partial class MindTests
var entMan = server.ResolveDependency<IServerEntityManager>(); var entMan = server.ResolveDependency<IServerEntityManager>();
var playerMan = server.ResolveDependency<IPlayerManager>(); var playerMan = server.ResolveDependency<IPlayerManager>();
var mindSystem = entMan.EntitySysManager.GetEntitySystem<SharedMindSystem>(); var mindSystem = entMan.EntitySysManager.GetEntitySystem<MindSystem>();
EntityUid entity = default!; EntityUid entity = default!;
EntityUid mindId = default!; EntityUid mindId = default!;
@@ -379,7 +379,7 @@ public sealed partial class MindTests
mob = entMan.SpawnEntity(null, new MapCoordinates()); mob = entMan.SpawnEntity(null, new MapCoordinates());
MakeSentientCommand.MakeSentient(mob, entMan); mindSystem.MakeSentient(mob);
mobMindId = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second"); mobMindId = mindSystem.CreateMind(player.UserId, "Mindy McThinker the Second");
mobMind = entMan.GetComponent<MindComponent>(mobMindId); mobMind = entMan.GetComponent<MindComponent>(mobMindId);

View File

@@ -1,4 +1,3 @@
using System.Threading;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.RoundEnd; using Content.Server.RoundEnd;
using Content.Shared.CCVar; using Content.Shared.CCVar;
@@ -22,7 +21,7 @@ namespace Content.IntegrationTests.Tests
private void OnRoundEnd(RoundEndSystemChangedEvent ev) private void OnRoundEnd(RoundEndSystemChangedEvent ev)
{ {
Interlocked.Increment(ref RoundCount); RoundCount += 1;
} }
} }
@@ -127,13 +126,17 @@ namespace Content.IntegrationTests.Tests
async Task WaitForEvent() async Task WaitForEvent()
{ {
var timeout = Task.Delay(TimeSpan.FromSeconds(10)); const int maxTicks = 60;
var currentCount = Thread.VolatileRead(ref sys.RoundCount); var currentCount = sys.RoundCount;
while (currentCount == Thread.VolatileRead(ref sys.RoundCount) && !timeout.IsCompleted) for (var i = 0; i < maxTicks; i++)
{ {
await pair.RunTicksSync(5); if (currentCount != sys.RoundCount)
return;
await pair.RunTicksSync(1);
} }
if (timeout.IsCompleted) throw new TimeoutException("Event took too long to trigger");
throw new TimeoutException("Event took too long to trigger");
} }
// Need to clean self up // Need to clean self up

View File

@@ -0,0 +1,111 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.SmartFridge;
namespace Content.IntegrationTests.Tests.SmartFridge;
public sealed class SmartFridgeInteractionTest : InteractionTest
{
private const string SmartFridgeProtoId = "SmartFridge";
private const string SampleItemProtoId = "FoodAmbrosiaVulgaris";
private const string SampleDumpableAndInsertableId = "PillCanisterSomething";
private const int SampleDumpableCount = 5;
private const string SampleDumpableId = "ChemBagSomething";
[TestPrototypes]
private const string TestPrototypes = $@"
- type: entity
parent: PillCanister
id: {SampleDumpableAndInsertableId}
components:
- type: StorageFill
contents:
- id: PillCopper
amount: 5
- type: entity
parent: ChemBag
id: {SampleDumpableId}
components:
- type: StorageFill
contents:
- id: PillCopper
amount: 5
";
[Test]
public async Task InsertAndDispenseItemTest()
{
await PlaceInHands(SampleItemProtoId);
await SpawnTarget(SmartFridgeProtoId);
var fridge = SEntMan.GetEntity(Target.Value);
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
// smartfridge spawns with nothing
Assert.That(component.Entries, Is.Empty);
await InteractUsing(SampleItemProtoId);
// smartfridge now has items
Assert.That(component.Entries, Is.Not.Empty);
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Not.Empty);
// open the UI
await Activate();
Assert.That(IsUiOpen(SmartFridgeUiKey.Key));
// dispense an item
await SendBui(SmartFridgeUiKey.Key, new SmartFridgeDispenseItemMessage(component.Entries[0]));
// assert that the listing is still there
Assert.That(component.Entries, Is.Not.Empty);
// but empty
Assert.That(component.ContainedEntries[component.Entries[0]], Is.Empty);
// and that the thing we dispensed is actually around
await AssertEntityLookup(
("APCBasic", 1),
(SampleItemProtoId, 1)
);
}
[Test]
public async Task InsertDumpableInsertableItemTest()
{
await PlaceInHands(SampleItemProtoId);
await SpawnTarget(SmartFridgeProtoId);
var fridge = SEntMan.GetEntity(Target.Value);
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
await InteractUsing(SampleDumpableAndInsertableId);
// smartfridge now has one item only
Assert.That(component.Entries, Is.Not.Empty);
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(1));
}
[Test]
public async Task InsertDumpableItemTest()
{
await PlaceInHands(SampleItemProtoId);
await SpawnTarget(SmartFridgeProtoId);
var fridge = SEntMan.GetEntity(Target.Value);
var component = SEntMan.GetComponent<SmartFridgeComponent>(fridge);
await SpawnEntity("APCBasic", SEntMan.GetCoordinates(TargetCoords));
await RunTicks(1);
await InteractUsing(SampleDumpableId);
// smartfridge now has N items
Assert.That(component.Entries, Is.Not.Empty);
Assert.That(component.ContainedEntries[component.Entries[0]].Count, Is.EqualTo(SampleDumpableCount));
}
}

View File

@@ -1,70 +0,0 @@
using Content.Shared.Alert;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Abilities.Mime
{
/// <summary>
/// Lets its owner entity use mime powers, like placing invisible walls.
/// </summary>
[RegisterComponent]
public sealed partial class MimePowersComponent : Component
{
/// <summary>
/// Whether this component is active or not.
/// </summarY>
[DataField("enabled")]
public bool Enabled = true;
/// <summary>
/// The wall prototype to use.
/// </summary>
[DataField("wallPrototype", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string WallPrototype = "WallInvisible";
[DataField("invisibleWallAction", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string? InvisibleWallAction = "ActionMimeInvisibleWall";
[DataField("invisibleWallActionEntity")] public EntityUid? InvisibleWallActionEntity;
// The vow zone lies below
public bool VowBroken = false;
/// <summary>
/// Whether this mime is ready to take the vow again.
/// Note that if they already have the vow, this is also false.
/// </summary>
public bool ReadyToRepent = false;
/// <summary>
/// Time when the mime can repent their vow
/// </summary>
[DataField("vowRepentTime", customTypeSerializer: typeof(TimeOffsetSerializer))]
public TimeSpan VowRepentTime = TimeSpan.Zero;
/// <summary>
/// How long it takes the mime to get their powers back
/// </summary>
[DataField("vowCooldown")]
public TimeSpan VowCooldown = TimeSpan.FromMinutes(5);
[DataField]
public ProtoId<AlertPrototype> VowAlert = "VowOfSilence";
[DataField]
public ProtoId<AlertPrototype> VowBrokenAlert = "VowBroken";
/// <summary>
/// Does this component prevent the mime from writing on paper while their vow is active?
/// </summary>
[DataField]
public bool PreventWriting = false;
/// <summary>
/// What message is displayed when the mime fails to write?
/// </summary>
[DataField]
public LocId FailWriteMessage = "paper-component-illiterate-mime";
}
}

View File

@@ -1,171 +0,0 @@
using Content.Server.Popups;
using Content.Shared.Abilities.Mime;
using Content.Shared.Actions;
using Content.Shared.Actions.Events;
using Content.Shared.Alert;
using Content.Shared.Coordinates.Helpers;
using Content.Shared.Maps;
using Content.Shared.Paper;
using Content.Shared.Physics;
using Robust.Shared.Containers;
using Robust.Shared.Map;
using Robust.Shared.Timing;
using Content.Shared.Speech.Muting;
namespace Content.Server.Abilities.Mime
{
public sealed class MimePowersSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedActionsSystem _actionsSystem = default!;
[Dependency] private readonly AlertsSystem _alertsSystem = default!;
[Dependency] private readonly TurfSystem _turf = default!;
[Dependency] private readonly IMapManager _mapMan = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MimePowersComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<MimePowersComponent, InvisibleWallActionEvent>(OnInvisibleWall);
SubscribeLocalEvent<MimePowersComponent, BreakVowAlertEvent>(OnBreakVowAlert);
SubscribeLocalEvent<MimePowersComponent, RetakeVowAlertEvent>(OnRetakeVowAlert);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
// Queue to track whether mimes can retake vows yet
var query = EntityQueryEnumerator<MimePowersComponent>();
while (query.MoveNext(out var uid, out var mime))
{
if (!mime.VowBroken || mime.ReadyToRepent)
continue;
if (_timing.CurTime < mime.VowRepentTime)
continue;
mime.ReadyToRepent = true;
_popupSystem.PopupEntity(Loc.GetString("mime-ready-to-repent"), uid, uid);
}
}
private void OnComponentInit(EntityUid uid, MimePowersComponent component, ComponentInit args)
{
EnsureComp<MutedComponent>(uid);
if (component.PreventWriting)
{
EnsureComp<BlockWritingComponent>(uid, out var illiterateComponent);
illiterateComponent.FailWriteMessage = component.FailWriteMessage;
Dirty(uid, illiterateComponent);
}
_alertsSystem.ShowAlert(uid, component.VowAlert);
_actionsSystem.AddAction(uid, ref component.InvisibleWallActionEntity, component.InvisibleWallAction, uid);
}
/// <summary>
/// Creates an invisible wall in a free space after some checks.
/// </summary>
private void OnInvisibleWall(EntityUid uid, MimePowersComponent component, InvisibleWallActionEvent args)
{
if (!component.Enabled)
return;
if (_container.IsEntityOrParentInContainer(uid))
return;
var xform = Transform(uid);
// Get the tile in front of the mime
var offsetValue = xform.LocalRotation.ToWorldVec();
var coords = xform.Coordinates.Offset(offsetValue).SnapToGrid(EntityManager, _mapMan);
var tile = _turf.GetTileRef(coords);
if (tile == null)
return;
// Check if the tile is blocked by a wall or mob, and don't create the wall if so
if (_turf.IsTileBlocked(tile.Value, CollisionGroup.Impassable | CollisionGroup.Opaque))
{
_popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-failed"), uid, uid);
return;
}
_popupSystem.PopupEntity(Loc.GetString("mime-invisible-wall-popup", ("mime", uid)), uid);
// Make sure we set the invisible wall to despawn properly
Spawn(component.WallPrototype, _turf.GetTileCenter(tile.Value));
// Handle args so cooldown works
args.Handled = true;
}
private void OnBreakVowAlert(Entity<MimePowersComponent> ent, ref BreakVowAlertEvent args)
{
if (args.Handled)
return;
BreakVow(ent, ent);
args.Handled = true;
}
private void OnRetakeVowAlert(Entity<MimePowersComponent> ent, ref RetakeVowAlertEvent args)
{
if (args.Handled)
return;
RetakeVow(ent, ent);
args.Handled = true;
}
/// <summary>
/// Break this mime's vow to not speak.
/// </summary>
public void BreakVow(EntityUid uid, MimePowersComponent? mimePowers = null)
{
if (!Resolve(uid, ref mimePowers))
return;
if (mimePowers.VowBroken)
return;
mimePowers.Enabled = false;
mimePowers.VowBroken = true;
mimePowers.VowRepentTime = _timing.CurTime + mimePowers.VowCooldown;
RemComp<MutedComponent>(uid);
if (mimePowers.PreventWriting)
RemComp<BlockWritingComponent>(uid);
_alertsSystem.ClearAlert(uid, mimePowers.VowAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowBrokenAlert);
_actionsSystem.RemoveAction(uid, mimePowers.InvisibleWallActionEntity);
}
/// <summary>
/// Retake this mime's vow to not speak.
/// </summary>
public void RetakeVow(EntityUid uid, MimePowersComponent? mimePowers = null)
{
if (!Resolve(uid, ref mimePowers))
return;
if (!mimePowers.ReadyToRepent)
{
_popupSystem.PopupEntity(Loc.GetString("mime-not-ready-repent"), uid, uid);
return;
}
mimePowers.Enabled = true;
mimePowers.ReadyToRepent = false;
mimePowers.VowBroken = false;
AddComp<MutedComponent>(uid);
if (mimePowers.PreventWriting)
{
EnsureComp<BlockWritingComponent>(uid, out var illiterateComponent);
illiterateComponent.FailWriteMessage = mimePowers.FailWriteMessage;
Dirty(uid, illiterateComponent);
}
_alertsSystem.ClearAlert(uid, mimePowers.VowBrokenAlert);
_alertsSystem.ShowAlert(uid, mimePowers.VowAlert);
_actionsSystem.AddAction(uid, ref mimePowers.InvisibleWallActionEntity, mimePowers.InvisibleWallAction, uid);
}
}
}

View File

@@ -0,0 +1,58 @@
using Content.Server.Administration.UI;
using Content.Server.EUI;
using Content.Shared.Administration;
using Robust.Server.Player;
using Robust.Shared.Console;
namespace Content.Server.Administration.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class CameraCommand : LocalizedCommands
{
[Dependency] private readonly EuiManager _eui = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
public override string Command => "camera";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (shell.Player is not { } user)
{
shell.WriteError(Loc.GetString("shell-cannot-run-command-from-server"));
return;
}
if (args.Length != 1)
{
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
if (!NetEntity.TryParse(args[0], out var targetNetId) || !_entManager.TryGetEntity(targetNetId, out var targetUid))
{
if (!_playerManager.TryGetSessionByUsername(args[0], out var player)
|| player.AttachedEntity == null)
{
shell.WriteError(Loc.GetString("cmd-camera-wrong-argument"));
return;
}
targetUid = player.AttachedEntity.Value;
}
var ui = new AdminCameraEui(targetUid.Value);
_eui.OpenEui(ui, user);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
if (args.Length == 1)
{
return CompletionResult.FromHintOptions(
CompletionHelper.SessionNames(players: _playerManager),
Loc.GetString("cmd-camera-hint"));
}
return CompletionResult.Empty;
}
}

View File

@@ -39,6 +39,7 @@ using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Slippery; using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using Content.Shared.Tabletop.Components; using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
using Content.Shared.Verbs; using Content.Shared.Verbs;
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems; using Robust.Shared.Physics.Systems;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Random; using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer; using Timer = Robust.Shared.Timing.Timer;
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
if (!hadSlipComponent) if (!hadSlipComponent)
{ {
slipComponent.SlipData.SuperSlippery = true; slipComponent.SlipData.SuperSlippery = true;
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5); slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.LaunchForwardsMultiplier = 20; slipComponent.SlipData.LaunchForwardsMultiplier = 20;
} }
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description")) Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
}; };
args.Verbs.Add(omniaccent); args.Verbs.Add(omniaccent);
var crawlerName = Loc.GetString("admin-smite-crawler-name").ToLowerInvariant();
Verb crawler = new()
{
Text = crawlerName,
Category = VerbCategory.Smite,
Icon = new SpriteSpecifier.Rsi(new("Mobs/Animals/snake.rsi"), "icon"),
Act = () =>
{
EnsureComp<WormComponent>(args.Target);
},
Impact = LogImpact.Extreme,
Message = string.Join(": ", crawlerName, Loc.GetString("admin-smite-crawler-description"))
};
args.Verbs.Add(crawler);
} }
} }

View File

@@ -4,7 +4,6 @@ using Content.Server.Administration.UI;
using Content.Server.Disposal.Tube; using Content.Server.Disposal.Tube;
using Content.Server.EUI; using Content.Server.EUI;
using Content.Server.Ghost.Roles; using Content.Server.Ghost.Roles;
using Content.Server.Mind.Commands;
using Content.Server.Mind; using Content.Server.Mind;
using Content.Server.Prayer; using Content.Server.Prayer;
using Content.Server.Silicons.Laws; using Content.Server.Silicons.Laws;
@@ -16,7 +15,6 @@ using Content.Shared.Configurable;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Examine; using Content.Shared.Examine;
using Content.Shared.GameTicking; using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Mind.Components; using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components; using Content.Shared.Movement.Components;
@@ -390,6 +388,22 @@ namespace Content.Server.Administration.Systems
Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"), Icon = new SpriteSpecifier.Rsi(new ResPath("/Textures/Interface/Actions/actions_borg.rsi"), "state-laws"),
}); });
} }
// open camera
args.Verbs.Add(new Verb()
{
Priority = 10,
Text = Loc.GetString("admin-verbs-camera"),
Message = Loc.GetString("admin-verbs-camera-description"),
Icon = new SpriteSpecifier.Texture(new("/Textures/Interface/VerbIcons/vv.svg.192dpi.png")),
Category = VerbCategory.Admin,
Act = () =>
{
var ui = new AdminCameraEui(args.Target);
_euiManager.OpenEui(ui, player);
},
Impact = LogImpact.Low
});
} }
} }
@@ -458,7 +472,7 @@ namespace Content.Server.Administration.Systems
Text = Loc.GetString("make-sentient-verb-get-data-text"), Text = Loc.GetString("make-sentient-verb-get-data-text"),
Category = VerbCategory.Debug, Category = VerbCategory.Debug,
Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")), Icon = new SpriteSpecifier.Texture(new ("/Textures/Interface/VerbIcons/sentient.svg.192dpi.png")),
Act = () => MakeSentientCommand.MakeSentient(args.Target, EntityManager), Act = () => _mindSystem.MakeSentient(args.Target),
Impact = LogImpact.Medium Impact = LogImpact.Medium
}; };
args.Verbs.Add(verb); args.Verbs.Add(verb);

View File

@@ -0,0 +1,97 @@
using Content.Server.Administration.Managers;
using Content.Server.EUI;
using Content.Shared.Administration;
using Content.Shared.Eui;
using Content.Shared.Follower;
using Content.Shared.Coordinates;
using Robust.Server.GameStates;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using JetBrains.Annotations;
namespace Content.Server.Administration.UI;
/// <summary>
/// Admin Eui for opening a viewport window to observe entities.
/// Use the "Open Camera" admin verb or the "camera" command to open.
/// </summary>
[UsedImplicitly]
public sealed partial class AdminCameraEui : BaseEui
{
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly FollowerSystem _follower = default!;
private readonly PvsOverrideSystem _pvs = default!;
private readonly SharedViewSubscriberSystem _viewSubscriber = default!;
private static readonly EntProtoId CameraProtoId = "AdminCamera";
private readonly EntityUid _target;
private EntityUid? _camera;
public AdminCameraEui(EntityUid target)
{
IoCManager.InjectDependencies(this);
_follower = _entityManager.System<FollowerSystem>();
_pvs = _entityManager.System<PvsOverrideSystem>();
_viewSubscriber = _entityManager.System<SharedViewSubscriberSystem>();
_target = target;
}
public override void Opened()
{
base.Opened();
_camera = CreateCamera(_target, Player);
StateDirty();
}
public override void Closed()
{
base.Closed();
_entityManager.DeleteEntity(_camera);
}
public override void HandleMessage(EuiMessageBase msg)
{
base.HandleMessage(msg);
switch (msg)
{
case AdminCameraFollowMessage:
if (!_admin.HasAdminFlag(Player, AdminFlags.Admin) || Player.AttachedEntity == null)
return;
_follower.StartFollowingEntity(Player.AttachedEntity.Value, _target);
break;
default:
break;
}
}
public override EuiStateBase GetNewState()
{
var name = _entityManager.GetComponent<MetaDataComponent>(_target).EntityName;
var netEnt = _entityManager.GetNetEntity(_camera);
return new AdminCameraEuiState(netEnt, name, _timing.CurTick);
}
private EntityUid CreateCamera(EntityUid target, ICommonSession observer)
{
// Spawn a camera entity attached to the target.
var coords = target.ToCoordinates();
var camera = _entityManager.SpawnAttachedTo(CameraProtoId, coords);
// Allow the user to see the entities near the camera.
// This also force sends the camera entity to the user, overriding the visibility flags.
// (The camera entity has its visibility flags set to VisibilityFlags.Admin so that cheat clients can't see it)
_viewSubscriber.AddViewSubscriber(camera, observer);
return camera;
}
}

View File

@@ -96,7 +96,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
EntityManager.AddComponents(ent, injectedAnom.Components); EntityManager.AddComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true); _stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true); _jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true);
if (ent.Comp.StartSound is not null) if (ent.Comp.StartSound is not null)
@@ -125,7 +125,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args) private void OnAnomalyPulse(Entity<InnerBodyAnomalyComponent> ent, ref AnomalyPulseEvent args)
{ {
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true); _stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity));
_jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true); _jitter.DoJitter(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration / 2 * args.Severity), true);
} }
@@ -213,7 +213,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom)) if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
EntityManager.RemoveComponents(ent, injectedAnom.Components); EntityManager.RemoveComponents(ent, injectedAnom.Components);
_stun.TryParalyze(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration), true); _stun.TryUpdateParalyzeDuration(ent, TimeSpan.FromSeconds(ent.Comp.StunDuration));
if (ent.Comp.EndMessage is not null && if (ent.Comp.EndMessage is not null &&
_mind.TryGetMind(ent, out _, out var mindComponent) && _mind.TryGetMind(ent, out _, out var mindComponent) &&

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Timing; using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.DeviceNetwork.Components; using Content.Shared.DeviceNetwork.Components;
using Content.Shared.NodeContainer; using Content.Shared.NodeContainer;
@@ -53,6 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
// Grid events // Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit); SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
SubscribeLocalEvent<PipeNodeGroupRemovedEvent>(OnPipeNodeGroupRemoved);
} }
#region Event handling #region Event handling
@@ -295,6 +297,25 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
#region Pipe net functions #region Pipe net functions
private void OnPipeNodeGroupRemoved(ref PipeNodeGroupRemovedEvent args)
{
// When a pipe node group is removed, we need to iterate over all of
// our pipe chunks and remove any entries with a matching net id.
// (We only need to check the chunks for the affected grid, though.)
if (!_gridAtmosPipeChunks.TryGetValue(args.Grid, out var chunkData))
return;
foreach (var chunk in chunkData.Values)
{
foreach (var key in chunk.AtmosPipeData.Keys)
{
if (key.NetId == args.NetId)
chunk.AtmosPipeData.Remove(key);
}
}
}
private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid) private void RebuildAtmosPipeGrid(EntityUid gridUid, MapGridComponent grid)
{ {
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>(); var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
@@ -411,7 +432,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
continue; continue;
var netId = GetPipeNodeNetId(pipeNode); var netId = GetPipeNodeNetId(pipeNode);
var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color.ToHex()); var subnet = new AtmosMonitoringConsoleSubnet(netId, pipeNode.CurrentPipeLayer, pipeColor.Color);
var pipeDirection = pipeNode.CurrentPipeDirection; var pipeDirection = pipeNode.CurrentPipeDirection;
chunk.AtmosPipeData.TryGetValue(subnet, out var atmosPipeData); chunk.AtmosPipeData.TryGetValue(subnet, out var atmosPipeData);

View File

@@ -279,6 +279,14 @@ public partial class AtmosphereSystem
public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet) public bool RemovePipeNet(Entity<GridAtmosphereComponent?> grid, PipeNet pipeNet)
{ {
// Technically this event can be fired even on grids that don't
// actually have grid atmospheres.
if (pipeNet.Grid is not null)
{
var ev = new PipeNodeGroupRemovedEvent(grid, pipeNet.NetId);
RaiseLocalEvent(ref ev);
}
return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet); return _atmosQuery.Resolve(grid, ref grid.Comp, false) && grid.Comp.PipeNets.Remove(pipeNet);
} }
@@ -329,3 +337,12 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct IsHotspotActiveMethodEvent [ByRefEvent] private record struct IsHotspotActiveMethodEvent
(EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false); (EntityUid Grid, Vector2i Tile, bool Result = false, bool Handled = false);
} }
/// <summary>
/// Raised broadcasted when a pipe node group within a grid has been removed.
/// </summary>
/// <param name="Grid">The grid with the removed node group.</param>
/// <param name="NetId">The net id of the removed node group.</param>
[ByRefEvent]
public record struct PipeNodeGroupRemovedEvent(EntityUid Grid, int NetId);

View File

@@ -409,7 +409,7 @@ namespace Content.Server.Atmos.EntitySystems
flammable.Resisting = true; flammable.Resisting = true;
_popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid); _popup.PopupEntity(Loc.GetString("flammable-component-resist-message"), uid, uid);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(2f), true); _stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(2f));
// TODO FLAMMABLE: Make this not use TimerComponent... // TODO FLAMMABLE: Make this not use TimerComponent...
uid.SpawnTimer(2000, () => uid.SpawnTimer(2000, () =>

View File

@@ -48,7 +48,7 @@ public sealed partial class AtmosAlarmableComponent : Component
public HashSet<string> SyncWithTags { get; private set; } = new(); public HashSet<string> SyncWithTags { get; private set; } = new();
[DataField("monitorAlertTypes")] [DataField("monitorAlertTypes")]
public HashSet<AtmosMonitorThresholdType>? MonitorAlertTypes { get; private set; } public AtmosMonitorThresholdTypeFlags MonitorAlertTypes { get; private set; }
/// <summary> /// <summary>
/// If this device should receive only. If it can only /// If this device should receive only. If it can only

View File

@@ -59,7 +59,7 @@ public sealed partial class AtmosMonitorComponent : Component
public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal; public AtmosAlarmType LastAlarmState = AtmosAlarmType.Normal;
[DataField("trippedThresholds")] [DataField("trippedThresholds")]
public HashSet<AtmosMonitorThresholdType> TrippedThresholds = new(); public AtmosMonitorThresholdTypeFlags TrippedThresholds;
/// <summary> /// <summary>
/// Registered devices in this atmos monitor. Alerts will be sent directly /// Registered devices in this atmos monitor. Alerts will be sent directly

View File

@@ -108,9 +108,9 @@ public sealed class AtmosAlarmableSystem : EntitySystem
break; break;
} }
if (args.Data.TryGetValue(AlertTypes, out HashSet<AtmosMonitorThresholdType>? types) && component.MonitorAlertTypes != null) if (args.Data.TryGetValue(AlertTypes, out AtmosMonitorThresholdTypeFlags types) && component.MonitorAlertTypes != AtmosMonitorThresholdTypeFlags.None)
{ {
isValid = types.Any(type => component.MonitorAlertTypes.Contains(type)); isValid = (types & component.MonitorAlertTypes) != 0;
} }
if (!component.NetworkAlarmStates.ContainsKey(args.SenderAddress)) if (!component.NetworkAlarmStates.ContainsKey(args.SenderAddress))

View File

@@ -207,7 +207,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (component.MonitorFire if (component.MonitorFire
&& component.LastAlarmState != AtmosAlarmType.Danger) && component.LastAlarmState != AtmosAlarmType.Danger)
{ {
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature); component.TrippedThresholds |= AtmosMonitorThresholdTypeFlags.Temperature;
Alert(uid, AtmosAlarmType.Danger, null, component); // technically??? Alert(uid, AtmosAlarmType.Danger, null, component); // technically???
} }
@@ -218,7 +218,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
&& component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState) && component.TemperatureThreshold.CheckThreshold(args.Temperature, out var temperatureState)
&& temperatureState > component.LastAlarmState) && temperatureState > component.LastAlarmState)
{ {
component.TrippedThresholds.Add(AtmosMonitorThresholdType.Temperature); component.TrippedThresholds |= AtmosMonitorThresholdTypeFlags.Temperature;
Alert(uid, AtmosAlarmType.Danger, null, component); Alert(uid, AtmosAlarmType.Danger, null, component);
} }
} }
@@ -259,7 +259,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (!Resolve(uid, ref monitor)) return; if (!Resolve(uid, ref monitor)) return;
var state = AtmosAlarmType.Normal; var state = AtmosAlarmType.Normal;
HashSet<AtmosMonitorThresholdType> alarmTypes = new(monitor.TrippedThresholds); var alarmTypes = monitor.TrippedThresholds;
if (monitor.TemperatureThreshold != null if (monitor.TemperatureThreshold != null
&& monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState)) && monitor.TemperatureThreshold.CheckThreshold(air.Temperature, out var temperatureState))
@@ -267,11 +267,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (temperatureState > state) if (temperatureState > state)
{ {
state = temperatureState; state = temperatureState;
alarmTypes.Add(AtmosMonitorThresholdType.Temperature); alarmTypes |= AtmosMonitorThresholdTypeFlags.Temperature;
} }
else if (temperatureState == AtmosAlarmType.Normal) else if (temperatureState == AtmosAlarmType.Normal)
{ {
alarmTypes.Remove(AtmosMonitorThresholdType.Temperature); alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Temperature;
} }
} }
@@ -282,11 +282,11 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (pressureState > state) if (pressureState > state)
{ {
state = pressureState; state = pressureState;
alarmTypes.Add(AtmosMonitorThresholdType.Pressure); alarmTypes |= AtmosMonitorThresholdTypeFlags.Pressure;
} }
else if (pressureState == AtmosAlarmType.Normal) else if (pressureState == AtmosAlarmType.Normal)
{ {
alarmTypes.Remove(AtmosMonitorThresholdType.Pressure); alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Pressure;
} }
} }
@@ -306,17 +306,17 @@ public sealed class AtmosMonitorSystem : EntitySystem
if (tripped) if (tripped)
{ {
alarmTypes.Add(AtmosMonitorThresholdType.Gas); alarmTypes |= AtmosMonitorThresholdTypeFlags.Gas;
} }
else else
{ {
alarmTypes.Remove(AtmosMonitorThresholdType.Gas); alarmTypes &= ~AtmosMonitorThresholdTypeFlags.Gas;
} }
} }
// if the state of the current air doesn't match the last alarm state, // if the state of the current air doesn't match the last alarm state,
// we update the state // we update the state
if (state != monitor.LastAlarmState || !alarmTypes.SetEquals(monitor.TrippedThresholds)) if (state != monitor.LastAlarmState || alarmTypes != monitor.TrippedThresholds)
{ {
Alert(uid, state, alarmTypes, monitor); Alert(uid, state, alarmTypes, monitor);
} }
@@ -327,7 +327,7 @@ public sealed class AtmosMonitorSystem : EntitySystem
/// </summary> /// </summary>
/// <param name="state">The alarm state to set this monitor to.</param> /// <param name="state">The alarm state to set this monitor to.</param>
/// <param name="alarms">The alarms that caused this alarm state.</param> /// <param name="alarms">The alarms that caused this alarm state.</param>
public void Alert(EntityUid uid, AtmosAlarmType state, HashSet<AtmosMonitorThresholdType>? alarms = null, AtmosMonitorComponent? monitor = null) public void Alert(EntityUid uid, AtmosAlarmType state, AtmosMonitorThresholdTypeFlags? alarms = null, AtmosMonitorComponent? monitor = null)
{ {
if (!Resolve(uid, ref monitor)) if (!Resolve(uid, ref monitor))
return; return;

View File

@@ -1,48 +0,0 @@
using Content.Server.Atmos.Piping.Components;
using Content.Shared.Atmos.Piping;
using Robust.Server.GameObjects;
namespace Content.Server.Atmos.Piping.EntitySystems
{
public sealed class AtmosPipeColorSystem : EntitySystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentStartup>(OnStartup);
SubscribeLocalEvent<AtmosPipeColorComponent, ComponentShutdown>(OnShutdown);
}
private void OnStartup(EntityUid uid, AtmosPipeColorComponent component, ComponentStartup args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, component.Color, appearance);
}
private void OnShutdown(EntityUid uid, AtmosPipeColorComponent component, ComponentShutdown args)
{
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, Color.White, appearance);
}
public void SetColor(EntityUid uid, AtmosPipeColorComponent component, Color color)
{
component.Color = color;
if (!TryComp(uid, out AppearanceComponent? appearance))
return;
_appearance.SetData(uid, PipeColorVisuals.Color, color, appearance);
var ev = new AtmosPipeColorChangedEvent(color);
RaiseLocalEvent(uid, ref ev);
}
}
}

View File

@@ -12,110 +12,109 @@ namespace Content.Server.Chat.Managers;
/// </summary> /// </summary>
public sealed class ChatSanitizationManager : IChatSanitizationManager public sealed class ChatSanitizationManager : IChatSanitizationManager
{ {
private static readonly Dictionary<string, string> ShorthandToEmote = new() private static readonly (Regex regex, string emoteKey)[] ShorthandToEmote =
{ [
// CP14-RU-Localization-Start // CP14-RU-Localization-Start
{ "лол", "chatsan-laughs" }, Entry( "лол", "chatsan-laughs" ),
{ "хд", "chatsan-laughs" }, Entry( "хд", "chatsan-laughs" ),
{ "о-о", "chatsan-wide-eyed" }, // cyrillic о Entry( "о-о", "chatsan-wide-eyed" ), // cyrillic о
{ "о.о", "chatsan-wide-eyed" }, // cyrillic о Entry( "о.о", "chatsan-wide-eyed" ), // cyrillic о
{ "0_о", "chatsan-wide-eyed" }, // cyrillic о Entry( "0_о", "chatsan-wide-eyed" ), // cyrillic о
{ "о/", "chatsan-waves" }, // cyrillic о Entry( "о/", "chatsan-waves" ), // cyrillic о
{ "о7", "chatsan-salutes" }, // cyrillic о Entry( "о7", "chatsan-salutes" ), // cyrillic о
{ "0_o", "chatsan-wide-eyed" }, Entry( "0_o", "chatsan-wide-eyed" ),
{ "лмао", "chatsan-laughs" }, Entry( "лмао", "chatsan-laughs" ),
{ "рофл", "chatsan-laughs" }, Entry( "рофл", "chatsan-laughs" ),
{ "яхз", "chatsan-shrugs" }, Entry( "яхз", "chatsan-shrugs" ),
{ ":0", "chatsan-surprised" }, Entry( ":0", "chatsan-surprised" ),
{ ":р", "chatsan-stick-out-tongue" }, // cyrillic р Entry( ":р", "chatsan-stick-out-tongue" ), // cyrillic р
{ "кек", "chatsan-laughs" }, Entry( "кек", "chatsan-laughs" ),
{ "T_T", "chatsan-cries" }, Entry( "T_T", "chatsan-cries" ),
{ "Т_Т", "chatsan-cries" }, // cyrillic T Entry( "Т_Т", "chatsan-cries" ), // cyrillic T
{ "=_(", "chatsan-cries" }, Entry( "=_(", "chatsan-cries" ),
{ "))", "chatsan-smiles-widely" }, Entry( "))", "chatsan-smiles-widely" ),
{ ")", "chatsan-smiles" }, Entry( ")", "chatsan-smiles" ),
{ "((", "chatsan-frowns-deeply" }, Entry( "((", "chatsan-frowns-deeply" ),
{ "(", "chatsan-frowns" }, Entry( "(", "chatsan-frowns" ),
// CP14-RU-Localization-End // CP14-RU-Localization-End
// I could've done this with regex, but felt it wasn't the right idea. Entry(":)", "chatsan-smiles"),
{ ":)", "chatsan-smiles" }, Entry(":]", "chatsan-smiles"),
{ ":]", "chatsan-smiles" }, Entry("=)", "chatsan-smiles"),
{ "=)", "chatsan-smiles" }, Entry("=]", "chatsan-smiles"),
{ "=]", "chatsan-smiles" }, Entry("(:", "chatsan-smiles"),
{ "(:", "chatsan-smiles" }, Entry("[:", "chatsan-smiles"),
{ "[:", "chatsan-smiles" }, Entry("(=", "chatsan-smiles"),
{ "(=", "chatsan-smiles" }, Entry("[=", "chatsan-smiles"),
{ "[=", "chatsan-smiles" }, Entry("^^", "chatsan-smiles"),
{ "^^", "chatsan-smiles" }, Entry("^-^", "chatsan-smiles"),
{ "^-^", "chatsan-smiles" }, Entry(":(", "chatsan-frowns"),
{ ":(", "chatsan-frowns" }, Entry(":[", "chatsan-frowns"),
{ ":[", "chatsan-frowns" }, Entry("=(", "chatsan-frowns"),
{ "=(", "chatsan-frowns" }, Entry("=[", "chatsan-frowns"),
{ "=[", "chatsan-frowns" }, Entry("):", "chatsan-frowns"),
{ "):", "chatsan-frowns" }, Entry(")=", "chatsan-frowns"),
{ ")=", "chatsan-frowns" }, Entry("]:", "chatsan-frowns"),
{ "]:", "chatsan-frowns" }, Entry("]=", "chatsan-frowns"),
{ "]=", "chatsan-frowns" }, Entry(":D", "chatsan-smiles-widely"),
{ ":D", "chatsan-smiles-widely" }, Entry("D:", "chatsan-frowns-deeply"),
{ "D:", "chatsan-frowns-deeply" }, Entry(":O", "chatsan-surprised"),
{ ":O", "chatsan-surprised" }, Entry(":3", "chatsan-smiles"),
{ ":3", "chatsan-smiles" }, Entry(":S", "chatsan-uncertain"),
{ ":S", "chatsan-uncertain" }, Entry(":>", "chatsan-grins"),
{ ":>", "chatsan-grins" }, Entry(":<", "chatsan-pouts"),
{ ":<", "chatsan-pouts" }, Entry("xD", "chatsan-laughs"),
{ "xD", "chatsan-laughs" }, Entry(":'(", "chatsan-cries"),
{ ":'(", "chatsan-cries" }, Entry(":'[", "chatsan-cries"),
{ ":'[", "chatsan-cries" }, Entry("='(", "chatsan-cries"),
{ "='(", "chatsan-cries" }, Entry("='[", "chatsan-cries"),
{ "='[", "chatsan-cries" }, Entry(")':", "chatsan-cries"),
{ ")':", "chatsan-cries" }, Entry("]':", "chatsan-cries"),
{ "]':", "chatsan-cries" }, Entry(")'=", "chatsan-cries"),
{ ")'=", "chatsan-cries" }, Entry("]'=", "chatsan-cries"),
{ "]'=", "chatsan-cries" }, Entry(";-;", "chatsan-cries"),
{ ";-;", "chatsan-cries" }, Entry(";_;", "chatsan-cries"),
{ ";_;", "chatsan-cries" }, Entry("qwq", "chatsan-cries"),
{ "qwq", "chatsan-cries" }, Entry(":u", "chatsan-smiles-smugly"),
{ ":u", "chatsan-smiles-smugly" }, Entry(":v", "chatsan-smiles-smugly"),
{ ":v", "chatsan-smiles-smugly" }, Entry(">:i", "chatsan-annoyed"),
{ ">:i", "chatsan-annoyed" }, Entry(":i", "chatsan-sighs"),
{ ":i", "chatsan-sighs" }, Entry(":|", "chatsan-sighs"),
{ ":|", "chatsan-sighs" }, Entry(":p", "chatsan-stick-out-tongue"),
{ ":p", "chatsan-stick-out-tongue" }, Entry(";p", "chatsan-stick-out-tongue"),
{ ";p", "chatsan-stick-out-tongue" }, Entry(":b", "chatsan-stick-out-tongue"),
{ ":b", "chatsan-stick-out-tongue" }, Entry("0-0", "chatsan-wide-eyed"),
{ "0-0", "chatsan-wide-eyed" }, Entry("o-o", "chatsan-wide-eyed"),
{ "o-o", "chatsan-wide-eyed" }, Entry("o.o", "chatsan-wide-eyed"),
{ "o.o", "chatsan-wide-eyed" }, Entry("._.", "chatsan-surprised"),
{ "._.", "chatsan-surprised" }, Entry(".-.", "chatsan-confused"),
{ ".-.", "chatsan-confused" }, Entry("-_-", "chatsan-unimpressed"),
{ "-_-", "chatsan-unimpressed" }, Entry("smh", "chatsan-unimpressed"),
{ "smh", "chatsan-unimpressed" }, Entry("o/", "chatsan-waves"),
{ "o/", "chatsan-waves" }, Entry("^^/", "chatsan-waves"),
{ "^^/", "chatsan-waves" }, Entry(":/", "chatsan-uncertain"),
{ ":/", "chatsan-uncertain" }, Entry(":\\", "chatsan-uncertain"),
{ ":\\", "chatsan-uncertain" }, Entry("lmao", "chatsan-laughs"),
{ "lmao", "chatsan-laughs" }, Entry("lmfao", "chatsan-laughs"),
{ "lmfao", "chatsan-laughs" }, Entry("lol", "chatsan-laughs"),
{ "lol", "chatsan-laughs" }, Entry("lel", "chatsan-laughs"),
{ "lel", "chatsan-laughs" }, Entry("kek", "chatsan-laughs"),
{ "kek", "chatsan-laughs" }, Entry("rofl", "chatsan-laughs"),
{ "rofl", "chatsan-laughs" }, Entry("o7", "chatsan-salutes"),
{ "o7", "chatsan-salutes" }, Entry(";_;7", "chatsan-tearfully-salutes"),
{ ";_;7", "chatsan-tearfully-salutes" }, Entry("idk", "chatsan-shrugs"),
{ "idk", "chatsan-shrugs" }, Entry(";)", "chatsan-winks"),
{ ";)", "chatsan-winks" }, Entry(";]", "chatsan-winks"),
{ ";]", "chatsan-winks" }, Entry("(;", "chatsan-winks"),
{ "(;", "chatsan-winks" }, Entry("[;", "chatsan-winks"),
{ "[;", "chatsan-winks" }, Entry(":')", "chatsan-tearfully-smiles"),
{ ":')", "chatsan-tearfully-smiles" }, Entry(":']", "chatsan-tearfully-smiles"),
{ ":']", "chatsan-tearfully-smiles" }, Entry("=')", "chatsan-tearfully-smiles"),
{ "=')", "chatsan-tearfully-smiles" }, Entry("=']", "chatsan-tearfully-smiles"),
{ "=']", "chatsan-tearfully-smiles" }, Entry("(':", "chatsan-tearfully-smiles"),
{ "(':", "chatsan-tearfully-smiles" }, Entry("[':", "chatsan-tearfully-smiles"),
{ "[':", "chatsan-tearfully-smiles" }, Entry("('=", "chatsan-tearfully-smiles"),
{ "('=", "chatsan-tearfully-smiles" }, Entry("['=", "chatsan-tearfully-smiles"),
{ "['=", "chatsan-tearfully-smiles" } ];
};
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ILocalizationManager _loc = default!;
@@ -149,21 +148,8 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
// -1 is just a canary for nothing found yet // -1 is just a canary for nothing found yet
var lastEmoteIndex = -1; var lastEmoteIndex = -1;
foreach (var (shorthand, emoteKey) in ShorthandToEmote) foreach (var (r, emoteKey) in ShorthandToEmote)
{ {
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern =
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))";
var r = new Regex(pattern, RegexOptions.RightToLeft | RegexOptions.IgnoreCase);
// We're using sanitized as the original message until the end so that we can make sure the indices of // We're using sanitized as the original message until the end so that we can make sure the indices of
// the emotes are accurate. // the emotes are accurate.
var lastMatch = r.Match(sanitized); var lastMatch = r.Match(sanitized);
@@ -183,4 +169,21 @@ public sealed class ChatSanitizationManager : IChatSanitizationManager
sanitized = message.Trim(); sanitized = message.Trim();
return emote is not null; return emote is not null;
} }
private static (Regex regex, string emoteKey) Entry(string shorthand, string emoteKey)
{
// We have to escape it because shorthands like ":)" or "-_-" would break the regex otherwise.
var escaped = Regex.Escape(shorthand);
// So there are 2 cases:
// - If there is whitespace before it and after it is either punctuation, whitespace, or the end of the line
// Delete the word and the whitespace before
// - If it is at the start of the string and is followed by punctuation, whitespace, or the end of the line
// Delete the word and the punctuation if it exists.
var pattern = new Regex(
$@"\s{escaped}(?=\p{{P}}|\s|$)|^{escaped}(?:\p{{P}}|(?=\s|$))",
RegexOptions.RightToLeft | RegexOptions.IgnoreCase | RegexOptions.Compiled);
return (pattern, emoteKey);
}
} }

View File

@@ -101,7 +101,7 @@ public sealed class CluwneSystem : EntitySystem
else if (_robustRandom.Prob(component.KnockChance)) else if (_robustRandom.Prob(component.KnockChance))
{ {
_audio.PlayPvs(component.KnockSound, uid); _audio.PlayPvs(component.KnockSound, uid);
_stunSystem.TryParalyze(uid, TimeSpan.FromSeconds(component.ParalyzeTime), true); _stunSystem.TryUpdateParalyzeDuration(uid, TimeSpan.FromSeconds(component.ParalyzeTime));
_chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal); _chat.TrySendInGameICMessage(uid, "spasms", InGameICChatType.Emote, ChatTransmitRange.Normal);
} }
} }

View File

@@ -42,7 +42,6 @@ namespace Content.Server.Communications
{ {
// All events that refresh the BUI // All events that refresh the BUI
SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged); SubscribeLocalEvent<AlertLevelChangedEvent>(OnAlertLevelChanged);
SubscribeLocalEvent<CommunicationsConsoleComponent, ComponentInit>((uid, comp, _) => UpdateCommsConsoleInterface(uid, comp));
SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent()); SubscribeLocalEvent<RoundEndSystemChangedEvent>(_ => OnGenericBroadcastEvent());
SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent()); SubscribeLocalEvent<AlertLevelDelayFinishedEvent>(_ => OnGenericBroadcastEvent());
@@ -85,6 +84,7 @@ namespace Content.Server.Communications
public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args) public void OnCommunicationsConsoleMapInit(EntityUid uid, CommunicationsConsoleComponent comp, MapInitEvent args)
{ {
comp.AnnouncementCooldownRemaining = comp.InitialDelay; comp.AnnouncementCooldownRemaining = comp.InitialDelay;
UpdateCommsConsoleInterface(uid, comp);
} }
/// <summary> /// <summary>

View File

@@ -1,4 +1,4 @@
using Content.Shared.Bed.Sleep; using Content.Shared.Bed.Sleep;
using Content.Shared.Drowsiness; using Content.Shared.Drowsiness;
using Content.Shared.StatusEffectNew; using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components; using Content.Shared.StatusEffectNew.Components;

View File

@@ -397,7 +397,12 @@ public sealed class ElectrocutionSystem : SharedElectrocutionSystem
var shouldStun = siemensCoefficient > 0.5f; var shouldStun = siemensCoefficient > 0.5f;
if (shouldStun) if (shouldStun)
_stun.TryParalyze(uid, time * ParalyzeTimeMultiplier, refresh, statusEffects); {
_ = refresh
? _stun.TryUpdateParalyzeDuration(uid, time * ParalyzeTimeMultiplier)
: _stun.TryAddParalyzeDuration(uid, time * ParalyzeTimeMultiplier);
}
// TODO: Sparks here. // TODO: Sparks here.

View File

@@ -1,5 +0,0 @@
using System.Threading;
using Content.Shared.Ensnaring.Components;

View File

@@ -133,7 +133,7 @@ public sealed class EntityEffectSystem : EntitySystem
args.Result = false; args.Result = false;
if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp)) if (TryComp(args.Args.TargetEntity, out TemperatureComponent? temp))
{ {
if (temp.CurrentTemperature > args.Condition.Min && temp.CurrentTemperature < args.Condition.Max) if (temp.CurrentTemperature >= args.Condition.Min && temp.CurrentTemperature <= args.Condition.Max)
args.Result = true; args.Result = true;
} }
} }

View File

@@ -339,6 +339,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Ensure we actually have the component // Ensure we actually have the component
EnsureComp<TileFrictionModifierComponent>(entity); EnsureComp<TileFrictionModifierComponent>(entity);
EnsureComp<SlipperyComponent>(entity, out var slipComp);
// This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on // This is the base amount of reagent needed before a puddle can be considered slippery. Is defined based on
// the sprite threshold for a puddle larger than 5 pixels. // the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold); var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@@ -357,17 +359,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var launchMult = FixedPoint2.Zero; var launchMult = FixedPoint2.Zero;
// A cumulative weighted amount of stun times from slippery reagents // A cumulative weighted amount of stun times from slippery reagents
var stunTimer = TimeSpan.Zero; var stunTimer = TimeSpan.Zero;
// A cumulative weighted amount of knockdown times from slippery reagents
var knockdownTimer = TimeSpan.Zero;
// Check if the puddle is big enough to slip in to avoid doing unnecessary logic // Check if the puddle is big enough to slip in to avoid doing unnecessary logic
if (solution.Volume <= smallPuddleThreshold) if (solution.Volume <= smallPuddleThreshold)
{ {
_stepTrigger.SetActive(entity, false, comp); _stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, 1f); _tile.SetModifier(entity, 1f);
slipComp.SlipData.SlipFriction = 1f;
slipComp.AffectsSliding = false;
Dirty(entity, slipComp);
return; return;
} }
if (!TryComp<SlipperyComponent>(entity, out var slipComp)) slipComp.AffectsSliding = true;
return;
foreach (var (reagent, quantity) in solution.Contents) foreach (var (reagent, quantity) in solution.Contents)
{ {
@@ -387,7 +393,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Aggregate launch speed based on quantity // Aggregate launch speed based on quantity
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity; launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * quantity;
// Aggregate stun times based on quantity // Aggregate stun times based on quantity
stunTimer += reagentProto.SlipData.ParalyzeTime * (float)quantity; stunTimer += reagentProto.SlipData.StunTime * (float)quantity;
knockdownTimer += reagentProto.SlipData.KnockdownTime * (float)quantity;
if (reagentProto.SlipData.SuperSlippery) if (reagentProto.SlipData.SuperSlippery)
superSlipperyUnits += quantity; superSlipperyUnits += quantity;
@@ -405,8 +412,9 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount. // A puddle with 10 units of lube vs a puddle with 10 of lube and 20 catchup should stun and launch forward the same amount.
if (slipperyUnits > 0) if (slipperyUnits > 0)
{ {
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits); slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/slipperyUnits);
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits; slipComp.SlipData.StunTime = (stunTimer/(float)slipperyUnits);
slipComp.SlipData.KnockdownTime = (knockdownTimer/(float)slipperyUnits);
} }
// Only make it super slippery if there is enough super slippery units for its own puddle // Only make it super slippery if there is enough super slippery units for its own puddle

View File

@@ -228,7 +228,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
continue; continue;
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction); _npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
_stun.TryParalyze(uid, stunTime, true); _stun.TryUpdateParalyzeDuration(uid, stunTime);
RemCompDeferred<RevolutionaryComponent>(uid); RemCompDeferred<RevolutionaryComponent>(uid);
_popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid); _popup.PopupEntity(Loc.GetString("rev-break-control", ("name", Identity.Entity(uid, EntityManager))), uid);
_adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying."); _adminLogManager.Add(LogType.Mind, LogImpact.Medium, $"{ToPrettyString(uid)} was deconverted due to all Head Revolutionaries dying.");

View File

@@ -125,8 +125,8 @@ public sealed class TraitorRuleSystem : GameRuleSystem<TraitorRuleComponent>
if (traitorRole is not null) if (traitorRole is not null)
{ {
Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Add traitor briefing components"); Log.Debug($"MakeTraitor {ToPrettyString(traitor)} - Add traitor briefing components");
AddComp<RoleBriefingComponent>(traitorRole.Value.Owner); EnsureComp<RoleBriefingComponent>(traitorRole.Value.Owner, out var briefingComp);
Comp<RoleBriefingComponent>(traitorRole.Value.Owner).Briefing = briefing; briefingComp.Briefing = briefing;
} }
else else
{ {

View File

@@ -5,7 +5,6 @@ using Content.Server.Ghost.Roles.Components;
using Content.Server.Ghost.Roles.Events; using Content.Server.Ghost.Roles.Events;
using Content.Shared.Ghost.Roles.Raffles; using Content.Shared.Ghost.Roles.Raffles;
using Content.Server.Ghost.Roles.UI; using Content.Server.Ghost.Roles.UI;
using Content.Server.Mind.Commands;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.CCVar; using Content.Shared.CCVar;
using Content.Shared.Database; using Content.Shared.Database;
@@ -698,7 +697,7 @@ public sealed class GhostRoleSystem : EntitySystem
RaiseLocalEvent(mob, spawnedEvent); RaiseLocalEvent(mob, spawnedEvent);
if (ghostRole.MakeSentient) if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(mob, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech); _mindSystem.MakeSentient(mob, ghostRole.AllowMovement, ghostRole.AllowSpeech);
EnsureComp<MindContainerComponent>(mob); EnsureComp<MindContainerComponent>(mob);
@@ -745,7 +744,7 @@ public sealed class GhostRoleSystem : EntitySystem
} }
if (ghostRole.MakeSentient) if (ghostRole.MakeSentient)
MakeSentientCommand.MakeSentient(uid, EntityManager, ghostRole.AllowMovement, ghostRole.AllowSpeech); _mindSystem.MakeSentient(uid, ghostRole.AllowMovement, ghostRole.AllowSpeech);
GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole); GhostRoleInternalCreateMindAndTransfer(args.Player, uid, uid, ghostRole);
UnregisterGhostRole((uid, ghostRole)); UnregisterGhostRole((uid, ghostRole));

View File

@@ -461,7 +461,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
{ {
if (instrument.InstrumentPlayer is {Valid: true} mob) if (instrument.InstrumentPlayer is {Valid: true} mob)
{ {
_stuns.TryParalyze(mob, TimeSpan.FromSeconds(1), true); _stuns.TryUpdateParalyzeDuration(mob, TimeSpan.FromSeconds(1));
_popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"), _popup.PopupEntity(Loc.GetString("instrument-component-finger-cramps-max-message"),
uid, mob, PopupType.LargeCaution); uid, mob, PopupType.LargeCaution);

View File

@@ -2,16 +2,15 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems; using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics; using Content.Server.Forensics;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Body.Components; using Content.Shared.Body.Components;
using Content.Shared.Body.Systems; using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement; using Content.Shared.IdentityManagement;
using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components; using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems; using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.StatusEffect;
using Robust.Server.Audio; using Robust.Server.Audio;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
@@ -27,7 +26,7 @@ namespace Content.Server.Medical
[Dependency] private readonly PopupSystem _popup = default!; [Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddle = default!; [Dependency] private readonly PuddleSystem _puddle = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly StunSystem _stun = default!; [Dependency] private readonly MovementModStatusSystem _movementMod = default!;
[Dependency] private readonly ThirstSystem _thirst = default!; [Dependency] private readonly ThirstSystem _thirst = default!;
[Dependency] private readonly ForensicsSystem _forensics = default!; [Dependency] private readonly ForensicsSystem _forensics = default!;
[Dependency] private readonly BloodstreamSystem _bloodstream = default!; [Dependency] private readonly BloodstreamSystem _bloodstream = default!;
@@ -57,8 +56,7 @@ namespace Content.Server.Medical
// It fully empties the stomach, this amount from the chem stream is relatively small // It fully empties the stomach, this amount from the chem stream is relatively small
var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6; var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6;
// Apply a bit of slowdown // Apply a bit of slowdown
if (TryComp<StatusEffectsComponent>(uid, out var status)) _movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f);
_stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status);
// TODO: Need decals // TODO: Need decals
var solution = new Solution(); var solution = new Solution();

View File

@@ -1,63 +1,30 @@
using Content.Server.Administration; using Content.Server.Administration;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.Emoting;
using Content.Shared.Examine;
using Content.Shared.Mind.Components;
using Content.Shared.Movement.Components;
using Content.Shared.Speech;
using Robust.Shared.Console; using Robust.Shared.Console;
namespace Content.Server.Mind.Commands namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeSentientCommand : LocalizedEntityCommands
{ {
[AdminCommand(AdminFlags.Admin)] [Dependency] private readonly MindSystem _mindSystem = default!;
public sealed class MakeSentientCommand : IConsoleCommand
public override string Command => "makesentient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
[Dependency] private readonly IEntityManager _entManager = default!; if (args.Length != 1)
public string Command => "makesentient";
public string Description => "Makes an entity sentient (able to be controlled by a player)";
public string Help => "makesentient <entity id>";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{ {
if (args.Length != 1) shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
{ return;
shell.WriteLine("Wrong number of arguments.");
return;
}
if (!NetEntity.TryParse(args[0], out var entNet) || !_entManager.TryGetEntity(entNet, out var entId))
{
shell.WriteLine("Invalid argument.");
return;
}
if (!_entManager.EntityExists(entId))
{
shell.WriteLine("Invalid entity specified!");
return;
}
MakeSentient(entId.Value, _entManager, true, true);
} }
public static void MakeSentient(EntityUid uid, IEntityManager entityManager, bool allowMovement = true, bool allowSpeech = true) if (!NetEntity.TryParse(args[0], out var entNet) || !EntityManager.TryGetEntity(entNet, out var entId) || !EntityManager.EntityExists(entId))
{ {
entityManager.EnsureComponent<MindContainerComponent>(uid); shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
if (allowMovement) return;
{
entityManager.EnsureComponent<InputMoverComponent>(uid);
entityManager.EnsureComponent<MobMoverComponent>(uid);
entityManager.EnsureComponent<MovementSpeedModifierComponent>(uid);
}
if (allowSpeech)
{
entityManager.EnsureComponent<SpeechComponent>(uid);
entityManager.EnsureComponent<EmotingComponent>(uid);
}
entityManager.EnsureComponent<ExaminerComponent>(uid);
} }
_mindSystem.MakeSentient(entId.Value);
} }
} }

View File

@@ -1,7 +1,6 @@
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.GameTicking; using Content.Server.GameTicking;
using Content.Server.Ghost; using Content.Server.Ghost;
using Content.Server.Mind.Commands;
using Content.Shared.Database; using Content.Shared.Database;
using Content.Shared.Ghost; using Content.Shared.Ghost;
using Content.Shared.Mind; using Content.Shared.Mind;
@@ -349,7 +348,7 @@ public sealed class MindSystem : SharedMindSystem
return; return;
} }
MakeSentientCommand.MakeSentient(target, EntityManager); MakeSentient(target);
TransferTo(mindId, target, ghostCheckOverride: true, mind: mind); TransferTo(mindId, target, ghostCheckOverride: true, mind: mind);
} }
} }

View File

@@ -65,6 +65,10 @@ public sealed class CritMobActionsSystem : EntitySystem
_quickDialog.OpenDialog(actor.PlayerSession, Loc.GetString("action-name-crit-last-words"), "", _quickDialog.OpenDialog(actor.PlayerSession, Loc.GetString("action-name-crit-last-words"), "",
(string lastWords) => (string lastWords) =>
{ {
// if a person is gibbed/deleted, they can't say last words
if (Deleted(uid))
return;
// Intentionally does not check for muteness // Intentionally does not check for muteness
if (actor.PlayerSession.AttachedEntity != uid if (actor.PlayerSession.AttachedEntity != uid
|| !_mobState.IsCritical(uid)) || !_mobState.IsCritical(uid))

View File

@@ -205,7 +205,7 @@ public sealed partial class NPCCombatSystem
return; return;
} }
_gun.AttemptShoot(uid, gunUid, gun, targetCordinates); _gun.AttemptShoot(uid, gunUid, gun, targetCordinates, comp.Target);
} }
} }
} }

View File

@@ -30,11 +30,16 @@ using Robust.Shared.Timing;
using Robust.Shared.Utility; using Robust.Shared.Utility;
using Content.Shared.Prying.Systems; using Content.Shared.Prying.Systems;
using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.ObjectPool;
using Prometheus;
namespace Content.Server.NPC.Systems; namespace Content.Server.NPC.Systems;
public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
{ {
private static readonly Gauge ActiveSteeringGauge = Metrics.CreateGauge(
"npc_steering_active_count",
"Amount of NPCs trying to actively do steering");
/* /*
* We use context steering to determine which way to move. * We use context steering to determine which way to move.
* This involves creating an array of possible directions and assigning a value for the desireability of each direction. * This involves creating an array of possible directions and assigning a value for the desireability of each direction.
@@ -87,6 +92,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
private object _obstacles = new(); private object _obstacles = new();
private int _activeSteeringCount;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -244,12 +251,15 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
}; };
var curTime = _timing.CurTime; var curTime = _timing.CurTime;
_activeSteeringCount = 0;
Parallel.For(0, index, options, i => Parallel.For(0, index, options, i =>
{ {
var (uid, steering, mover, xform) = npcs[i]; var (uid, steering, mover, xform) = npcs[i];
Steer(uid, steering, mover, xform, frameTime, curTime); Steer(uid, steering, mover, xform, frameTime, curTime);
}); });
ActiveSteeringGauge.Set(_activeSteeringCount);
if (_subscribedSessions.Count > 0) if (_subscribedSessions.Count > 0)
{ {
@@ -324,6 +334,8 @@ public sealed partial class NPCSteeringSystem : SharedNPCSteeringSystem
return; return;
} }
Interlocked.Increment(ref _activeSteeringCount);
var agentRadius = steering.Radius; var agentRadius = steering.Radius;
var worldPos = _transform.GetWorldPosition(xform); var worldPos = _transform.GetWorldPosition(xform);
var (layer, mask) = _physics.GetHardCollision(uid); var (layer, mask) = _physics.GetHardCollision(uid);

View File

@@ -8,6 +8,7 @@ using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems; using Content.Shared.Mobs.Systems;
using Content.Shared.NPC; using Content.Shared.NPC;
using Content.Shared.NPC.Systems; using Content.Shared.NPC.Systems;
using Prometheus;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
using Robust.Shared.Configuration; using Robust.Shared.Configuration;
using Robust.Shared.Player; using Robust.Shared.Player;
@@ -19,6 +20,10 @@ namespace Content.Server.NPC.Systems
/// </summary> /// </summary>
public sealed partial class NPCSystem : EntitySystem public sealed partial class NPCSystem : EntitySystem
{ {
private static readonly Gauge ActiveGauge = Metrics.CreateGauge(
"npc_active_count",
"Amount of NPCs that are actively processing");
[Dependency] private readonly IConfigurationManager _configurationManager = default!; [Dependency] private readonly IConfigurationManager _configurationManager = default!;
[Dependency] private readonly HTNSystem _htn = default!; [Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly MobStateSystem _mobState = default!; [Dependency] private readonly MobStateSystem _mobState = default!;
@@ -138,6 +143,8 @@ namespace Content.Server.NPC.Systems
// Add your system here. // Add your system here.
_htn.UpdateNPC(ref _count, _maxUpdates, frameTime); _htn.UpdateNPC(ref _count, _maxUpdates, frameTime);
ActiveGauge.Set(Count<ActiveNPCComponent>());
} }
public void OnMobStateChange(EntityUid uid, HTNComponent component, MobStateChangedEvent args) public void OnMobStateChange(EntityUid uid, HTNComponent component, MobStateChangedEvent args)

View File

@@ -82,8 +82,8 @@ public sealed class NameIdentifierSystem : EntitySystem
randomVal = set[^1]; randomVal = set[^1];
set.RemoveAt(set.Count - 1); set.RemoveAt(set.Count - 1);
return proto.Prefix is not null return proto.Format is not null
? $"{proto.Prefix}-{randomVal}" ? Loc.GetString(proto.Format, ("number", randomVal))
: $"{randomVal}"; : $"{randomVal}";
} }
@@ -104,8 +104,8 @@ public sealed class NameIdentifierSystem : EntitySystem
ids.Remove(ent.Comp.Identifier)) ids.Remove(ent.Comp.Identifier))
{ {
id = ent.Comp.Identifier; id = ent.Comp.Identifier;
uniqueName = group.Prefix is not null uniqueName = group.Format is not null
? $"{group.Prefix}-{id}" ? Loc.GetString(group.Format, ("number", id))
: $"{id}"; : $"{id}";
} }
else else

View File

@@ -63,7 +63,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
_audio.PlayPvs(comp.Sound, target); _audio.PlayPvs(comp.Sound, target);
_damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid); _damageable.TryChangeDamage(target, comp.StunDamage, false, true, null, origin: uid);
_stun.TryParalyze(target, comp.StunTime, refresh: false); _stun.TryAddParalyzeDuration(target, comp.StunTime);
// short cooldown to prevent instant stunlocking // short cooldown to prevent instant stunlocking
_useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId); _useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId);

View File

@@ -7,6 +7,7 @@ using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems; using Content.Shared.Movement.Systems;
using Content.Shared.Shuttles.Components; using Content.Shared.Shuttles.Components;
using Content.Shared.Shuttles.Systems; using Content.Shared.Shuttles.Systems;
using Prometheus;
using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Components;
using Robust.Shared.Player; using Robust.Shared.Player;
using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent; using DroneConsoleComponent = Content.Server.Shuttles.DroneConsoleComponent;
@@ -17,6 +18,10 @@ namespace Content.Server.Physics.Controllers;
public sealed class MoverController : SharedMoverController public sealed class MoverController : SharedMoverController
{ {
private static readonly Gauge ActiveMoverGauge = Metrics.CreateGauge(
"physics_active_mover_count",
"Active amount of InputMovers being processed by MoverController");
[Dependency] private readonly ThrusterSystem _thruster = default!; [Dependency] private readonly ThrusterSystem _thruster = default!;
[Dependency] private readonly SharedTransformSystem _xformSystem = default!; [Dependency] private readonly SharedTransformSystem _xformSystem = default!;
@@ -97,6 +102,8 @@ public sealed class MoverController : SharedMoverController
HandleMobMovement(mover, frameTime); HandleMobMovement(mover, frameTime);
} }
ActiveMoverGauge.Set(_movers.Count);
HandleShuttleMovement(frameTime); HandleShuttleMovement(frameTime);
} }

View File

@@ -1,4 +1,3 @@
using Content.Server.Atmos.Components;
using Content.Server.Atmos.EntitySystems; using Content.Server.Atmos.EntitySystems;
using Content.Server.Storage.EntitySystems; using Content.Server.Storage.EntitySystems;
using Content.Server.Stunnable; using Content.Server.Stunnable;
@@ -7,7 +6,6 @@ using Content.Shared.Atmos.Components;
using Content.Shared.Containers.ItemSlots; using Content.Shared.Containers.ItemSlots;
using Content.Shared.Interaction; using Content.Shared.Interaction;
using Content.Shared.PneumaticCannon; using Content.Shared.PneumaticCannon;
using Content.Shared.StatusEffect;
using Content.Shared.Tools.Systems; using Content.Shared.Tools.Systems;
using Content.Shared.Weapons.Ranged.Components; using Content.Shared.Weapons.Ranged.Components;
using Content.Shared.Weapons.Ranged.Events; using Content.Shared.Weapons.Ranged.Events;
@@ -80,10 +78,9 @@ public sealed class PneumaticCannonSystem : SharedPneumaticCannonSystem
if (gas == null && component.GasUsage > 0f) if (gas == null && component.GasUsage > 0f)
return; return;
if (TryComp<StatusEffectsComponent>(args.User, out var status) if (component.Power == PneumaticCannonPower.High
&& component.Power == PneumaticCannonPower.High) && _stun.TryUpdateParalyzeDuration(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime)))
{ {
_stun.TryParalyze(args.User, TimeSpan.FromSeconds(component.HighPowerStunTime), true, status);
Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-power-stun", Popup.PopupEntity(Loc.GetString("pneumatic-cannon-component-power-stun",
("cannon", uid)), cannon, args.User); ("cannon", uid)), cannon, args.User);
} }

View File

@@ -1,10 +1,7 @@
using Content.Server.Actions; using Content.Server.Actions;
using Content.Server.Humanoid; using Content.Server.Humanoid;
using Content.Server.Inventory; using Content.Server.Inventory;
using Content.Server.Mind.Commands;
using Content.Server.Polymorph.Components; using Content.Server.Polymorph.Components;
using Content.Shared.Actions;
using Content.Shared.Actions.Components;
using Content.Shared.Buckle; using Content.Shared.Buckle;
using Content.Shared.Coordinates; using Content.Shared.Coordinates;
using Content.Shared.Damage; using Content.Shared.Damage;
@@ -210,7 +207,7 @@ public sealed partial class PolymorphSystem : EntitySystem
("child", Identity.Entity(child, EntityManager))), ("child", Identity.Entity(child, EntityManager))),
child); child);
MakeSentientCommand.MakeSentient(child, EntityManager); _mindSystem.MakeSentient(child);
var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>(); var polymorphedComp = Factory.GetComponent<PolymorphedEntityComponent>();
polymorphedComp.Parent = uid; polymorphedComp.Parent = uid;

View File

@@ -1,4 +1,6 @@
using Content.Server.Power.Components; using Content.Server.Administration.Logs;
using Content.Server.Power.Components;
using Content.Shared.Database;
using Content.Shared.Power; using Content.Shared.Power;
using Robust.Server.GameObjects; using Robust.Server.GameObjects;
@@ -19,6 +21,7 @@ namespace Content.Server.Power.EntitySystems;
/// </remarks> /// </remarks>
public sealed class BatteryInterfaceSystem : EntitySystem public sealed class BatteryInterfaceSystem : EntitySystem
{ {
[Dependency] private readonly IAdminLogManager _adminLog = default!;
[Dependency] private readonly UserInterfaceSystem _uiSystem = null!; [Dependency] private readonly UserInterfaceSystem _uiSystem = null!;
public override void Initialize() public override void Initialize()
@@ -43,12 +46,16 @@ public sealed class BatteryInterfaceSystem : EntitySystem
{ {
var netBattery = Comp<PowerNetworkBatteryComponent>(ent); var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
netBattery.CanCharge = args.On; netBattery.CanCharge = args.On;
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set input breaker to {args.On} on {ToPrettyString(ent):target}");
} }
private void HandleSetOutputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetOutputBreakerMessage args) private void HandleSetOutputBreaker(Entity<BatteryInterfaceComponent> ent, ref BatterySetOutputBreakerMessage args)
{ {
var netBattery = Comp<PowerNetworkBatteryComponent>(ent); var netBattery = Comp<PowerNetworkBatteryComponent>(ent);
netBattery.CanDischarge = args.On; netBattery.CanDischarge = args.On;
_adminLog.Add(LogType.Action,$"{ToPrettyString(args.Actor):actor} set output breaker to {args.On} on {ToPrettyString(ent):target}");
} }
private void HandleSetChargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetChargeRateMessage args) private void HandleSetChargeRate(Entity<BatteryInterfaceComponent> ent, ref BatterySetChargeRateMessage args)

View File

@@ -177,7 +177,7 @@ public sealed partial class RevenantSystem : EntitySystem
ChangeEssenceAmount(uid, -abilityCost, component, false); ChangeEssenceAmount(uid, -abilityCost, component, false);
_statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false); _statusEffects.TryAddStatusEffect<CorporealComponent>(uid, "Corporeal", TimeSpan.FromSeconds(debuffs.Y), false);
_stun.TryStun(uid, TimeSpan.FromSeconds(debuffs.X), false); _stun.TryAddStunDuration(uid, TimeSpan.FromSeconds(debuffs.X));
return true; return true;
} }

View File

@@ -1,6 +1,6 @@
using Content.Server.Administration.Managers; using Content.Server.Administration.Managers;
using Content.Server.Atmos.Piping.Components; using Content.Shared.Atmos.Components;
using Content.Server.Atmos.Piping.EntitySystems; using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Administration; using Content.Shared.Administration;
using Content.Shared.NodeContainer; using Content.Shared.NodeContainer;
using Content.Shared.NodeContainer.NodeGroups; using Content.Shared.NodeContainer.NodeGroups;
@@ -78,7 +78,7 @@ namespace Content.Server.Sandbox.Commands
if (!EntityManager.TryGetComponent(x.Owner, out AtmosPipeColorComponent? atmosPipeColorComponent)) if (!EntityManager.TryGetComponent(x.Owner, out AtmosPipeColorComponent? atmosPipeColorComponent))
continue; continue;
_pipeColorSystem.SetColor(x.Owner, atmosPipeColorComponent, color); _pipeColorSystem.SetColor((x.Owner, atmosPipeColorComponent), color);
} }
} }
} }

View File

@@ -618,10 +618,7 @@ public sealed partial class ShuttleSystem
{ {
foreach (var child in toKnock) foreach (var child in toKnock)
{ {
if (!_statusQuery.TryGetComponent(child, out var status)) _stuns.TryUpdateParalyzeDuration(child, _hyperspaceKnockdownTime);
continue;
_stuns.TryParalyze(child, _hyperspaceKnockdownTime, true, status);
// If the guy we knocked down is on a spaced tile, throw them too // If the guy we knocked down is on a spaced tile, throw them too
if (grid != null) if (grid != null)

View File

@@ -249,7 +249,7 @@ public sealed partial class ShuttleSystem
if (direction.LengthSquared() > minsq) if (direction.LengthSquared() > minsq)
{ {
_stuns.TryKnockdown(uid, knockdownTime, true); _stuns.TryUpdateKnockdownDuration(uid, knockdownTime);
_throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false); _throwing.TryThrow(uid, direction, physics, Transform(uid), _projQuery, direction.Length(), playSound: false);
} }
else else

View File

@@ -19,9 +19,21 @@ namespace Content.Server.Speech.EntitySystems
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly ILocalizationManager _loc = default!; [Dependency] private readonly ILocalizationManager _loc = default!;
private readonly Dictionary<ProtoId<ReplacementAccentPrototype>, (Regex regex, string replacement)[]>
_cachedReplacements = new();
public override void Initialize() public override void Initialize()
{ {
SubscribeLocalEvent<ReplacementAccentComponent, AccentGetEvent>(OnAccent); SubscribeLocalEvent<ReplacementAccentComponent, AccentGetEvent>(OnAccent);
_proto.PrototypesReloaded += OnPrototypesReloaded;
}
public override void Shutdown()
{
base.Shutdown();
_proto.PrototypesReloaded -= OnPrototypesReloaded;
} }
private void OnAccent(EntityUid uid, ReplacementAccentComponent component, AccentGetEvent args) private void OnAccent(EntityUid uid, ReplacementAccentComponent component, AccentGetEvent args)
@@ -48,27 +60,22 @@ namespace Content.Server.Speech.EntitySystems
return prototype.FullReplacements.Length != 0 ? Loc.GetString(_random.Pick(prototype.FullReplacements)) : ""; return prototype.FullReplacements.Length != 0 ? Loc.GetString(_random.Pick(prototype.FullReplacements)) : "";
} }
if (prototype.WordReplacements == null)
return message;
// Prohibition of repeated word replacements. // Prohibition of repeated word replacements.
// All replaced words placed in the final message are placed here as dashes (___) with the same length. // All replaced words placed in the final message are placed here as dashes (___) with the same length.
// The regex search goes through this buffer message, from which the already replaced words are crossed out, // The regex search goes through this buffer message, from which the already replaced words are crossed out,
// ensuring that the replaced words cannot be replaced again. // ensuring that the replaced words cannot be replaced again.
var maskMessage = message; var maskMessage = message;
foreach (var (first, replace) in prototype.WordReplacements) foreach (var (regex, replace) in GetCachedReplacements(prototype))
{ {
var f = _loc.GetString(first);
var r = _loc.GetString(replace);
// this is kind of slow but its not that bad // this is kind of slow but its not that bad
// essentially: go over all matches, try to match capitalization where possible, then replace // essentially: go over all matches, try to match capitalization where possible, then replace
// rather than using regex.replace // rather than using regex.replace
for (int i = Regex.Count(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase); i > 0; i--) for (int i = regex.Count(maskMessage); i > 0; i--)
{ {
// fetch the match again as the character indices may have changed // fetch the match again as the character indices may have changed
Match match = Regex.Match(maskMessage, $@"(?<!\w){f}(?!\w)", RegexOptions.IgnoreCase); Match match = regex.Match(maskMessage);
var replacement = r; var replacement = replace;
// Intelligently replace capitalization // Intelligently replace capitalization
// two cases where we will do so: // two cases where we will do so:
@@ -98,5 +105,40 @@ namespace Content.Server.Speech.EntitySystems
return message; return message;
} }
private (Regex regex, string replacement)[] GetCachedReplacements(ReplacementAccentPrototype prototype)
{
if (!_cachedReplacements.TryGetValue(prototype.ID, out var replacements))
{
replacements = GenerateCachedReplacements(prototype);
_cachedReplacements.Add(prototype.ID, replacements);
}
return replacements;
}
private (Regex regex, string replacement)[] GenerateCachedReplacements(ReplacementAccentPrototype prototype)
{
if (prototype.WordReplacements is not { } replacements)
return [];
return replacements.Select(kv =>
{
var (first, replace) = kv;
var firstLoc = _loc.GetString(first);
var replaceLoc = _loc.GetString(replace);
var regex = new Regex($@"(?<!\w){firstLoc}(?!\w)", RegexOptions.IgnoreCase);
return (regex, replaceLoc);
})
.ToArray();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs obj)
{
_cachedReplacements.Clear();
}
} }
} }

View File

@@ -1,4 +1,4 @@
using Content.Server.Abilities.Mime; using Content.Shared.Abilities.Mime;
using Content.Server.Chat.Systems; using Content.Server.Chat.Systems;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Server.Speech.Components; using Content.Server.Speech.Components;

View File

@@ -1,5 +1,5 @@
using Content.Server.Atmos.Piping.Components; using Content.Shared.Atmos.Components;
using Content.Server.Atmos.Piping.EntitySystems; using Content.Shared.Atmos.EntitySystems;
using Content.Server.Charges; using Content.Server.Charges;
using Content.Server.Decals; using Content.Server.Decals;
using Content.Server.Destructible; using Content.Server.Destructible;
@@ -147,7 +147,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
return; return;
Audio.PlayPvs(ent.Comp.SpraySound, ent); Audio.PlayPvs(ent.Comp.SpraySound, ent);
_pipeColor.SetColor(target, color, args.Color); _pipeColor.SetColor((target, color), args.Color);
args.Handled = true; args.Handled = true;
} }

View File

@@ -1,7 +1,7 @@
using Content.Server.Access.Systems; using Content.Server.Access.Systems;
using Content.Server.Humanoid; using Content.Server.Humanoid;
using Content.Server.IdentityManagement; using Content.Server.IdentityManagement;
using Content.Server.Mind.Commands; using Content.Server.Mind;
using Content.Server.PDA; using Content.Server.PDA;
using Content.Server.Station.Components; using Content.Server.Station.Components;
using Content.Shared.Access.Components; using Content.Shared.Access.Components;
@@ -41,6 +41,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
[Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly PdaSystem _pdaSystem = default!; [Dependency] private readonly PdaSystem _pdaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly MindSystem _mindSystem = default!;
/// <summary> /// <summary>
/// Attempts to spawn a player character onto the given station. /// Attempts to spawn a player character onto the given station.
@@ -110,7 +111,7 @@ public sealed class StationSpawningSystem : SharedStationSpawningSystem
{ {
DebugTools.Assert(entity is null); DebugTools.Assert(entity is null);
var jobEntity = Spawn(prototype.JobEntity, coordinates); var jobEntity = Spawn(prototype.JobEntity, coordinates);
MakeSentientCommand.MakeSentient(jobEntity, EntityManager); _mindSystem.MakeSentient(jobEntity);
// Make sure custom names get handled, what is gameticker control flow whoopy. // Make sure custom names get handled, what is gameticker control flow whoopy.
if (loadout != null) if (loadout != null)

View File

@@ -8,21 +8,47 @@ namespace Content.Server.Stunnable.Components
{ {
// TODO: Can probably predict this. // TODO: Can probably predict this.
// See stunsystem for what these do /// <summary>
[DataField("stunAmount")] /// How long we are stunned for
public int StunAmount; /// </summary>
[DataField]
public TimeSpan StunAmount;
[DataField("knockdownAmount")] /// <summary>
public int KnockdownAmount; /// How long we are knocked down for
/// </summary>
[DataField]
public TimeSpan KnockdownAmount;
[DataField("slowdownAmount")] /// <summary>
public int SlowdownAmount; /// How long we are slowed down for
/// </summary>
[DataField]
public TimeSpan SlowdownAmount;
[DataField("walkSpeedMultiplier")] /// <summary>
public float WalkSpeedMultiplier = 1f; /// Multiplier for a mob's walking speed
/// </summary>
[DataField]
public float WalkSpeedModifier = 1f;
[DataField("runSpeedMultiplier")] /// <summary>
public float RunSpeedMultiplier = 1f; /// Multiplier for a mob's sprinting speed
/// </summary>
[DataField]
public float SprintSpeedModifier = 1f;
/// <summary>
/// Refresh Stun or Slowdown on hit
/// </summary>
[DataField]
public bool Refresh = true;
/// <summary>
/// Should the entity try and stand automatically after being knocked down?
/// </summary>
[DataField]
public bool AutoStand = true;
/// <summary> /// <summary>
/// Fixture we track for the collision. /// Fixture we track for the collision.

View File

@@ -0,0 +1,6 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable;
public sealed class StunSystem : SharedStunSystem;

View File

@@ -1,8 +1,6 @@
using Content.Server.Stunnable.Components; using Content.Server.Stunnable.Components;
using Content.Shared.Standing; using Content.Shared.Movement.Systems;
using Content.Shared.StatusEffect;
using JetBrains.Annotations; using JetBrains.Annotations;
using Robust.Shared.Physics.Dynamics;
using Content.Shared.Throwing; using Content.Shared.Throwing;
using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Events;
@@ -12,6 +10,7 @@ namespace Content.Server.Stunnable
internal sealed class StunOnCollideSystem : EntitySystem internal sealed class StunOnCollideSystem : EntitySystem
{ {
[Dependency] private readonly StunSystem _stunSystem = default!; [Dependency] private readonly StunSystem _stunSystem = default!;
[Dependency] private readonly MovementModStatusSystem _movementMod = default!;
public override void Initialize() public override void Initialize()
{ {
@@ -22,18 +21,19 @@ namespace Content.Server.Stunnable
private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target) private void TryDoCollideStun(EntityUid uid, StunOnCollideComponent component, EntityUid target)
{ {
_stunSystem.TryUpdateStunDuration(target, component.StunAmount);
if (TryComp<StatusEffectsComponent>(target, out var status)) _stunSystem.TryKnockdown(target, component.KnockdownAmount, component.Refresh, component.AutoStand, force: true);
{
_stunSystem.TryStun(target, TimeSpan.FromSeconds(component.StunAmount), true, status);
_stunSystem.TryKnockdown(target, TimeSpan.FromSeconds(component.KnockdownAmount), true, _movementMod.TryUpdateMovementSpeedModDuration(
status); target,
MovementModStatusSystem.TaserSlowdown,
_stunSystem.TrySlowdown(target, TimeSpan.FromSeconds(component.SlowdownAmount), true, component.SlowdownAmount,
component.WalkSpeedMultiplier, component.RunSpeedMultiplier, status); component.WalkSpeedModifier,
} component.SprintSpeedModifier
);
} }
private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args) private void HandleCollide(EntityUid uid, StunOnCollideComponent component, ref StartCollideEvent args)
{ {
if (args.OurFixtureId != component.FixtureID) if (args.OurFixtureId != component.FixtureID)

View File

@@ -1,7 +0,0 @@
using Content.Shared.Stunnable;
namespace Content.Server.Stunnable
{
public sealed class StunSystem : SharedStunSystem
{}
}

View File

@@ -1,6 +1,8 @@
using Content.Server.Administration.Logs;
using Content.Server.DeviceNetwork.Systems; using Content.Server.DeviceNetwork.Systems;
using Content.Server.Emp; using Content.Server.Emp;
using Content.Shared.ActionBlocker; using Content.Shared.ActionBlocker;
using Content.Shared.Database;
using Content.Shared.DeviceNetwork; using Content.Shared.DeviceNetwork;
using Content.Shared.DeviceNetwork.Events; using Content.Shared.DeviceNetwork.Events;
using Content.Shared.Power; using Content.Shared.Power;
@@ -21,6 +23,8 @@ public sealed class SurveillanceCameraSystem : EntitySystem
[Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!; [Dependency] private readonly DeviceNetworkSystem _deviceNetworkSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!; [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
// Pings a surveillance camera subnet. All cameras will always respond // Pings a surveillance camera subnet. All cameras will always respond
// with a data message if they are on the same subnet. // with a data message if they are on the same subnet.
@@ -170,6 +174,7 @@ public sealed class SurveillanceCameraSystem : EntitySystem
component.CameraId = args.Name; component.CameraId = args.Name;
component.NameSet = true; component.NameSet = true;
UpdateSetupInterface(uid, component); UpdateSetupInterface(uid, component);
_adminLogger.Add(LogType.Chat, LogImpact.Low, $"{ToPrettyString(args.Actor)} set the name of {ToPrettyString(uid)} to \"{args.Name}.\"");
} }
private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component, private void OnSetNetwork(EntityUid uid, SurveillanceCameraComponent component,

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