Merge branch 'master' of https://github.com/space-wizards/space-station-14 into FoodSizeAdjustment

This commit is contained in:
5tickman
2025-07-25 21:36:15 -05:00
1022 changed files with 14129 additions and 3994 deletions

View File

@@ -334,7 +334,12 @@ namespace Content.Client.Actions
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;
// let world target component handle it
@@ -345,8 +350,6 @@ namespace Content.Client.Actions
return;
}
args.Handled = true;
var action = args.Action;
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="BanButton" Text="{Loc player-panel-ban}" Disabled="True"/>
<controls:ConfirmButton Name="RejuvenateButton" Text="{Loc player-panel-rejuvenate}" Disabled="True"/>
<Button Name="CameraButton" Text="{Loc player-panel-camera}" Disabled="True"/>
</GridContainer>
</BoxContainer>
</BoxContainer>

View File

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

View File

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

View File

@@ -62,11 +62,11 @@ public sealed class AnomalySystem : SharedAnomalySystem
{
base.Update(frameTime);
var query = EntityQueryEnumerator<AnomalySupercriticalComponent, SpriteComponent>();
var query = EntityQueryEnumerator<AnomalyComponent, AnomalySupercriticalComponent, SpriteComponent>();
while (query.MoveNext(out var uid, out var super, out var sprite))
while (query.MoveNext(out var uid, out var anomaly, out var super, out var sprite))
{
var completion = 1f - (float)((super.EndTime - _timing.CurTime) / super.SupercriticalDuration);
var completion = 1f - (float) ((super.EndTime - _timing.CurTime) / anomaly.SupercriticalDuration);
var scale = completion * (super.MaxScaleAmount - 1f) + 1f;
_sprite.SetScale((uid, sprite), new Vector2(scale, scale));

View File

@@ -162,10 +162,10 @@ public sealed partial class AtmosMonitoringConsoleNavMapControl : NavMapControl
{
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
var color = Color.FromHex(hexColor) * _basePipeNetColor;
var color = pipeColor * _basePipeNetColor;
if (FocusNetId != null && FocusNetId != netId)
color *= _unfocusedPipeNetColor;

View File

@@ -0,0 +1,28 @@
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Atmos.EntitySystems;
/// <summary>
/// Used to change the appearance of gas canisters.
/// </summary>
public sealed class GasCanisterAppearanceSystem : VisualizerSystem<GasCanisterComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
protected override void OnAppearanceChange(EntityUid uid, GasCanisterComponent component, ref AppearanceChangeEvent args)
{
if (!AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var protoName, args.Component) || args.Sprite is not { } old)
return;
if (!_prototypeManager.HasIndex(protoName))
return;
// Create the given prototype and get its first layer.
var tempUid = Spawn(protoName);
SpriteSystem.LayerSetRsiState(uid, 0, SpriteSystem.LayerGetRsiState(tempUid, 0));
QueueDel(tempUid);
}
}

View File

@@ -1,11 +1,46 @@
using Content.Client.Atmos.Components;
using Robust.Client.GameObjects;
using Content.Client.UserInterface.Systems.Storage.Controls;
using Content.Shared.Atmos.Piping;
using Content.Shared.Hands;
using Content.Shared.Atmos.Components;
using Content.Shared.Item;
namespace Content.Client.Atmos.EntitySystems;
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)
{
if (TryComp<SpriteComponent>(uid, out var sprite)
@@ -15,6 +50,8 @@ public sealed class PipeColorVisualizerSystem : VisualizerSystem<PipeColorVisual
var layer = sprite[PipeVisualLayers.Pipe];
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:ui="clr-namespace:Content.Client.UserInterface.Controls"
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">
<!-- Status (pressure, temperature, alarm state, device total, address, etc) -->
<BoxContainer Orientation="Horizontal" Margin="0 0 0 2">

View File

@@ -1,4 +1,5 @@
using Robust.Client.Graphics;
using System.Numerics;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
@@ -36,7 +37,7 @@ namespace Content.Client.Cooldown
if (Progress >= 0f)
{
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
{

View File

@@ -1,7 +1,126 @@
using Content.Shared.Damage.Systems;
using Content.Client.Stunnable;
using Content.Shared.Damage.Components;
using Content.Shared.Damage.Systems;
using Content.Shared.Mobs;
using Content.Shared.Mobs.Systems;
using Robust.Client.GameObjects;
namespace Content.Client.Damage.Systems;
public sealed partial class StaminaSystem : SharedStaminaSystem
{
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
[Dependency] private readonly StunSystem _stun = default!; // Clientside Stun System
private const string StaminaAnimationKey = "stamina";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StaminaComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<ActiveStaminaComponent, ComponentShutdown>(OnActiveStaminaShutdown);
SubscribeLocalEvent<StaminaComponent, MobStateChangedEvent>(OnMobStateChanged);
}
protected override void OnStamHandleState(Entity<StaminaComponent> entity, ref AfterAutoHandleStateEvent args)
{
base.OnStamHandleState(entity, ref args);
TryStartAnimation(entity);
}
private void OnActiveStaminaShutdown(Entity<ActiveStaminaComponent> entity, ref ComponentShutdown args)
{
// If we don't have active stamina, we shouldn't have stamina damage. If the update loop can trust it we can trust it.
if (!TryComp<StaminaComponent>(entity, out var stamina))
return;
StopAnimation((entity, stamina));
}
protected override void OnShutdown(Entity<StaminaComponent> entity, ref ComponentShutdown args)
{
base.OnShutdown(entity, ref args);
StopAnimation(entity);
}
private void OnMobStateChanged(Entity<StaminaComponent> ent, ref MobStateChangedEvent args)
{
if (args.NewMobState == MobState.Dead)
StopAnimation(ent);
}
private void TryStartAnimation(Entity<StaminaComponent> entity)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
// If the animation is running, the system should update it accordingly
// If we're below the threshold to animate, don't try to animate
// If we're in stamcrit don't override it
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage || _animation.HasRunningAnimation(entity, StaminaAnimationKey))
return;
// Don't animate if we're dead
if (_mobState.IsDead(entity))
return;
entity.Comp.StartOffset = sprite.Offset;
PlayAnimation((entity, entity.Comp, sprite));
}
private void StopAnimation(Entity<StaminaComponent, SpriteComponent?> entity)
{
if(!Resolve(entity, ref entity.Comp2))
return;
_animation.Stop(entity.Owner, StaminaAnimationKey);
entity.Comp1.StartOffset = entity.Comp2.Offset;
}
private void OnAnimationCompleted(Entity<StaminaComponent> entity, ref AnimationCompletedEvent args)
{
if (args.Key != StaminaAnimationKey || !args.Finished || !TryComp<SpriteComponent>(entity, out var sprite))
return;
// stop looping if we're below the threshold
if (entity.Comp.AnimationThreshold > entity.Comp.StaminaDamage)
{
_animation.Stop(entity.Owner, StaminaAnimationKey);
_sprite.SetOffset((entity, sprite), entity.Comp.StartOffset);
return;
}
if (!HasComp<AnimationPlayerComponent>(entity))
return;
PlayAnimation((entity, entity.Comp, sprite));
}
private void PlayAnimation(Entity<StaminaComponent, SpriteComponent> entity)
{
var step = Math.Clamp((entity.Comp1.StaminaDamage - entity.Comp1.AnimationThreshold) /
(entity.Comp1.CritThreshold - entity.Comp1.AnimationThreshold),
0f,
1f); // The things I do for project 0 warnings
var frequency = entity.Comp1.FrequencyMin + step * entity.Comp1.FrequencyMod;
var jitter = entity.Comp1.JitterAmplitudeMin + step * entity.Comp1.JitterAmplitudeMod;
var breathing = entity.Comp1.BreathingAmplitudeMin + step * entity.Comp1.BreathingAmplitudeMod;
_animation.Play(entity.Owner,
_stun.GetFatigueAnimation(entity.Comp2,
frequency,
entity.Comp1.Jitters,
jitter * entity.Comp1.JitterMin,
jitter * entity.Comp1.JitterMax,
breathing,
entity.Comp1.StartOffset,
ref entity.Comp1.LastJitter),
StaminaAnimationKey);
}
}

View File

@@ -1,6 +0,0 @@
using Content.Shared.Devour;
namespace Content.Client.Devour;
public sealed class DevourSystem : SharedDevourSystem
{
}

View File

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

View File

@@ -1,16 +1,17 @@
using Content.Shared.Doors.Components;
using Content.Shared.Doors.Systems;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Prototypes;
namespace Content.Client.Doors;
public sealed class DoorSystem : SharedDoorSystem
{
[Dependency] private readonly AnimationPlayerSystem _animationSystem = default!;
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public override void Initialize()
@@ -85,8 +86,8 @@ public sealed class DoorSystem : SharedDoorSystem
if (!AppearanceSystem.TryGetData<DoorState>(entity, DoorVisuals.State, out var state, args.Component))
state = DoorState.Closed;
if (AppearanceSystem.TryGetData<string>(entity, DoorVisuals.BaseRSI, out var baseRsi, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), baseRsi);
if (AppearanceSystem.TryGetData<string>(entity, PaintableVisuals.Prototype, out var prototype, args.Component))
UpdateSpriteLayers((entity.Owner, args.Sprite), prototype);
if (_animationSystem.HasRunningAnimation(entity, DoorComponent.AnimationKey))
_animationSystem.Stop(entity.Owner, DoorComponent.AnimationKey);
@@ -139,14 +140,14 @@ public sealed class DoorSystem : SharedDoorSystem
}
}
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string baseRsi)
private void UpdateSpriteLayers(Entity<SpriteComponent> sprite, string targetProto)
{
if (!_resourceCache.TryGetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / baseRsi, out var res))
{
Log.Error("Unable to load RSI '{0}'. Trace:\n{1}", baseRsi, Environment.StackTrace);
if (!_prototypeManager.TryIndex(targetProto, out var target))
return;
}
_sprite.SetBaseRsi(sprite.AsNullable(), res.RSI);
if (!target.TryGetComponent(out SpriteComponent? targetSprite, _componentFactory))
return;
_sprite.SetBaseRsi(sprite.AsNullable(), targetSprite.BaseRSI);
}
}

View File

@@ -17,7 +17,7 @@ public sealed class DrowsinessOverlay : Overlay
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedStatusEffectsSystem _statusEffects = default!;
private readonly StatusEffectsSystem _statusEffects = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
@@ -33,7 +33,7 @@ public sealed class DrowsinessOverlay : Overlay
{
IoCManager.InjectDependencies(this);
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
_drowsinessShader = _prototypeManager.Index(Shader).InstanceUnique();
}

View File

@@ -10,7 +10,7 @@ public sealed class DrowsinessSystem : SharedDrowsinessSystem
{
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly IOverlayManager _overlayMan = default!;
[Dependency] private readonly SharedStatusEffectsSystem _statusEffects = default!;
[Dependency] private readonly StatusEffectsSystem _statusEffects = default!;
private DrowsinessOverlay _overlay = default!;

View File

@@ -20,7 +20,7 @@ public sealed class RainbowOverlay : Overlay
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEntitySystemManager _sysMan = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private readonly SharedStatusEffectsSystem _statusEffects = default!;
private readonly StatusEffectsSystem _statusEffects = default!;
public override OverlaySpace Space => OverlaySpace.WorldSpace;
public override bool RequestScreenTexture => true;
@@ -41,7 +41,7 @@ public sealed class RainbowOverlay : Overlay
{
IoCManager.InjectDependencies(this);
_statusEffects = _sysMan.GetEntitySystem<SharedStatusEffectsSystem>();
_statusEffects = _sysMan.GetEntitySystem<StatusEffectsSystem>();
_rainbowShader = _prototypeManager.Index(Shader).InstanceUnique();
_config.OnValueChanged(CCVars.ReducedMotion, OnReducedMotionChanged, invokeImmediately: true);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -195,7 +195,6 @@ public sealed class GeneratedParallaxCache : IPostInjectInit
public required ResPath ConfigPath;
public required Task<Texture> LoadTask;
public required CancellationTokenSource CancellationSource;
public ValueList<CancellationTokenRegistration> CancelRegistrations;
public int RefCount;
}

View File

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

View File

@@ -9,7 +9,7 @@ public static class StaticPowerSystem
public static bool IsPowered(this EntitySystem system, EntityUid uid, IEntityManager entManager, ApcPowerReceiverComponent? receiver = null)
{
if (receiver == null && !entManager.TryGetComponent(uid, out receiver))
return false;
return true;
return receiver.Powered;
}

View File

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

View File

@@ -10,7 +10,6 @@ using Robust.Shared.Physics;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Vector2 = System.Numerics.Vector2;
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 distance = gridCentre.Length();
var gridDistance = (gridBody.LocalCenter - xform.LocalPosition).Length();
var labelText = Loc.GetString("shuttle-console-iff-label", ("name", labelName),
("distance", $"{distance:0.0}"));
("distance", $"{gridDistance:0.0}"));
var mapCoords = _transform.GetWorldPosition(gUid);
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,56 +1,129 @@
using Content.Shared.SprayPainter;
using Robust.Client.Graphics;
using Robust.Client.ResourceManagement;
using Robust.Shared.Serialization.TypeSerializers.Implementations;
using Robust.Shared.Utility;
using System.Linq;
using Robust.Shared.Graphics;
using Content.Client.Items;
using Content.Client.Message;
using Content.Client.Stylesheets;
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Content.Shared.SprayPainter.Prototypes;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter;
/// <summary>
/// Client-side spray painter functions. Caches information for spray painter windows and updates the UI to reflect component state.
/// </summary>
public sealed class SprayPainterSystem : SharedSprayPainterSystem
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
public List<SprayPainterEntry> Entries { get; private set; } = new();
public List<SprayPainterDecalEntry> Decals = [];
public Dictionary<string, List<string>> PaintableGroupsByCategory = new();
public Dictionary<string, Dictionary<string, EntProtoId>> PaintableStylesByGroup = new();
protected override void CacheStyles()
public override void Initialize()
{
base.CacheStyles();
base.Initialize();
Entries.Clear();
foreach (var style in Styles)
Subs.ItemStatus<SprayPainterComponent>(ent => new StatusControl(ent));
SubscribeLocalEvent<SprayPainterComponent, AfterAutoHandleStateEvent>(OnStateUpdate);
SubscribeLocalEvent<PrototypesReloadedEventArgs>(OnPrototypesReloaded);
CachePrototypes();
}
private void OnStateUpdate(Entity<SprayPainterComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
protected override void UpdateUi(Entity<SprayPainterComponent> ent)
{
if (_ui.TryGetOpenUi(ent.Owner, SprayPainterUiKey.Key, out var bui))
bui.Update();
}
private void OnPrototypesReloaded(PrototypesReloadedEventArgs args)
{
if (!args.WasModified<PaintableGroupCategoryPrototype>() || !args.WasModified<PaintableGroupPrototype>() || !args.WasModified<DecalPrototype>())
return;
CachePrototypes();
}
private void CachePrototypes()
{
PaintableGroupsByCategory.Clear();
PaintableStylesByGroup.Clear();
foreach (var category in Proto.EnumeratePrototypes<PaintableGroupCategoryPrototype>().OrderBy(x => x.ID))
{
var name = style.Name;
string? iconPath = Groups
.FindAll(x => x.StylePaths.ContainsKey(name))?
.MaxBy(x => x.IconPriority)?.StylePaths[name];
if (iconPath == null)
var groupList = new List<string>();
foreach (var groupId in category.Groups)
{
Entries.Add(new SprayPainterEntry(name, null));
continue;
if (!Proto.TryIndex(groupId, out var group))
continue;
groupList.Add(groupId);
PaintableStylesByGroup[groupId] = group.Styles;
}
RSIResource doorRsi = _resourceCache.GetResource<RSIResource>(SpriteSpecifierSerializer.TextureRoot / new ResPath(iconPath));
if (!doorRsi.RSI.TryGetState("closed", out var icon))
{
Entries.Add(new SprayPainterEntry(name, null));
continue;
}
if (groupList.Count > 0)
PaintableGroupsByCategory[category.ID] = groupList;
}
Entries.Add(new SprayPainterEntry(name, icon.Frame0));
Decals.Clear();
foreach (var decalPrototype in Proto.EnumeratePrototypes<DecalPrototype>().OrderBy(x => x.ID))
{
if (!decalPrototype.Tags.Contains("station")
&& !decalPrototype.Tags.Contains("markings")
|| decalPrototype.Tags.Contains("dirty"))
continue;
Decals.Add(new SprayPainterDecalEntry(decalPrototype.ID, decalPrototype.Sprite));
}
}
private sealed class StatusControl : Control
{
private readonly RichTextLabel _label;
private readonly Entity<SprayPainterComponent> _entity;
private DecalPaintMode? _lastPaintingDecals = null;
public StatusControl(Entity<SprayPainterComponent> ent)
{
_entity = ent;
_label = new RichTextLabel { StyleClasses = { StyleNano.StyleClassItemStatus } };
AddChild(_label);
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_entity.Comp.DecalMode == _lastPaintingDecals)
return;
_lastPaintingDecals = _entity.Comp.DecalMode;
string modeLocString = _entity.Comp.DecalMode switch
{
DecalPaintMode.Add => "spray-painter-item-status-add",
DecalPaintMode.Remove => "spray-painter-item-status-remove",
_ => "spray-painter-item-status-off"
};
_label.SetMarkupPermissive(Robust.Shared.Localization.Loc.GetString("spray-painter-item-status-label",
("mode", Robust.Shared.Localization.Loc.GetString(modeLocString))));
}
}
}
public sealed class SprayPainterEntry
{
public string Name;
public Texture? Icon;
public SprayPainterEntry(string name, Texture? icon)
{
Name = name;
Icon = icon;
}
}
/// <summary>
/// A spray paintable decal, mapped by ID.
/// </summary>
public sealed record SprayPainterDecalEntry(string Name, SpriteSpecifier Sprite);

View File

@@ -1,42 +1,96 @@
using Content.Shared.Decals;
using Content.Shared.SprayPainter;
using Content.Shared.SprayPainter.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.SprayPainter.UI;
public sealed class SprayPainterBoundUserInterface : BoundUserInterface
/// <summary>
/// A BUI for a spray painter. Allows selecting pipe colours, decals, and paintable object types sorted by category.
/// </summary>
public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private SprayPainterWindow? _window;
public SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
_window = this.CreateWindow<SprayPainterWindow>();
_window.OnSpritePicked = OnSpritePicked;
_window.OnColorPicked = OnColorPicked;
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? comp))
if (_window == null)
{
_window.Populate(EntMan.System<SprayPainterSystem>().Entries, comp.Index, comp.PickedColor, comp.ColorPalette);
_window = this.CreateWindow<SprayPainterWindow>();
_window.OnSpritePicked += OnSpritePicked;
_window.OnSetPipeColor += OnSetPipeColor;
_window.OnTabChanged += OnTabChanged;
_window.OnDecalChanged += OnDecalChanged;
_window.OnDecalColorChanged += OnDecalColorChanged;
_window.OnDecalAngleChanged += OnDecalAngleChanged;
_window.OnDecalSnapChanged += OnDecalSnapChanged;
}
var sprayPainter = EntMan.System<SprayPainterSystem>();
_window.PopulateCategories(sprayPainter.PaintableStylesByGroup, sprayPainter.PaintableGroupsByCategory, sprayPainter.Decals);
Update();
if (EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainterComp))
_window.SetSelectedTab(sprayPainterComp.SelectedTab);
}
private void OnSpritePicked(ItemList.ItemListSelectedEventArgs args)
public override void Update()
{
SendMessage(new SprayPainterSpritePickedMessage(args.ItemIndex));
if (_window == null)
return;
if (!EntMan.TryGetComponent(Owner, out SprayPainterComponent? sprayPainter))
return;
_window.PopulateColors(sprayPainter.ColorPalette);
if (sprayPainter.PickedColor != null)
_window.SelectColor(sprayPainter.PickedColor);
_window.SetSelectedStyles(sprayPainter.StylesByGroup);
_window.SetSelectedDecal(sprayPainter.SelectedDecal);
_window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
_window.SetDecalColor(sprayPainter.SelectedDecalColor);
_window.SetDecalSnap(sprayPainter.SnapDecals);
}
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
private void OnDecalSnapChanged(bool snap)
{
SendPredictedMessage(new SprayPainterSetDecalSnapMessage(snap));
}
private void OnDecalAngleChanged(int angle)
{
SendPredictedMessage(new SprayPainterSetDecalAngleMessage(angle));
}
private void OnDecalColorChanged(Color? color)
{
SendPredictedMessage(new SprayPainterSetDecalColorMessage(color));
}
private void OnDecalChanged(ProtoId<DecalPrototype> protoId)
{
SendPredictedMessage(new SprayPainterSetDecalMessage(protoId));
}
private void OnTabChanged(int index, bool isSelectedTabWithDecals)
{
SendPredictedMessage(new SprayPainterTabChangedMessage(index, isSelectedTabWithDecals));
}
private void OnSpritePicked(string group, string style)
{
SendPredictedMessage(new SprayPainterSetPaintableStyleMessage(group, style));
}
private void OnSetPipeColor(ItemList.ItemListSelectedEventArgs args)
{
var key = _window?.IndexToColorKey(args.ItemIndex);
SendMessage(new SprayPainterColorPickedMessage(key));
SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
}
}

View File

@@ -0,0 +1,26 @@
<controls:SprayPainterDecals
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
<BoxContainer Orientation="Vertical">
<Label Text="{Loc 'spray-painter-selected-decals'}" />
<ScrollContainer VerticalExpand="True">
<GridContainer Columns="7" Name="DecalsGrid">
<!-- populated by code -->
</GridContainer>
</ScrollContainer>
<BoxContainer Orientation="Vertical">
<ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
<CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
<CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
</BoxContainer>
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'spray-painter-angle-rotation'}" />
<SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
<Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
</BoxContainer>
</BoxContainer>
</controls:SprayPainterDecals>

View File

@@ -0,0 +1,174 @@
using System.Numerics;
using Content.Client.Stylesheets;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// Used to control decal painting parameters for the spray painter.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterDecals : Control
{
public Action<ProtoId<DecalPrototype>>? OnDecalSelected;
public Action<Color?>? OnColorChanged;
public Action<int>? OnAngleChanged;
public Action<bool>? OnSnapChanged;
private SpriteSystem? _sprite;
private string _selectedDecal = string.Empty;
private List<SprayPainterDecalEntry> _decals = [];
public SprayPainterDecals()
{
RobustXamlLoader.Load(this);
AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
ColorSelector.OnColorChanged += OnColorSelected;
}
private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
{
OnColorChanged?.Invoke(UseCustomColorCheckBox.Pressed ? ColorSelector.Color : null);
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
private void SnapToTileCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
{
OnSnapChanged?.Invoke(SnapToTileCheckBox.Pressed);
}
/// <summary>
/// Updates the decal list.
/// </summary>
public void PopulateDecals(List<SprayPainterDecalEntry> decals, SpriteSystem sprite)
{
_sprite ??= sprite;
_decals = decals;
DecalsGrid.Children.Clear();
foreach (var decal in decals)
{
var button = new TextureButton()
{
TextureNormal = sprite.Frame0(decal.Sprite),
Name = decal.Name,
ToolTip = decal.Name,
Scale = new Vector2(2, 2),
};
button.OnPressed += DecalButtonOnPressed;
if (UseCustomColorCheckBox.Pressed)
{
button.Modulate = ColorSelector.Color;
}
if (_selectedDecal == decal.Name)
{
var panelContainer = new PanelContainer()
{
PanelOverride = new StyleBoxFlat()
{
BackgroundColor = StyleNano.ButtonColorDefault,
},
Children =
{
button,
},
};
DecalsGrid.AddChild(panelContainer);
}
else
{
DecalsGrid.AddChild(button);
}
}
}
private void OnColorSelected(Color color)
{
if (!UseCustomColorCheckBox.Pressed)
return;
OnColorChanged?.Invoke(color);
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
private void UpdateColorButtons(bool apply)
{
Color modulateColor = apply ? ColorSelector.Color : Color.White;
foreach (var button in DecalsGrid.Children)
{
switch (button)
{
case TextureButton:
button.Modulate = modulateColor;
break;
case PanelContainer panelContainer:
{
foreach (TextureButton textureButton in panelContainer.Children)
textureButton.Modulate = modulateColor;
break;
}
}
}
}
private void DecalButtonOnPressed(BaseButton.ButtonEventArgs obj)
{
if (obj.Button.Name is not { } name)
return;
_selectedDecal = name;
OnDecalSelected?.Invoke(_selectedDecal);
if (_sprite is null)
return;
PopulateDecals(_decals, _sprite);
}
public void SetSelectedDecal(string name)
{
_selectedDecal = name;
if (_sprite is null)
return;
PopulateDecals(_decals, _sprite);
}
public void SetAngle(int degrees)
{
AngleSpinBox.OverrideValue(degrees);
}
public void SetColor(Color? color)
{
UseCustomColorCheckBox.Pressed = color != null;
if (color != null)
ColorSelector.Color = color.Value;
UpdateColorButtons(UseCustomColorCheckBox.Pressed);
}
public void SetSnap(bool snap)
{
SnapToTileCheckBox.Pressed = snap;
}
}

View File

@@ -0,0 +1,12 @@
<BoxContainer
xmlns="https://spacestation14.io"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
Orientation="Vertical">
<Label Text="{Loc 'spray-painter-selected-style'}" />
<controls:ListContainer
Name="StyleList"
Toggle="True"
Group="True">
<!-- populated by code -->
</controls:ListContainer>
</BoxContainer>

View File

@@ -0,0 +1,66 @@
using Content.Client.UserInterface.Controls;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// Used to display a group of paintable styles in the spray painter menu.
/// (e.g. each type of paintable locker or plastic crate)
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterGroup : BoxContainer
{
public event Action<SpriteListData>? OnButtonPressed;
public SprayPainterGroup()
{
RobustXamlLoader.Load(this);
StyleList.GenerateItem = GenerateItems;
}
public void PopulateList(List<SpriteListData> spriteList)
{
StyleList.PopulateList(spriteList);
}
public void SelectItemByStyle(string key)
{
foreach (var elem in StyleList.Data)
{
if (elem is not SpriteListData spriteElem)
continue;
if (spriteElem.Style == key)
{
StyleList.Select(spriteElem);
break;
}
}
}
private void GenerateItems(ListData data, ListContainerButton button)
{
if (data is not SpriteListData spriteListData)
return;
var box = new BoxContainer() { Orientation = LayoutOrientation.Horizontal };
var protoView = new EntityPrototypeView();
protoView.SetPrototype(spriteListData.Prototype);
var label = new Label()
{
Text = Loc.GetString($"spray-painter-style-{spriteListData.Group.ToLower()}-{spriteListData.Style.ToLower()}")
};
box.AddChild(protoView);
box.AddChild(label);
button.AddChild(box);
button.AddStyleClass(ListContainer.StyleClassListContainerButton);
button.OnPressed += _ => OnButtonPressed?.Invoke(spriteListData);
if (spriteListData.SelectedIndex == button.Index)
button.Pressed = true;
}
}

View File

@@ -1,34 +1,6 @@
<DefaultWindow xmlns="https://spacestation14.io"
MinSize="500 300"
SetSize="500 500"
Title="{Loc 'spray-painter-window-title'}">
<BoxContainer Orientation="Horizontal"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="450">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="200">
<Label Name="SelectedSpriteLabel"
Text="{Loc 'spray-painter-selected-style'}">
</Label>
<ItemList Name="SpriteList"
SizeFlagsStretchRatio="8"
VerticalExpand="True"/>
</BoxContainer>
<BoxContainer Orientation="Vertical"
HorizontalExpand="True"
VerticalExpand="True"
SeparationOverride="4"
MinWidth="200">
<Label Name="SelectedColorLabel"
Text="{Loc 'spray-painter-selected-color'}"/>
<ItemList Name="ColorList"
SizeFlagsStretchRatio="8"
VerticalExpand="True"/>
</BoxContainer>
</BoxContainer>
MinSize="520 300"
SetSize="520 700"
Title="{Loc 'spray-painter-window-title'}">
<TabContainer Name="Tabs"/>
</DefaultWindow>

View File

@@ -1,12 +1,19 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Decals;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.SprayPainter.UI;
/// <summary>
/// A window to select spray painter settings by object type, as well as pipe colours and decals.
/// </summary>
[GenerateTypedNameReferences]
public sealed partial class SprayPainterWindow : DefaultWindow
{
@@ -15,13 +22,33 @@ public sealed partial class SprayPainterWindow : DefaultWindow
private readonly SpriteSystem _spriteSystem;
public Action<ItemList.ItemListSelectedEventArgs>? OnSpritePicked;
public Action<ItemList.ItemListSelectedEventArgs>? OnColorPicked;
// Events
public event Action<string, string>? OnSpritePicked;
public event Action<int, bool>? OnTabChanged;
public event Action<ProtoId<DecalPrototype>>? OnDecalChanged;
public event Action<ItemList.ItemListSelectedEventArgs>? OnSetPipeColor;
public event Action<Color?>? OnDecalColorChanged;
public event Action<int>? OnDecalAngleChanged;
public event Action<bool>? OnDecalSnapChanged;
// Pipe color data
private ItemList _colorList = default!;
public Dictionary<string, int> ItemColorIndex = new();
private Dictionary<string, Color> currentPalette = new();
private const string colorLocKeyPrefix = "pipe-painter-color-";
private List<SprayPainterEntry> CurrentEntries = new List<SprayPainterEntry>();
private Dictionary<string, Color> _currentPalette = new();
private const string ColorLocKeyPrefix = "pipe-painter-color-";
// Paintable objects
private Dictionary<string, Dictionary<string, EntProtoId>> _currentStylesByGroup = new();
private Dictionary<string, List<string>> _currentGroupsByCategory = new();
// Tab controls
private Dictionary<string, SprayPainterGroup> _paintableControls = new();
private BoxContainer? _pipeControl;
// Decals
private List<SprayPainterDecalEntry> _currentDecals = [];
private SprayPainterDecals? _sprayPainterDecals;
private readonly SpriteSpecifier _colorEntryIconTexture = new SpriteSpecifier.Rsi(
new ResPath("Structures/Piping/Atmospherics/pipe.rsi"),
@@ -32,13 +59,14 @@ public sealed partial class SprayPainterWindow : DefaultWindow
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_spriteSystem = _sysMan.GetEntitySystem<SpriteSystem>();
Tabs.OnTabChanged += (index) => OnTabChanged?.Invoke(index, _sprayPainterDecals?.GetPositionInParent() == index);
}
private string GetColorLocString(string? colorKey)
{
if (string.IsNullOrEmpty(colorKey))
return Loc.GetString("pipe-painter-no-color-selected");
var locKey = colorLocKeyPrefix + colorKey;
var locKey = ColorLocKeyPrefix + colorKey;
if (!_loc.TryGetString(locKey, out var locString))
locString = colorKey;
@@ -48,51 +76,229 @@ public sealed partial class SprayPainterWindow : DefaultWindow
public string? IndexToColorKey(int index)
{
return (string?) ColorList[index].Metadata;
return _colorList[index].Text;
}
public void Populate(List<SprayPainterEntry> entries, int selectedStyle, string? selectedColorKey, Dictionary<string, Color> palette)
private void OnStyleSelected(ListData data)
{
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
if (!CurrentEntries.Equals(entries))
if (data is SpriteListData listData)
OnSpritePicked?.Invoke(listData.Group, listData.Style);
}
/// <summary>
/// Wrapper to allow for selecting/deselecting the event to avoid loops
/// </summary>
private void OnColorPicked(ItemList.ItemListSelectedEventArgs args)
{
OnSetPipeColor?.Invoke(args);
}
/// <summary>
/// Setup function for the window.
/// </summary>
/// <param name="stylesByGroup">Each group, mapped by name to the set of named styles by their associated entity prototype.</param>
/// <param name="groupsByCategory">The set of categories and the groups associated with them.</param>
/// <param name="decals">A list of each decal.</param>
public void PopulateCategories(Dictionary<string, Dictionary<string, EntProtoId>> stylesByGroup, Dictionary<string, List<string>> groupsByCategory, List<SprayPainterDecalEntry> decals)
{
bool tabsCleared = false;
var lastTab = Tabs.CurrentTab;
if (!_currentGroupsByCategory.Equals(groupsByCategory))
{
CurrentEntries = entries;
SpriteList.Clear();
foreach (var entry in entries)
// Destroy all existing tabs
tabsCleared = true;
_paintableControls.Clear();
_pipeControl = null;
_sprayPainterDecals = null;
Tabs.RemoveAllChildren();
}
// Only clear if the entries change. Otherwise the list would "jump" after selecting an item
if (tabsCleared || !_currentStylesByGroup.Equals(stylesByGroup))
{
_currentStylesByGroup = stylesByGroup;
var tabIndex = 0;
foreach (var (categoryName, categoryGroups) in groupsByCategory.OrderBy(c => c.Key))
{
SpriteList.AddItem(entry.Name, entry.Icon);
if (categoryGroups.Count <= 0)
continue;
// Repopulating controls:
// ensure that categories with multiple groups have separate subtabs
// but single-group categories do not.
if (tabsCleared)
{
TabContainer? subTabs = null;
if (categoryGroups.Count > 1)
subTabs = new();
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles))
continue;
var groupControl = new SprayPainterGroup();
groupControl.OnButtonPressed += OnStyleSelected;
_paintableControls[group] = groupControl;
if (categoryGroups.Count > 1)
{
if (subTabs != null)
{
subTabs?.AddChild(groupControl);
var subTabLocalization = Loc.GetString("spray-painter-tab-group-" + group.ToLower());
TabContainer.SetTabTitle(groupControl, subTabLocalization);
}
}
else
{
Tabs.AddChild(groupControl);
}
}
if (subTabs != null)
Tabs.AddChild(subTabs);
var tabLocalization = Loc.GetString("spray-painter-tab-category-" + categoryName.ToLower());
Tabs.SetTabTitle(tabIndex, tabLocalization);
tabIndex++;
}
// Finally, populate all groups with new data.
foreach (var group in categoryGroups)
{
if (!stylesByGroup.TryGetValue(group, out var styles) ||
!_paintableControls.TryGetValue(group, out var control))
continue;
var dataList = styles
.Select(e => new SpriteListData(group, e.Key, e.Value, 0))
.OrderBy(d => Loc.GetString($"spray-painter-style-{group.ToLower()}-{d.Style.ToLower()}"))
.ToList();
control.PopulateList(dataList);
}
}
}
if (!currentPalette.Equals(palette))
{
currentPalette = palette;
ItemColorIndex.Clear();
ColorList.Clear();
PopulateColors(_currentPalette);
if (!_currentDecals.Equals(decals))
{
_currentDecals = decals;
if (_sprayPainterDecals is null)
{
_sprayPainterDecals = new SprayPainterDecals();
_sprayPainterDecals.OnDecalSelected += id => OnDecalChanged?.Invoke(id);
_sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
_sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
_sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
Tabs.AddChild(_sprayPainterDecals);
TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
}
_sprayPainterDecals.PopulateDecals(decals, _spriteSystem);
}
if (tabsCleared)
SetSelectedTab(lastTab);
}
public void PopulateColors(Dictionary<string, Color> palette)
{
// Create pipe tab controls if they don't exist
bool tabCreated = false;
if (_pipeControl == null)
{
_pipeControl = new BoxContainer() { Orientation = BoxContainer.LayoutOrientation.Vertical };
var label = new Label() { Text = Loc.GetString("spray-painter-selected-color") };
_colorList = new ItemList() { VerticalExpand = true };
_colorList.OnItemSelected += OnColorPicked;
_pipeControl.AddChild(label);
_pipeControl.AddChild(_colorList);
Tabs.AddChild(_pipeControl);
TabContainer.SetTabTitle(_pipeControl, Loc.GetString("spray-painter-tab-category-pipes"));
tabCreated = true;
}
// Populate the tab if needed (new tab/new data)
if (tabCreated || !_currentPalette.Equals(palette))
{
_currentPalette = palette;
ItemColorIndex.Clear();
_colorList.Clear();
int index = 0;
foreach (var color in palette)
{
var locString = GetColorLocString(color.Key);
var item = ColorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture));
var item = _colorList.AddItem(locString, _spriteSystem.Frame0(_colorEntryIconTexture), metadata: color.Key);
item.IconModulate = color.Value;
item.Metadata = color.Key;
ItemColorIndex.Add(color.Key, ColorList.IndexOf(item));
ItemColorIndex.Add(color.Key, index);
index++;
}
}
// Disable event so we don't send a new event for pre-selectedStyle entry and end up in a loop
if (selectedColorKey != null)
{
var index = ItemColorIndex[selectedColorKey];
ColorList.OnItemSelected -= OnColorPicked;
ColorList[index].Selected = true;
ColorList.OnItemSelected += OnColorPicked;
}
SpriteList.OnItemSelected -= OnSpritePicked;
SpriteList[selectedStyle].Selected = true;
SpriteList.OnItemSelected += OnSpritePicked;
}
# region Setters
public void SetSelectedStyles(Dictionary<string, string> selectedStyles)
{
foreach (var (group, style) in selectedStyles)
{
if (!_paintableControls.TryGetValue(group, out var control))
continue;
control.SelectItemByStyle(style);
}
}
public void SelectColor(string color)
{
if (_colorList != null && ItemColorIndex.TryGetValue(color, out var colorIdx))
{
_colorList.OnItemSelected -= OnColorPicked;
_colorList[colorIdx].Selected = true;
_colorList.OnItemSelected += OnColorPicked;
}
}
public void SetSelectedTab(int tab)
{
Tabs.CurrentTab = int.Min(tab, Tabs.ChildCount - 1);
}
public void SetSelectedDecal(string decal)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSelectedDecal(decal);
}
public void SetDecalAngle(int angle)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetAngle(angle);
}
public void SetDecalColor(Color? color)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetColor(color);
}
public void SetDecalSnap(bool snap)
{
if (_sprayPainterDecals != null)
_sprayPainterDecals.SetSnap(snap);
}
# endregion
}
public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;

View File

@@ -1,50 +0,0 @@
using Content.Shared.StatusEffectNew;
using Content.Shared.StatusEffectNew.Components;
using Robust.Shared.Collections;
using Robust.Shared.GameStates;
namespace Content.Client.StatusEffectNew;
/// <inheritdoc/>
public sealed partial class ClientStatusEffectsSystem : SharedStatusEffectsSystem
{
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StatusEffectContainerComponent, ComponentHandleState>(OnHandleState);
}
private void OnHandleState(Entity<StatusEffectContainerComponent> ent, ref ComponentHandleState args)
{
if (args.Current is not StatusEffectContainerComponentState state)
return;
var toRemove = new ValueList<EntityUid>();
foreach (var effect in ent.Comp.ActiveStatusEffects)
{
if (state.ActiveStatusEffects.Contains(GetNetEntity(effect)))
continue;
toRemove.Add(effect);
}
foreach (var effect in toRemove)
{
ent.Comp.ActiveStatusEffects.Remove(effect);
var ev = new StatusEffectRemovedEvent(ent);
RaiseLocalEvent(effect, ref ev);
}
foreach (var effect in state.ActiveStatusEffects)
{
var effectUid = GetEntity(effect);
if (ent.Comp.ActiveStatusEffects.Contains(effectUid))
continue;
ent.Comp.ActiveStatusEffects.Add(effectUid);
var ev = new StatusEffectAppliedEvent(ent);
RaiseLocalEvent(effectUid, ref ev);
}
}
}

View File

@@ -1,10 +1,15 @@
using Content.Shared.SprayPainter.Prototypes;
using Content.Shared.Storage;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client.Storage.Visualizers;
public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStorageVisualsComponent>
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
public override void Initialize()
{
base.Initialize();
@@ -26,12 +31,34 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
SpriteSystem.LayerSetRsiState((uid, sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
protected override void OnAppearanceChange(EntityUid uid, EntityStorageVisualsComponent comp, ref AppearanceChangeEvent args)
protected override void OnAppearanceChange(EntityUid uid,
EntityStorageVisualsComponent comp,
ref AppearanceChangeEvent args)
{
if (args.Sprite == null
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
|| !AppearanceSystem.TryGetData<bool>(uid, StorageVisuals.Open, out var open, args.Component))
return;
var forceRedrawBase = false;
if (AppearanceSystem.TryGetData<string>(uid, PaintableVisuals.Prototype, out var prototype, args.Component))
{
if (_prototypeManager.TryIndex(prototype, out var proto))
{
if (proto.TryGetComponent(out SpriteComponent? sprite, _componentFactory))
{
SpriteSystem.SetBaseRsi((uid, args.Sprite), sprite.BaseRSI);
}
if (proto.TryGetComponent(out EntityStorageVisualsComponent? visuals, _componentFactory))
{
comp.StateBaseOpen = visuals.StateBaseOpen;
comp.StateBaseClosed = visuals.StateBaseClosed;
comp.StateDoorOpen = visuals.StateDoorOpen;
comp.StateDoorClosed = visuals.StateDoorClosed;
forceRedrawBase = true;
}
}
}
// Open/Closed state for the storage entity.
if (SpriteSystem.LayerMapTryGet((uid, args.Sprite), StorageVisualLayers.Door, out _, false))
{
@@ -52,6 +79,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
if (comp.StateBaseOpen != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
else if (forceRedrawBase && comp.StateBaseClosed != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
}
else
{
@@ -68,6 +97,8 @@ public sealed class EntityStorageVisualizerSystem : VisualizerSystem<EntityStora
if (comp.StateBaseClosed != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseClosed);
else if (forceRedrawBase && comp.StateBaseOpen != null)
SpriteSystem.LayerSetRsiState((uid, args.Sprite), StorageVisualLayers.Base, comp.StateBaseOpen);
}
}
}

View File

@@ -1,9 +1,182 @@
using System.Numerics;
using Content.Shared.CombatMode;
using Content.Shared.Interaction;
using Content.Shared.Stunnable;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Random;
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 SpriteSystem _spriteSystem = default!;
private readonly int[] _sign = [-1, 1];
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StunVisualsComponent, ComponentInit>(OnComponentInit);
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>
/// Add stun visual layers
/// </summary>
private void OnComponentInit(Entity<StunVisualsComponent> entity, ref ComponentInit args)
{
if (!TryComp<SpriteComponent>(entity, out var sprite))
return;
var spriteEntity = (entity.Owner, sprite);
_spriteSystem.LayerMapReserve(spriteEntity, StunVisualLayers.StamCrit);
_spriteSystem.LayerSetVisible(spriteEntity, StunVisualLayers.StamCrit, false);
_spriteSystem.LayerSetOffset(spriteEntity, StunVisualLayers.StamCrit, new Vector2(0, 0.3125f));
_spriteSystem.LayerSetRsi(spriteEntity, StunVisualLayers.StamCrit, entity.Comp.StarsPath);
UpdateAppearance((entity, sprite), entity.Comp.State);
}
private void OnAppearanceChanged(Entity<StunVisualsComponent> entity, ref AppearanceChangeEvent args)
{
if (args.Sprite != null)
UpdateAppearance((entity, args.Sprite), entity.Comp.State);
}
private void UpdateAppearance(Entity<SpriteComponent?> entity, string state)
{
if (!Resolve(entity, ref entity.Comp))
return;
if (!_spriteSystem.LayerMapTryGet((entity, entity.Comp), StunVisualLayers.StamCrit, out var index, false))
return;
var visible = Appearance.TryGetData<bool>(entity, StunVisuals.SeeingStars, out var stars) && stars;
_spriteSystem.LayerSetVisible((entity, entity.Comp), index, visible);
_spriteSystem.LayerSetRsiState((entity, entity.Comp), index, state);
}
/// <summary>
/// A simple fatigue animation, a mild modification of the jittering animation. The animation constructor is
/// quite complex, but that's because the AnimationSystem doesn't have proper adjustment layers. In a potential
/// future where proper adjustment layers are added feel free to clean this up to be an animation with two adjustment
/// layers rather than one mega layer.
/// </summary>
/// <param name="sprite">The spriteComponent we're adjusting the offset of</param>
/// <param name="frequency">How many times per second does the animation run?</param>
/// <param name="jitters">How many times should we jitter during the animation? Also determines breathing frequency</param>
/// <param name="minJitter">Mininum jitter offset multiplier for X and Y directions</param>
/// <param name="maxJitter">Maximum jitter offset multiplier for X and Y directions</param>
/// <param name="breathing">Maximum breathing offset, this is in the Y direction</param>
/// <param name="startOffset">Starting offset because we don't have adjustment layers</param>
/// <param name="lastJitter">Last jitter so we don't jitter to the same quadrant</param>
/// <returns></returns>
public Animation GetFatigueAnimation(SpriteComponent sprite,
float frequency,
int jitters,
Vector2 minJitter,
Vector2 maxJitter,
float breathing,
Vector2 startOffset,
ref Vector2 lastJitter)
{
// avoid animations with negative length or infinite length
if (frequency <= 0)
return new Animation();
var breaths = new Vector2(0, breathing * 2) / jitters;
var length = 1 / frequency;
var frames = length / jitters;
var keyFrames = new List<AnimationTrackProperty.KeyFrame> { new(sprite.Offset, 0f) };
// Spits out a list of keyframes to feed to the AnimationPlayer based on the variables we've inputted
for (var i = 1; i <= jitters; i++)
{
var offset = new Vector2(_random.NextFloat(minJitter.X, maxJitter.X),
_random.NextFloat(minJitter.Y, maxJitter.Y));
offset.X *= _random.Pick(_sign);
offset.Y *= _random.Pick(_sign);
if (i == 1 && Math.Sign(offset.X) == Math.Sign(lastJitter.X)
&& Math.Sign(offset.Y) == Math.Sign(lastJitter.Y))
{
// If the sign is the same as last time on both axis we flip one randomly
// to avoid jitter staying in one quadrant too much.
if (_random.Prob(0.5f))
offset.X *= -1;
else
offset.Y *= -1;
}
lastJitter = offset;
// For the first half of the jitter, we vertically displace the sprite upwards to simulate breathing in
if (i <= jitters / 2)
{
keyFrames.Add(new AnimationTrackProperty.KeyFrame(startOffset + breaths * i + offset, frames));
}
// For the next quarter we displace the sprite down, to about 12.5% breathing offset below our starting position
// Simulates breathing out
else if (i < jitters * 3 / 4)
{
keyFrames.Add(
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( jitters - i * 1.5f ) + offset, frames));
}
// Return to our starting position for breathing, jitter reaches its final position
else
{
keyFrames.Add(
new AnimationTrackProperty.KeyFrame(startOffset + breaths * ( i - jitters ) + offset, frames));
}
}
return new Animation
{
Length = TimeSpan.FromSeconds(length),
AnimationTracks =
{
new AnimationTrackComponentProperty
{
// Heavy Breathing
ComponentType = typeof(SpriteComponent),
Property = nameof(SpriteComponent.Offset),
InterpolationMode = AnimationInterpolationMode.Cubic,
KeyFrames = keyFrames,
},
}
};
}
}
public enum StunVisualLayers : byte
{
StamCrit,
}

View File

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

View File

@@ -1,3 +1,4 @@
using System.Numerics;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
@@ -43,7 +44,7 @@ public sealed class ProgressColorSystem : EntitySystem
// lerp
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);

View File

@@ -185,7 +185,12 @@ public sealed class ItemGridPiece : Control, IEntityControl
handle.SetTransform(pos, iconRotation);
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);
}
else
@@ -298,6 +303,19 @@ public sealed class ItemGridPiece : Control, IEntityControl
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
{
First,

View File

@@ -621,7 +621,7 @@ public sealed class StorageWindow : BaseWindow
{
marked.Add(cell);
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");
}
}

View File

@@ -56,7 +56,7 @@ namespace Content.IntegrationTests.Tests
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
@@ -112,7 +112,7 @@ namespace Content.IntegrationTests.Tests
entityMan.SpawnEntity(protoId, map.GridCoords);
}
});
await server.WaitRunTicks(15);
await server.WaitRunTicks(450); // 15 seconds, enough to trigger most update loops
await server.WaitPost(() =>
{
static IEnumerable<(EntityUid, TComp)> Query<TComp>(IEntityManager entityMan)
@@ -270,7 +270,7 @@ namespace Content.IntegrationTests.Tests
await pair.RunTicksSync(3);
// 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>);
await Assert.MultipleAsync(async () =>

View File

@@ -1,4 +1,4 @@
using Content.Server.Stunnable;
using Content.Server.Stunnable;
using Content.Shared.Inventory;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
@@ -17,9 +17,7 @@ namespace Content.IntegrationTests.Tests
components:
- type: Inventory
- type: ContainerContainer
- type: StatusEffects
allowed:
- Stun
- type: MobState
- type: entity
name: InventoryJumpsuitJanitorDummy
@@ -70,7 +68,7 @@ namespace Content.IntegrationTests.Tests
});
#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
// Since the mob is stunned, they can't equip this.

View File

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

View File

@@ -1,4 +1,3 @@
using System.Threading;
using Content.Server.GameTicking;
using Content.Server.RoundEnd;
using Content.Shared.CCVar;
@@ -22,7 +21,7 @@ namespace Content.IntegrationTests.Tests
private void OnRoundEnd(RoundEndSystemChangedEvent ev)
{
Interlocked.Increment(ref RoundCount);
RoundCount += 1;
}
}
@@ -127,13 +126,17 @@ namespace Content.IntegrationTests.Tests
async Task WaitForEvent()
{
var timeout = Task.Delay(TimeSpan.FromSeconds(10));
var currentCount = Thread.VolatileRead(ref sys.RoundCount);
while (currentCount == Thread.VolatileRead(ref sys.RoundCount) && !timeout.IsCompleted)
const int maxTicks = 60;
var currentCount = sys.RoundCount;
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

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

@@ -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.Popups;
using Content.Shared.Slippery;
using Content.Shared.Stunnable;
using Content.Shared.Tabletop.Components;
using Content.Shared.Tools.Systems;
using Content.Shared.Verbs;
@@ -48,6 +49,7 @@ using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Player;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Timer = Robust.Shared.Timing.Timer;
@@ -877,7 +879,7 @@ public sealed partial class AdminVerbSystem
if (!hadSlipComponent)
{
slipComponent.SlipData.SuperSlippery = true;
slipComponent.SlipData.ParalyzeTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.StunTime = TimeSpan.FromSeconds(5);
slipComponent.SlipData.LaunchForwardsMultiplier = 20;
}
@@ -922,5 +924,20 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", omniaccentName, Loc.GetString("admin-smite-omni-accent-description"))
};
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.EUI;
using Content.Server.Ghost.Roles;
using Content.Server.Mind.Commands;
using Content.Server.Mind;
using Content.Server.Prayer;
using Content.Server.Silicons.Laws;
@@ -16,7 +15,6 @@ using Content.Shared.Configurable;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.GameTicking;
using Content.Shared.Hands.Components;
using Content.Shared.Inventory;
using Content.Shared.Mind.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"),
});
}
// 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"),
Category = VerbCategory.Debug,
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
};
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

@@ -20,7 +20,7 @@ public sealed partial class ParrotMemoryComponent : Component
/// The % chance an entity with this component learns a phrase when learning is off cooldown
/// </summary>
[DataField]
public float LearnChance = 0.4f;
public float LearnChance = 0.6f;
/// <summary>
/// Time after which another attempt can be made at learning a phrase

View File

@@ -96,7 +96,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
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);
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)
{
_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);
}
@@ -213,7 +213,7 @@ public sealed class InnerBodyAnomalySystem : SharedInnerBodyAnomalySystem
if (_proto.TryIndex(ent.Comp.InjectionProto, out var injectedAnom))
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 &&
_mind.TryGetMind(ent, out _, out var mindComponent) &&

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Map.Components;
using Robust.Shared.Timing;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Atmos.EntitySystems;
using Content.Shared.DeviceNetwork.Components;
using Content.Shared.NodeContainer;
@@ -53,6 +54,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
// Grid events
SubscribeLocalEvent<GridSplitEvent>(OnGridSplit);
SubscribeLocalEvent<PipeNodeGroupRemovedEvent>(OnPipeNodeGroupRemoved);
}
#region Event handling
@@ -295,6 +297,25 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
#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)
{
var allChunks = new Dictionary<Vector2i, AtmosPipeChunk>();
@@ -411,7 +432,7 @@ public sealed class AtmosMonitoringConsoleSystem : SharedAtmosMonitoringConsoleS
continue;
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;
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)
{
// 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);
}
@@ -329,3 +337,12 @@ public partial class AtmosphereSystem
[ByRefEvent] private record struct IsHotspotActiveMethodEvent
(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

@@ -596,8 +596,17 @@ namespace Content.Server.Atmos.EntitySystems
if (!reconsiderAdjacent)
return;
// Before updating the adjacent tile flags that determine whether air is allowed to flow
// or not, we explicitly update airtight data on these tiles right now.
// This ensures that UpdateAdjacentTiles has updated data before updating flags.
// This allows monstermos' floodfill check that determines if firelocks have dropped
// to work correctly.
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, tile);
UpdateAirtightData(ent.Owner, ent.Comp1, ent.Comp3, other);
UpdateAdjacentTiles(ent, tile);
UpdateAdjacentTiles(ent, other);
InvalidateVisuals(ent, tile);
InvalidateVisuals(ent, other);
}

View File

@@ -394,7 +394,7 @@ namespace Content.Server.Atmos.EntitySystems
flammable.Resisting = true;
_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...
uid.SpawnTimer(2000, () =>

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

@@ -1,44 +0,0 @@
namespace Content.Server.Atmos
{
public abstract class PressureEvent : EntityEventArgs
{
/// <summary>
/// The environment pressure.
/// </summary>
public float Pressure { get; }
/// <summary>
/// The modifier for the apparent pressure.
/// This number will be added to the environment pressure for calculation purposes.
/// It can be negative to reduce the felt pressure, or positive to increase it.
/// </summary>
/// <remarks>
/// Do not set this directly. Add to it, or subtract from it to modify it.
/// </remarks>
public float Modifier { get; set; } = 0f;
/// <summary>
/// The multiplier for the apparent pressure.
/// The environment pressure will be multiplied by this for calculation purposes.
/// </summary>
/// <remarks>
/// Do not set, add to or subtract from this directly. Multiply this by your multiplier only.
/// </remarks>
public float Multiplier { get; set; } = 1f;
protected PressureEvent(float pressure)
{
Pressure = pressure;
}
}
public sealed class LowPressureEvent : PressureEvent
{
public LowPressureEvent(float pressure) : base(pressure) { }
}
public sealed class HighPressureEvent : PressureEvent
{
public HighPressureEvent(float pressure) : base(pressure) { }
}
}

View File

@@ -1,12 +1,13 @@
using System.Linq;
using Content.Server.Administration;
using Content.Server.Body.Systems;
using Content.Server.Hands.Systems;
using Content.Shared.Administration;
using Content.Shared.Body.Components;
using Content.Shared.Body.Part;
using Content.Shared.Hands.Components;
using Robust.Shared.Console;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Server.Body.Commands
{
@@ -15,9 +16,9 @@ namespace Content.Server.Body.Commands
{
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IRobustRandom _random = default!;
private static readonly EntProtoId DefaultHandPrototype = "LeftHandHuman";
private static int _handIdAccumulator;
public string Command => "addhand";
public string Description => "Adds a hand to your entity.";
@@ -114,9 +115,17 @@ namespace Content.Server.Body.Commands
if (!_entManager.TryGetComponent(entity, out BodyComponent? body) || body.RootContainer.ContainedEntity == null)
{
var text = $"You have no body{(_random.Prob(0.2f) ? " and you must scream." : ".")}";
var location = _entManager.GetComponentOrNull<BodyPartComponent>(hand)?.Symmetry switch
{
BodyPartSymmetry.None => HandLocation.Middle,
BodyPartSymmetry.Left => HandLocation.Left,
BodyPartSymmetry.Right => HandLocation.Right,
_ => HandLocation.Right
};
_entManager.DeleteEntity(hand);
shell.WriteLine(text);
// You have no body and you must scream.
_entManager.System<HandsSystem>().AddHand(entity, $"{hand}-cmd-{_handIdAccumulator++}", location);
return;
}

View File

@@ -10,16 +10,6 @@ namespace Content.Server.Body.Components
[RegisterComponent, Access(typeof(RespiratorSystem)), AutoGenerateComponentPause]
public sealed partial class RespiratorComponent : Component
{
/// <summary>
/// Gas container for this entity
/// </summary>
[DataField]
public GasMixture Air = new()
{
Volume = 6, // 6 liters, the average lung capacity for a human according to Google
Temperature = Atmospherics.NormalBodyTemperature
};
/// <summary>
/// Volume of our breath in liters
/// </summary>

View File

@@ -48,7 +48,7 @@ public sealed class InternalsSystem : SharedInternalsSystem
return;
// Could the entity metabolise the air in the linked gas tank?
if (!_respirator.CanMetabolizeGas(uid, tank.Value.Comp.Air))
if (!_respirator.CanMetabolizeInhaledAir(uid, tank.Value.Comp.Air))
return;
ToggleInternals(uid, uid, force: false, component, ToggleMode.On);

View File

@@ -49,8 +49,13 @@ public sealed class RespiratorSystem : EntitySystem
UpdatesAfter.Add(typeof(MetabolizerSystem));
SubscribeLocalEvent<RespiratorComponent, MapInitEvent>(OnMapInit);
SubscribeLocalEvent<RespiratorComponent, ApplyMetabolicMultiplierEvent>(OnApplyMetabolicMultiplier);
// BodyComp stuff
SubscribeLocalEvent<BodyComponent, InhaledGasEvent>(OnGasInhaled);
SubscribeLocalEvent<BodyComponent, ExhaledGasEvent>(OnGasExhaled);
SubscribeLocalEvent<BodyComponent, CanMetabolizeGasEvent>(CanBodyMetabolizeGas);
SubscribeLocalEvent<BodyComponent, SuffocationEvent>(OnSuffocation);
SubscribeLocalEvent<BodyComponent, StopSuffocatingEvent>(OnStopSuffocating);
}
private void OnMapInit(Entity<RespiratorComponent> ent, ref MapInitEvent args)
@@ -80,11 +85,11 @@ public sealed class RespiratorSystem : EntitySystem
switch (respirator.Status)
{
case RespiratorStatus.Inhaling:
Inhale(uid);
Inhale((uid, respirator));
respirator.Status = RespiratorStatus.Exhaling;
break;
case RespiratorStatus.Exhaling:
Exhale(uid);
Exhale((uid, respirator));
respirator.Status = RespiratorStatus.Inhaling;
break;
}
@@ -111,10 +116,10 @@ public sealed class RespiratorSystem : EntitySystem
}
}
public bool Inhale(Entity<RespiratorComponent?> entity)
public void Inhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return false;
return;
// Inhale gas
var ev = new InhaleLocationEvent
@@ -126,23 +131,22 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= _atmosSys.GetContainingMixture(entity.Owner, excite: true);
if (ev.Gas is null)
{
return false;
}
return;
var gas = ev.Gas.RemoveVolume(entity.Comp.BreathVolume);
var inhaleEv = new InhaledGasEvent(gas);
RaiseLocalEvent(entity, ref inhaleEv);
return inhaleEv.Handled && inhaleEv.Succeeded;
}
public void Exhale(Entity<RespiratorComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
if (inhaleEv.Handled && inhaleEv.Succeeded)
return;
// If nothing could inhale the gas give it back.
_atmosSys.Merge(ev.Gas, gas);
}
public void Exhale(Entity<RespiratorComponent> entity)
{
// exhale gas
var ev = new ExhaleLocationEvent();
@@ -157,8 +161,16 @@ public sealed class RespiratorSystem : EntitySystem
ev.Gas ??= GasMixture.SpaceGas;
}
var exhaleEv = new ExhaledGasEvent(ev.Gas);
RaiseLocalEvent(entity, ref exhaleEv);
Exhale(entity!, ev.Gas);
}
public void Exhale(Entity<RespiratorComponent?> entity, GasMixture gas)
{
if (!Resolve(entity, ref entity.Comp, logMissing: false))
return;
var ev = new ExhaledGasEvent(gas);
RaiseLocalEvent(entity, ref ev);
}
/// <summary>
@@ -176,52 +188,76 @@ public sealed class RespiratorSystem : EntitySystem
}
/// <summary>
/// Check whether or not an entity can metabolize inhaled air without suffocating or taking damage (i.e., no toxic
/// gasses).
/// Checks if it's safe for a given entity to breathe the air from the environment it is currently situated in.
/// </summary>
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <returns>Returns true only if the air is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
if (!Inhale(ent))
return false;
// Get the gas at our location but don't actually remove it from the gas mixture.
var ev = new InhaleLocationEvent
{
Respirator = ent.Comp,
};
RaiseLocalEvent(ent, ref ev);
// If we don't have a body we can't be poisoned by gas, yet...
var success = TryMetabolizeGas((ent, ent.Comp));
ev.Gas ??= _atmosSys.GetContainingMixture(ent.Owner, excite: true);
// Don't keep that gas in our lungs lest it poisons a poor nuclear operative.
Exhale(ent);
return success;
// If there's no air to breathe or we can't metabolize it then internals should be on.
return ev.Gas is not null && CanMetabolizeInhaledAir(ent, ev.Gas);
}
/// <summary>
/// Check whether or not an entity can metabolize the given gas mixture without suffocating or taking damage
/// (i.e., no toxic gasses).
/// Checks if a given entity can safely metabolize a given gas mixture.
/// </summary>
public bool CanMetabolizeGas(Entity<RespiratorComponent?> ent, GasMixture gas)
/// <param name="ent">The entity attempting to metabolize the gas.</param>
/// <param name="gas">The gas mixture we are trying to metabolize.</param>
/// <returns>Returns true only if the gas mixture is not toxic, and it wouldn't suffocate.</returns>
public bool CanMetabolizeInhaledAir(Entity<RespiratorComponent?> ent, GasMixture gas)
{
if (!Resolve(ent, ref ent.Comp))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
var ev = new CanMetabolizeGasEvent(gas);
RaiseLocalEvent(ent, ref ev);
if (!ev.Handled || ev.Toxic)
return false;
gas = new GasMixture(gas);
var lungRatio = 1.0f / organs.Count;
gas.Multiply(MathF.Min(lungRatio * gas.Volume / ent.Comp.BreathVolume, lungRatio));
var solution = _lungSystem.GasToReagent(gas);
return ev.Saturation > ent.Comp.UpdateInterval.TotalSeconds;
}
float saturation = 0;
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private void CanBodyMetabolizeGas(Entity<BodyComponent> ent, ref CanMetabolizeGasEvent args)
{
if (args.Handled)
return;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return;
var solution = _lungSystem.GasToReagent(args.Gas);
var saturation = 0f;
foreach (var organ in organs)
{
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
if (!toxic)
continue;
args.Handled = true;
args.Toxic = true;
return;
}
return saturation > ent.Comp.UpdateInterval.TotalSeconds;
args.Handled = true;
args.Saturation = saturation;
}
public bool TryInhaleGasToBody(Entity<BodyComponent?> entity, GasMixture gas)
@@ -265,30 +301,6 @@ public sealed class RespiratorSystem : EntitySystem
_atmosSys.Merge(gas, outGas);
}
/// <summary>
/// Tries to safely metabolize the current solutions in a body's lungs.
/// </summary>
private bool TryMetabolizeGas(Entity<RespiratorComponent, BodyComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp2))
return false;
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
if (organs.Count == 0)
return false;
float saturation = 0;
foreach (var organ in organs)
{
var solution = _lungSystem.GasToReagent(organ.Comp1.Air);
saturation += GetSaturation(solution, organ.Owner, out var toxic);
if (toxic)
return false;
}
return saturation > ent.Comp1.UpdateInterval.TotalSeconds;
}
/// <summary>
/// Get the amount of saturation that would be generated if the lung were to metabolize the given solution.
/// </summary>
@@ -354,15 +366,11 @@ public sealed class RespiratorSystem : EntitySystem
_damageableSys.TryChangeDamage(ent, ent.Comp.Damage, interruptsDoAfters: false);
if (ent.Comp.SuffocationCycles >= ent.Comp.SuffocationCycleThreshold)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
if (ent.Comp.SuffocationCycles < ent.Comp.SuffocationCycleThreshold)
return;
var ev = new SuffocationEvent();
RaiseLocalEvent(ent, ref ev);
}
private void StopSuffocation(Entity<RespiratorComponent> ent)
@@ -372,6 +380,22 @@ public sealed class RespiratorSystem : EntitySystem
_damageableSys.TryChangeDamage(ent, ent.Comp.DamageRecovery);
var ev = new StopSuffocatingEvent();
RaiseLocalEvent(ent, ref ev);
}
private void OnSuffocation(Entity<BodyComponent> ent, ref SuffocationEvent args)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
{
_alertsSystem.ShowAlert(ent, entity.Comp1.Alert);
}
}
private void OnStopSuffocating(Entity<BodyComponent> ent, ref StopSuffocatingEvent args)
{
// TODO: This is not going work with multiple different lungs, if that ever becomes a possibility
var organs = _bodySystem.GetBodyOrganEntityComps<LungComponent>((ent, null));
foreach (var entity in organs)
@@ -397,6 +421,9 @@ public sealed class RespiratorSystem : EntitySystem
private void OnGasInhaled(Entity<BodyComponent> entity, ref InhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
args.Succeeded = TryInhaleGasToBody((entity, entity.Comp), args.Gas);
@@ -404,20 +431,65 @@ public sealed class RespiratorSystem : EntitySystem
private void OnGasExhaled(Entity<BodyComponent> entity, ref ExhaledGasEvent args)
{
if (args.Handled)
return;
args.Handled = true;
RemoveGasFromBody(entity, args.Gas);
}
}
/// <summary>
/// Event raised when an entity first tries to inhale that returns a GasMixture from a given location.
/// </summary>
/// <param name="Gas">The gas that gets returned, null if there is none.</param>
/// <param name="Respirator">The Respirator component of the entity attempting to inhale</param>
[ByRefEvent]
public record struct InhaleLocationEvent(GasMixture? Gas, RespiratorComponent Respirator);
/// <summary>
/// Event raised when an entity first tries to exhale a gas, determines where the gas they're exhaling will be sent.
/// </summary>
/// <param name="Gas">The gas mixture that the exhaled gas will be merged into.</param>
[ByRefEvent]
public record struct ExhaleLocationEvent(GasMixture? Gas);
/// <summary>
/// Event raised when an entity successfully inhales a gas, attempts to find a place to put the gas.
/// </summary>
/// <param name="Gas">The gas we're inhaling.</param>
/// <param name="Handled">Whether a system has responded appropriately.</param>
/// <param name="Succeeded">Whether we successfully managed to inhale the gas</param>
[ByRefEvent]
public record struct InhaledGasEvent(GasMixture Gas, bool Handled = false, bool Succeeded = false);
/// <summary>
/// Event raised when an entity is exhaling
/// </summary>
/// <param name="Gas">The gas mixture we're exhaling into.</param>
/// <param name="Handled">Whether we have successfully exhaled or not.</param>
[ByRefEvent]
public record struct ExhaledGasEvent(GasMixture Gas, bool Handled = false);
/// <summary>
/// Raised when an entity starts suffocating and when suffocation progresses.
/// </summary>
[ByRefEvent]
public record struct SuffocationEvent;
/// <summary>
/// Raised when an entity that was suffocating stops suffocating.
/// </summary>
[ByRefEvent]
public record struct StopSuffocatingEvent;
/// <summary>
/// An event raised to inhalation handlers that asks them nicely to simulate what it would be like to metabolize
/// a given volume of gas, without actually metabolizing it.
/// </summary>
/// <param name="Gas">The gas mixture we are testing.</param>
/// <param name="Toxic">Whether the gas returns as toxic to any respirator.</param>
/// <param name="Saturation">The amount of saturation we got from the gas.</param>
[ByRefEvent]
public record struct CanMetabolizeGasEvent(GasMixture Gas, bool Toxic = false, float Saturation = 0f, bool Handled = false);

View File

@@ -1,5 +1,7 @@
using System.Collections.Frozen;
using Content.Server.Popups;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Emoting;
using Content.Shared.Speech;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -10,6 +12,8 @@ namespace Content.Server.Chat.Systems;
// emotes using emote prototype
public partial class ChatSystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
private FrozenDictionary<string, EmotePrototype> _wordEmoteDict = FrozenDictionary<string, EmotePrototype>.Empty;
protected override void OnPrototypeReload(PrototypesReloadedEventArgs obj)
@@ -51,7 +55,8 @@ public partial class ChatSystem
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat(
EntityUid source,
string emoteId,
ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -62,8 +67,8 @@ public partial class ChatSystem
)
{
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
return;
TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
return false;
return TryEmoteWithChat(source, proto, range, hideLog: hideLog, nameOverride, ignoreActionBlocker: ignoreActionBlocker, forceEmote: forceEmote);
}
/// <summary>
@@ -76,7 +81,8 @@ public partial class ChatSystem
/// <param name="range">Conceptual range of transmission, if it shows in the chat window, if it shows to far-away ghosts or ghosts at all...</param>
/// <param name="nameOverride">The name to use for the speaking entity. Usually this should just be modified via <see cref="TransformSpeakerNameEvent"/>. If this is set, the event will not get raised.</param>
/// <param name="forceEmote">Bypasses whitelist/blacklist/availibility checks for if the entity can use this emote</param>
public void TryEmoteWithChat(
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithChat(
EntityUid source,
EmotePrototype emote,
ChatTransmitRange range = ChatTransmitRange.Normal,
@@ -84,43 +90,46 @@ public partial class ChatSystem
string? nameOverride = null,
bool ignoreActionBlocker = false,
bool forceEmote = false
)
)
{
if (!forceEmote && !AllowedToUseEmote(source, emote))
return;
return false;
var didEmote = TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
// check if proto has valid message for chat
if (emote.ChatMessages.Count != 0)
if (didEmote && emote.ChatMessages.Count != 0)
{
// not all emotes are loc'd, but for the ones that are we pass in entity
var action = Loc.GetString(_random.Pick(emote.ChatMessages), ("entity", source));
SendEntityEmote(source, action, range, nameOverride, hideLog: hideLog, checkEmote: false, ignoreActionBlocker: ignoreActionBlocker);
}
// do the rest of emote event logic here
TryEmoteWithoutChat(source, emote, ignoreActionBlocker);
return didEmote;
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
public void TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, string emoteId, bool ignoreActionBlocker = false)
{
if (!_prototypeManager.TryIndex<EmotePrototype>(emoteId, out var proto))
return;
return false;
TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
return TryEmoteWithoutChat(uid, proto, ignoreActionBlocker);
}
/// <summary>
/// Makes selected entity to emote using <see cref="EmotePrototype"/> without sending any messages to chat.
/// </summary>
public void TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
/// <returns>True if an emote was performed. False if the emote is unvailable, cancelled, etc.</returns>
public bool TryEmoteWithoutChat(EntityUid uid, EmotePrototype proto, bool ignoreActionBlocker = false)
{
if (!_actionBlocker.CanEmote(uid) && !ignoreActionBlocker)
return;
return false;
InvokeEmoteEvent(uid, proto);
return TryInvokeEmoteEvent(uid, proto);
}
/// <summary>
@@ -160,17 +169,17 @@ public partial class ChatSystem
/// </summary>
/// <param name="uid"></param>
/// <param name="textInput"></param>
private void TryEmoteChatInput(EntityUid uid, string textInput)
/// <returns>True if the chat message should be displayed (because the emote was explicitly cancelled), false if it should not be.</returns>
private bool TryEmoteChatInput(EntityUid uid, string textInput)
{
var actionTrimmedLower = TrimPunctuation(textInput.ToLower());
if (!_wordEmoteDict.TryGetValue(actionTrimmedLower, out var emote))
return;
return true;
if (!AllowedToUseEmote(uid, emote))
return;
return true;
InvokeEmoteEvent(uid, emote);
return;
return TryInvokeEmoteEvent(uid, emote);
static string TrimPunctuation(string textInput)
{
@@ -220,11 +229,50 @@ public partial class ChatSystem
return true;
}
private void InvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
/// <summary>
/// Creates and raises <see cref="BeforeEmoteEvent"/> and then <see cref="EmoteEvent"/> to let other systems do things like play audio.
/// In the case that the Before event is cancelled, EmoteEvent will NOT be raised, and will optionally show a message to the player
/// explaining why the emote didn't happen.
/// </summary>
/// <param name="uid">The entity which is emoting</param>
/// <param name="proto">The emote which is being performed</param>
/// <returns>True if the emote was performed, false otherwise.</returns>
private bool TryInvokeEmoteEvent(EntityUid uid, EmotePrototype proto)
{
var beforeEv = new BeforeEmoteEvent(uid, proto);
RaiseLocalEvent(uid, ref beforeEv);
if (beforeEv.Cancelled)
{
if (beforeEv.Blocker != null)
{
_popupSystem.PopupEntity(
Loc.GetString(
"chat-system-emote-cancelled-blocked",
("emote", Loc.GetString(proto.Name).ToLower()),
("blocker", beforeEv.Blocker.Value)
),
uid,
uid
);
}
else
{
_popupSystem.PopupEntity(
Loc.GetString("chat-system-emote-cancelled-generic",
("emote", Loc.GetString(proto.Name).ToLower())),
uid,
uid
);
}
return false;
}
var ev = new EmoteEvent(proto);
RaiseLocalEvent(uid, ref ev);
return true;
}
}
@@ -233,9 +281,8 @@ public partial class ChatSystem
/// Use it to play sound, change sprite or something else.
/// </summary>
[ByRefEvent]
public struct EmoteEvent
public sealed class EmoteEvent : HandledEntityEventArgs
{
public bool Handled;
public readonly EmotePrototype Emote;
public EmoteEvent(EmotePrototype emote)

View File

@@ -5,9 +5,8 @@ using Content.Server.Administration.Logs;
using Content.Server.Administration.Managers;
using Content.Server.Chat.Managers;
using Content.Server.GameTicking;
using Content.Server.Players.RateLimiting;
using Content.Server.Speech.Prototypes;
using Content.Server.Speech.EntitySystems;
using Content.Server.Speech.Prototypes;
using Content.Server.Station.Components;
using Content.Server.Station.Systems;
using Content.Shared.ActionBlocker;
@@ -536,7 +535,7 @@ public sealed partial class ChatSystem : SharedChatSystem
if (MessageRangeCheck(session, data, range) != MessageRangeCheckResult.Full)
continue; // Won't get logged to chat, and ghosts are too far away to see the pop-up, so we just won't send it to them.
if (data.Range <= WhisperClearRange)
if (data.Range <= WhisperClearRange || data.Observer)
_chatManager.ChatMessageToOne(ChatChannel.Whisper, message, wrappedMessage, source, false, session.Channel);
//If listener is too far, they only hear fragments of the message
else if (_examineSystem.InRangeUnOccluded(source, listener, WhisperMuffledRange))
@@ -593,8 +592,10 @@ public sealed partial class ChatSystem : SharedChatSystem
("entity", ent),
("message", FormattedMessage.RemoveMarkupOrThrow(action)));
if (checkEmote)
TryEmoteChatInput(source, action);
if (checkEmote &&
!TryEmoteChatInput(source, action))
return;
SendInVoiceRange(ChatChannel.Emotes, action, wrappedMessage, source, range, author);
if (!hideLog)
if (name != Name(source))

View File

@@ -1,6 +1,6 @@
using Content.Server.Animals.Systems;
using Content.Server.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Prototypes;
namespace Content.Server.Chemistry.Components;
@@ -19,19 +19,19 @@ public sealed partial class TransformableContainerComponent : Component
/// It will revert to this when emptied.
/// /// It defaults to the description of the parent entity unless overwritten.
/// </summary>
[DataField("initialDescription")]
[DataField]
public string? InitialDescription;
/// <summary>
/// This stores whatever primary reagent is currently in the container.
/// It is used to help determine if a transformation is needed on solution update.
/// </summary>
[DataField("currentReagent")]
public ReagentPrototype? CurrentReagent;
[DataField]
public ProtoId<ReagentPrototype>? CurrentReagent;
/// <summary>
/// This returns whether this container in a transformed or initial state.
/// </summary>
///
[DataField("transformed")]
[DataField]
public bool Transformed;
}

View File

@@ -45,8 +45,8 @@ public sealed class TransformableContainerSystem : EntitySystem
//the biggest reagent in the solution decides the appearance
var reagentId = solution.GetPrimaryReagentId();
//If biggest reagent didn't changed - don't change anything at all
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent.ID == reagentId?.Prototype)
//If biggest reagent didn't change - don't change anything at all
if (entity.Comp.CurrentReagent != null && entity.Comp.CurrentReagent == reagentId?.Prototype)
{
return;
}
@@ -66,7 +66,7 @@ public sealed class TransformableContainerSystem : EntitySystem
private void OnRefreshNameModifiers(Entity<TransformableContainerComponent> entity, ref RefreshNameModifiersEvent args)
{
if (entity.Comp.CurrentReagent is { } currentReagent)
if (_prototypeManager.TryIndex(entity.Comp.CurrentReagent, out var currentReagent))
{
args.AddModifier("transformable-container-component-glass", priority: -1, ("reagent", currentReagent.LocalizedName));
}

View File

@@ -101,7 +101,7 @@ public sealed class CluwneSystem : EntitySystem
else if (_robustRandom.Prob(component.KnockChance))
{
_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);
}
}

View File

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

View File

@@ -1,16 +1,18 @@
using Content.Shared.Damage.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
using Content.Shared.Dataset;
using Robust.Shared.Prototypes;
namespace Content.Server.Damage.Components;
/// <summary>
/// This component shows entity damage severity when it is examined by player.
/// This component shows entity damage severity when it is examined by player.
/// </summary>
[RegisterComponent]
public sealed partial class ExaminableDamageComponent : Component
{
[DataField("messages", required: true, customTypeSerializer:typeof(PrototypeIdSerializer<ExaminableDamagePrototype>))]
public string? MessagesProtoId;
public ExaminableDamagePrototype? MessagesProto;
/// <summary>
/// ID of the <see cref="LocalizedDatasetPrototype"/> containing messages to display a different damage levels.
/// The first message will be used at 0 damage with the others equally distributed across the range from undamaged to fully damaged.
/// </summary>
[DataField]
public ProtoId<LocalizedDatasetPrototype>? Messages;
}

View File

@@ -1,9 +1,6 @@
using System.Linq;
using Content.Server.Damage.Components;
using Content.Server.Damage.Components;
using Content.Server.Destructible;
using Content.Server.Destructible.Thresholds.Triggers;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.Examine;
using Content.Shared.Rounding;
using Robust.Shared.Prototypes;
@@ -12,59 +9,42 @@ namespace Content.Server.Damage.Systems;
public sealed class ExaminableDamageSystem : EntitySystem
{
[Dependency] private readonly DestructibleSystem _destructible = default!;
[Dependency] private readonly IPrototypeManager _prototype = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ExaminableDamageComponent, ComponentInit>(OnInit);
SubscribeLocalEvent<ExaminableDamageComponent, ExaminedEvent>(OnExamine);
}
private void OnInit(EntityUid uid, ExaminableDamageComponent component, ComponentInit args)
private void OnExamine(Entity<ExaminableDamageComponent> ent, ref ExaminedEvent args)
{
if (component.MessagesProtoId == null)
if (!_prototype.TryIndex(ent.Comp.Messages, out var proto) || proto.Values.Count == 0)
return;
component.MessagesProto = _prototype.Index<ExaminableDamagePrototype>(component.MessagesProtoId);
var percent = GetDamagePercent(ent);
var level = ContentHelpers.RoundToNearestLevels(percent, 1, proto.Values.Count - 1);
var msg = Loc.GetString(proto.Values[level]);
args.PushMarkup(msg, -99);
}
private void OnExamine(EntityUid uid, ExaminableDamageComponent component, ExaminedEvent args)
/// <summary>
/// Returns a value between 0 and 1 representing how damaged the entity is,
/// where 0 is undamaged and 1 is fully damaged.
/// </summary>
/// <returns>How damaged the entity is from 0 to 1</returns>
private float GetDamagePercent(Entity<ExaminableDamageComponent> ent)
{
if (component.MessagesProto == null)
return;
var messages = component.MessagesProto.Messages;
if (messages.Length == 0)
return;
var level = GetDamageLevel(uid, component);
var msg = Loc.GetString(messages[level]);
args.PushMarkup(msg,-99);
}
private int GetDamageLevel(EntityUid uid, ExaminableDamageComponent? component = null,
DamageableComponent? damageable = null, DestructibleComponent? destructible = null)
{
if (!Resolve(uid, ref component, ref damageable, ref destructible))
return 0;
if (component.MessagesProto == null)
return 0;
var maxLevels = component.MessagesProto.Messages.Length - 1;
if (maxLevels <= 0)
return 0;
var trigger = (DamageTrigger?) destructible.Thresholds
.LastOrDefault(threshold => threshold.Trigger is DamageTrigger)?.Trigger;
if (trigger == null)
if (!TryComp<DamageableComponent>(ent, out var damageable))
return 0;
var damage = damageable.TotalDamage;
var damageThreshold = trigger.Damage;
var fraction = damageThreshold == 0 ? 0f : (float) damage / damageThreshold;
var damageThreshold = _destructible.DestroyedAt(ent);
var level = ContentHelpers.RoundToNearestLevels(fraction, 1, maxLevels);
return level;
if (damageThreshold == 0)
return 0;
return (damage / damageThreshold).Float();
}
}

View File

@@ -1,62 +0,0 @@
using Content.Server.Body.Systems;
using Content.Shared.Body.Events;
using Content.Shared.Chemistry.Components;
using Content.Shared.Devour;
using Content.Shared.Devour.Components;
using Content.Shared.Whitelist;
namespace Content.Server.Devour;
public sealed class DevourSystem : SharedDevourSystem
{
[Dependency] private readonly BloodstreamSystem _bloodstreamSystem = default!;
[Dependency] private readonly EntityWhitelistSystem _entityWhitelistSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DevourerComponent, DevourDoAfterEvent>(OnDoAfter);
SubscribeLocalEvent<DevourerComponent, BeingGibbedEvent>(OnGibContents);
}
private void OnDoAfter(EntityUid uid, DevourerComponent component, DevourDoAfterEvent args)
{
if (args.Handled || args.Cancelled)
return;
var ichorInjection = new Solution(component.Chemical, component.HealRate);
// Grant ichor if the devoured thing meets the dragon's food preference
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPassOrNull(component.FoodPreferenceWhitelist, (EntityUid)args.Args.Target))
{
_bloodstreamSystem.TryAddToChemicals(uid, ichorInjection);
}
// If the devoured thing meets the stomach whitelist criteria, add it to the stomach
if (args.Args.Target != null && _entityWhitelistSystem.IsWhitelistPass(component.StomachStorageWhitelist, (EntityUid)args.Args.Target))
{
ContainerSystem.Insert(args.Args.Target.Value, component.Stomach);
}
//TODO: Figure out a better way of removing structures via devour that still entails standing still and waiting for a DoAfter. Somehow.
//If it's not alive, it must be a structure.
// Delete if the thing isn't in the stomach storage whitelist (or the stomach whitelist is null/empty)
else if (args.Args.Target != null)
{
QueueDel(args.Args.Target.Value);
}
_audioSystem.PlayPvs(component.SoundDevour, uid);
}
private void OnGibContents(EntityUid uid, DevourerComponent component, ref BeingGibbedEvent args)
{
if (component.StomachStorageWhitelist == null)
return;
// For some reason we have two different systems that should handle gibbing,
// and for some another reason GibbingSystem, which should empty all containers, doesn't get involved in this process
ContainerSystem.EmptyContainer(component.Stomach);
}
}

View File

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

View File

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

View File

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

View File

@@ -58,6 +58,11 @@ public sealed class ProjectileGrenadeSystem : EntitySystem
var grenadeCoord = _transformSystem.GetMapCoordinates(uid);
var shootCount = 0;
var totalCount = component.Container.ContainedEntities.Count + component.UnspawnedCount;
// Just in case
if (totalCount == 0)
return;
var segmentAngle = 360 / totalCount;
while (TrySpawnContents(grenadeCoord, component, out var contentUid))

View File

@@ -338,6 +338,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Ensure we actually have the component
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
// the sprite threshold for a puddle larger than 5 pixels.
var smallPuddleThreshold = FixedPoint2.New(entity.Comp.OverflowVolume.Float() * LowThreshold);
@@ -356,17 +358,21 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
var launchMult = FixedPoint2.Zero;
// A cumulative weighted amount of stun times from slippery reagents
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
if (solution.Volume <= smallPuddleThreshold)
{
_stepTrigger.SetActive(entity, false, comp);
_tile.SetModifier(entity, 1f);
slipComp.SlipData.SlipFriction = 1f;
slipComp.AffectsSliding = false;
Dirty(entity, slipComp);
return;
}
if (!TryComp<SlipperyComponent>(entity, out var slipComp))
return;
slipComp.AffectsSliding = true;
foreach (var (reagent, quantity) in solution.Contents)
{
@@ -386,7 +392,8 @@ public sealed partial class PuddleSystem : SharedPuddleSystem
// Aggregate launch speed based on quantity
launchMult += reagentProto.SlipData.LaunchForwardsMultiplier * 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)
superSlipperyUnits += quantity;
@@ -404,8 +411,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.
if (slipperyUnits > 0)
{
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult / slipperyUnits);
slipComp.SlipData.ParalyzeTime = stunTimer / (float)slipperyUnits;
slipComp.SlipData.LaunchForwardsMultiplier = (float)(launchMult/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

View File

@@ -228,7 +228,7 @@ public sealed class RevolutionaryRuleSystem : GameRuleSystem<RevolutionaryRuleCo
continue;
_npcFaction.RemoveFaction(uid, RevolutionaryNpcFaction);
_stun.TryParalyze(uid, stunTime, true);
_stun.TryUpdateParalyzeDuration(uid, stunTime);
RemCompDeferred<RevolutionaryComponent>(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.");

View File

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

View File

@@ -10,55 +10,46 @@ using Robust.Shared.Network;
namespace Content.Server.Info;
[AdminCommand(AdminFlags.Admin)]
public sealed class ShowRulesCommand : IConsoleCommand
public sealed class ShowRulesCommand : LocalizedCommands
{
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IConfigurationManager _configuration = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly IPlayerManager _player = default!;
public string Command => "showrules";
public string Description => "Opens the rules popup for the specified player.";
public string Help => "showrules <username> [seconds]";
public async void Execute(IConsoleShell shell, string argStr, string[] args)
public override string Command => "showrules";
public override async void Execute(IConsoleShell shell, string argStr, string[] args)
{
string target;
float seconds;
switch (args.Length)
if (args.Length is < 1 or > 2)
{
case 1:
{
target = args[0];
seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
break;
}
case 2:
{
if (!float.TryParse(args[1], out seconds))
{
shell.WriteError($"{args[1]} is not a valid amount of seconds.\n{Help}");
return;
}
target = args[0];
break;
}
default:
{
shell.WriteLine(Help);
return;
}
shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
return;
}
var seconds = _configuration.GetCVar(CCVars.RulesWaitTime);
if (!_player.TryGetSessionByUsername(target, out var player))
if (args.Length == 2 && !float.TryParse(args[1], out seconds))
{
shell.WriteError("Unable to find a player with that name.");
return;
shell.WriteError(Loc.GetString("cmd-showrules-invalid-seconds", ("seconds", args[1])));
return;
}
if (!_player.TryGetSessionByUsername(args[0], out var player))
{
shell.WriteError(Loc.GetString("shell-target-player-does-not-exist"));
return;
}
var coreRules = _configuration.GetCVar(CCVars.RulesFile);
var message = new SendRulesInformationMessage { PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true};
var message = new SendRulesInformationMessage
{ PopupTime = seconds, CoreRules = coreRules, ShouldShowRules = true };
_net.ServerSendMessage(message, player.Channel);
}
public override CompletionResult GetCompletion(IConsoleShell shell, string[] args)
{
return args.Length == 1
? CompletionResult.FromOptions(CompletionHelper.SessionNames(players: _player))
: CompletionResult.Empty;
}
}

View File

@@ -461,7 +461,7 @@ public sealed partial class InstrumentSystem : SharedInstrumentSystem
{
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"),
uid, mob, PopupType.LargeCaution);

View File

@@ -4,6 +4,7 @@ using Content.Server.Materials.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Interaction;
using Content.Shared.Popups;
using Robust.Server.Audio;
namespace Content.Server.Materials;
@@ -13,6 +14,7 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
[Dependency] private readonly AudioSystem _audio = default!;
[Dependency] private readonly MaterialStorageSystem _materialStorage = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainer = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
/// <inheritdoc/>
public override void Initialize()
@@ -39,7 +41,16 @@ public sealed class ProduceMaterialExtractorSystem : EntitySystem
var matAmount = solution.Value.Comp.Solution.Contents
.Where(r => ent.Comp.ExtractionReagents.Contains(r.Reagent.Prototype))
.Sum(r => r.Quantity.Float());
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, (int) matAmount);
var changed = (int)matAmount;
if (changed == 0)
{
_popup.PopupEntity(Loc.GetString("material-extractor-comp-wrongreagent", ("used", args.Used)), args.User, args.User);
return;
}
_materialStorage.TryChangeMaterialAmount(ent, ent.Comp.ExtractedMaterial, changed);
_audio.PlayPvs(ent.Comp.ExtractSound, ent);
QueueDel(args.Used);

View File

@@ -2,16 +2,15 @@ using Content.Server.Body.Systems;
using Content.Server.Fluids.EntitySystems;
using Content.Server.Forensics;
using Content.Server.Popups;
using Content.Server.Stunnable;
using Content.Shared.Body.Components;
using Content.Shared.Body.Systems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.IdentityManagement;
using Content.Shared.Movement.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.StatusEffect;
using Robust.Server.Audio;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
@@ -27,7 +26,7 @@ namespace Content.Server.Medical
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly PuddleSystem _puddle = 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 ForensicsSystem _forensics = 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
var solutionSize = (MathF.Abs(thirstAdded) + MathF.Abs(hungerAdded)) / 6;
// Apply a bit of slowdown
if (TryComp<StatusEffectsComponent>(uid, out var status))
_stun.TrySlowdown(uid, TimeSpan.FromSeconds(solutionSize), true, 0.5f, 0.5f, status);
_movementMod.TryUpdateMovementSpeedModDuration(uid, MovementModStatusSystem.VomitingSlowdown, TimeSpan.FromSeconds(solutionSize), 0.5f);
// TODO: Need decals
var solution = new Solution();

View File

@@ -1,63 +1,30 @@
using Content.Server.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;
namespace Content.Server.Mind.Commands
namespace Content.Server.Mind.Commands;
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeSentientCommand : LocalizedEntityCommands
{
[AdminCommand(AdminFlags.Admin)]
public sealed class MakeSentientCommand : IConsoleCommand
[Dependency] private readonly MindSystem _mindSystem = default!;
public override string Command => "makesentient";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
[Dependency] private readonly IEntityManager _entManager = default!;
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)
{
if (args.Length != 1)
{
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);
shell.WriteLine(Loc.GetString("shell-need-exactly-one-argument"));
return;
}
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);
if (allowMovement)
{
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);
shell.WriteLine(Loc.GetString("shell-could-not-find-entity-with-uid", ("uid", args[0])));
return;
}
_mindSystem.MakeSentient(entId.Value);
}
}

View File

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

View File

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

View File

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

View File

@@ -63,7 +63,7 @@ public sealed class StunProviderSystem : SharedStunProviderSystem
_audio.PlayPvs(comp.Sound, target);
_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
_useDelay.SetLength((uid, useDelay), comp.Cooldown, id: comp.DelayId);

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