Merge branch 'space-wizards:master' into elk-new

This commit is contained in:
Deerstop
2025-04-02 17:12:10 +11:00
committed by GitHub
521 changed files with 4750 additions and 2290 deletions

View File

@@ -1,4 +1,3 @@
using Content.Client.Atmos.UI;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
using Content.Shared.Atmos.Piping.Binary.Components;
@@ -15,7 +14,12 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
private void OnPumpUpdate(Entity<GasPressurePumpComponent> ent, ref AfterAutoHandleStateEvent args)
{
if (UserInterfaceSystem.TryGetOpenUi<GasPressurePumpBoundUserInterface>(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
UpdateUi(ent);
}
protected override void UpdateUi(Entity<GasPressurePumpComponent> ent)
{
if (UserInterfaceSystem.TryGetOpenUi(ent.Owner, GasPressurePumpUiKey.Key, out var bui))
{
bui.Update();
}

View File

@@ -1,8 +1,7 @@
using Content.Shared.Atmos;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.IdentityManagement;
using Content.Shared.Localizations;
using JetBrains.Annotations;
using Robust.Client.UserInterface;
@@ -12,7 +11,7 @@ namespace Content.Client.Atmos.UI;
/// Initializes a <see cref="GasPressurePumpWindow"/> and updates it when new server messages are received.
/// </summary>
[UsedImplicitly]
public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
public sealed class GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
[ViewVariables]
private const float MaxPressure = Atmospherics.MaxOutputPressure;
@@ -20,10 +19,6 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
[ViewVariables]
private GasPressurePumpWindow? _window;
public GasPressurePumpBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
protected override void Open()
{
base.Open();
@@ -35,7 +30,7 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
Update();
}
public void Update()
public override void Update()
{
if (_window == null)
return;
@@ -52,7 +47,9 @@ public sealed class GasPressurePumpBoundUserInterface : BoundUserInterface
private void OnToggleStatusButtonPressed()
{
if (_window is null) return;
if (_window is null)
return;
SendPredictedMessage(new GasPressurePumpToggleStatusMessage(_window.PumpStatus));
}

View File

@@ -1,7 +1,6 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Atmos;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
namespace Content.Client.Atmos.UI

View File

@@ -67,8 +67,10 @@ public sealed partial class CargoSystem
if (!Resolve(uid, ref sprite))
return;
if (!TryComp<AnimationPlayerComponent>(uid, out var player))
return;
_appearance.TryGetData<CargoTelepadState?>(uid, CargoTelepadVisuals.State, out var state);
AnimationPlayerComponent? player = null;
switch (state)
{
@@ -76,7 +78,7 @@ public sealed partial class CargoSystem
if (_player.HasRunningAnimation(uid, TelepadBeamKey))
return;
_player.Stop(uid, player, TelepadIdleKey);
_player.Play(uid, player, CargoTelepadBeamAnimation, TelepadBeamKey);
_player.Play((uid, player), CargoTelepadBeamAnimation, TelepadBeamKey);
break;
case CargoTelepadState.Unpowered:
sprite.LayerSetVisible(CargoTelepadLayers.Beam, false);
@@ -90,7 +92,7 @@ public sealed partial class CargoSystem
_player.HasRunningAnimation(uid, player, TelepadBeamKey))
return;
_player.Play(uid, player, CargoTelepadIdleAnimation, TelepadIdleKey);
_player.Play((uid, player), CargoTelepadIdleAnimation, TelepadIdleKey);
break;
}
}

View File

@@ -1,31 +0,0 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-general'}" TargetLayer="General" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-vocal'}" TargetLayer="Vocal" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Emotes/vocal.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'emote-menu-category-hands'}" TargetLayer="Hands" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"/>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
<!-- General -->
<ui:RadialContainer Name="General" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Vocal -->
<ui:RadialContainer Name="Vocal" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Hands -->
<ui:RadialContainer Name="Hands" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
</ui:RadialMenu>

View File

@@ -1,111 +0,0 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Speech;
using Content.Shared.Whitelist;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
namespace Content.Client.Chat.UI;
[GenerateTypedNameReferences]
public sealed partial class EmotesMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
public event Action<ProtoId<EmotePrototype>>? OnPlayEmote;
public EmotesMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
var spriteSystem = _entManager.System<SpriteSystem>();
var whitelistSystem = _entManager.System<EntityWhitelistSystem>();
var main = FindControl<RadialContainer>("Main");
var emotes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
foreach (var emote in emotes)
{
var player = _playerManager.LocalSession?.AttachedEntity;
if (emote.Category == EmoteCategory.Invalid ||
emote.ChatTriggers.Count == 0 ||
!(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value)) ||
whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
continue;
if (!emote.Available &&
_entManager.TryGetComponent<SpeechComponent>(player.Value, out var speech) &&
!speech.AllowedEmotes.Contains(emote.ID))
continue;
var parent = FindControl<RadialContainer>(emote.Category.ToString());
var button = new EmoteMenuButton
{
SetSize = new Vector2(64f, 64f),
ToolTip = Loc.GetString(emote.Name),
ProtoId = emote.ID,
};
var tex = new TextureRect
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = spriteSystem.Frame0(emote.Icon),
TextureScale = new Vector2(2f, 2f),
};
button.AddChild(tex);
parent.AddChild(button);
foreach (var child in main.Children)
{
if (child is not RadialMenuTextureButton castChild)
continue;
if (castChild.TargetLayer == emote.Category.ToString())
{
castChild.Visible = true;
break;
}
}
}
// Set up menu actions
foreach (var child in Children)
{
if (child is not RadialContainer container)
continue;
AddEmoteClickAction(container);
}
}
private void AddEmoteClickAction(RadialContainer container)
{
foreach (var child in container.Children)
{
if (child is not EmoteMenuButton castChild)
continue;
castChild.OnButtonUp += _ =>
{
OnPlayEmote?.Invoke(castChild.ProtoId);
Close();
};
}
}
}
public sealed class EmoteMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<EmotePrototype> ProtoId { get; set; }
}

View File

@@ -6,7 +6,6 @@ using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
using System.Linq;
@@ -14,6 +13,7 @@ using System.Numerics;
using Content.Shared.FixedPoint;
using Robust.Client.Graphics;
using static Robust.Client.UserInterface.Controls.BoxContainer;
using Robust.Client.GameObjects;
namespace Content.Client.Chemistry.UI
{
@@ -24,6 +24,10 @@ namespace Content.Client.Chemistry.UI
public sealed partial class ChemMasterWindow : FancyWindow
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly SpriteSystem _sprite;
public event Action<BaseButton.ButtonEventArgs, ReagentButton>? OnReagentButtonPressed;
public readonly Button[] PillTypeButtons;
@@ -38,6 +42,8 @@ namespace Content.Client.Chemistry.UI
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_sprite = _entityManager.System<SpriteSystem>();
// Pill type selection buttons, in total there are 20 pills.
// Pill rsi file should have states named as pill1, pill2, and so on.
var resourcePath = new ResPath(PillsRsiPath);
@@ -69,7 +75,7 @@ namespace Content.Client.Chemistry.UI
var specifier = new SpriteSpecifier.Rsi(resourcePath, "pill" + (i + 1));
TextureRect pillTypeTexture = new TextureRect
{
Texture = specifier.Frame0(),
Texture = _sprite.Frame0(specifier),
TextureScale = new Vector2(1.75f, 1.75f),
Stretch = TextureRect.StretchMode.KeepCentered,
};

View File

@@ -1,4 +1,4 @@
using Content.Shared.Chemistry.Components;
using Content.Shared.Chemistry.Components;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
@@ -37,7 +37,7 @@ public sealed class FoamVisualizerSystem : VisualizerSystem<FoamVisualsComponent
if (TryComp(uid, out AnimationPlayerComponent? animPlayer)
&& !AnimationSystem.HasRunningAnimation(uid, animPlayer, FoamVisualsComponent.AnimationKey))
{
AnimationSystem.Play(uid, animPlayer, comp.Animation, FoamVisualsComponent.AnimationKey);
AnimationSystem.Play((uid, animPlayer), comp.Animation, FoamVisualsComponent.AnimationKey);
}
}
}

View File

@@ -1,4 +1,4 @@
using Content.Shared.Vapor;
using Content.Shared.Vapor;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
@@ -41,7 +41,7 @@ public sealed class VaporVisualizerSystem : VisualizerSystem<VaporVisualsCompone
TryComp<AnimationPlayerComponent>(uid, out var animPlayer) &&
!AnimationSystem.HasRunningAnimation(uid, animPlayer, VaporVisualsComponent.AnimationKey))
{
AnimationSystem.Play(uid, animPlayer, comp.VaporFlick, VaporVisualsComponent.AnimationKey);
AnimationSystem.Play((uid, animPlayer), comp.VaporFlick, VaporVisualsComponent.AnimationKey);
}
}

View File

@@ -1,11 +1,7 @@
using Content.Client.ContextMenu.UI;
using Content.Client.Stylesheets;
using Content.Shared.Verbs;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Utility;
using Robust.Shared.Utility;
@@ -27,14 +23,16 @@ public sealed class ExamineButton : ContainerButton
public TextureRect Icon;
public ExamineVerb Verb;
private SpriteSystem _sprite;
public ExamineButton(ExamineVerb verb)
public ExamineButton(ExamineVerb verb, SpriteSystem spriteSystem)
{
Margin = new Thickness(Thickness, Thickness, Thickness, Thickness);
SetOnlyStyleClass(StyleClassExamineButton);
Verb = verb;
_sprite = spriteSystem;
if (verb.Disabled)
{
@@ -61,7 +59,7 @@ public sealed class ExamineButton : ContainerButton
if (verb.Icon != null)
{
Icon.Texture = verb.Icon.Frame0();
Icon.Texture = _sprite.Frame0(verb.Icon);
Icon.Stretch = TextureRect.StretchMode.KeepAspectCentered;
AddChild(Icon);

View File

@@ -30,6 +30,7 @@ namespace Content.Client.Examine
[Dependency] private readonly IPlayerManager _playerManager = default!;
[Dependency] private readonly IEyeManager _eyeManager = default!;
[Dependency] private readonly VerbSystem _verbSystem = default!;
[Dependency] private readonly SpriteSystem _sprite = default!;
public const string StyleClassEntityTooltip = "entity-tooltip";
@@ -332,7 +333,7 @@ namespace Content.Client.Examine
if (!examine.ShowOnExamineTooltip)
continue;
var button = new ExamineButton(examine);
var button = new ExamineButton(examine, _sprite);
if (examine.HoverVerb)
{

View File

@@ -85,7 +85,7 @@ public sealed class RotatingLightSystem : SharedRotatingLightSystem
if (!_animations.HasRunningAnimation(uid, player, AnimKey))
{
_animations.Play(uid, player, GetAnimation(comp.Speed), AnimKey);
_animations.Play((uid, player), GetAnimation(comp.Speed), AnimKey);
}
}
}

View File

@@ -2,7 +2,6 @@ using Content.Shared.Light;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
using Robust.Shared.Animations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Random;
@@ -53,13 +52,14 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
/// </summary>
private void OnAnimationCompleted(EntityUid uid, PoweredLightVisualsComponent comp, AnimationCompletedEvent args)
{
if (!TryComp<AnimationPlayerComponent>(uid, out var animationPlayer))
return;
if (args.Key != PoweredLightVisualsComponent.BlinkingAnimationKey)
return;
if(!comp.IsBlinking)
return;
AnimationSystem.Play(uid, Comp<AnimationPlayerComponent>(uid), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
AnimationSystem.Play((uid, animationPlayer), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
}
/// <summary>
@@ -76,7 +76,7 @@ public sealed class PoweredLightVisualizerSystem : VisualizerSystem<PoweredLight
var animationPlayer = EnsureComp<AnimationPlayerComponent>(uid);
if (shouldBeBlinking)
{
AnimationSystem.Play(uid, animationPlayer, BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
AnimationSystem.Play((uid, animationPlayer), BlinkingAnimation(comp), PoweredLightVisualsComponent.BlinkingAnimationKey);
}
else if (AnimationSystem.HasRunningAnimation(uid, animationPlayer, PoweredLightVisualsComponent.BlinkingAnimationKey))
{

View File

@@ -1,4 +1,4 @@
using System.Linq;
using System.Linq;
using System.Numerics;
using Content.Client.Administration.Managers;
using Content.Client.ContextMenu.UI;
@@ -149,7 +149,7 @@ public sealed class MappingState : GameplayStateBase
{
Deselect();
var coords = args.Coordinates.ToMap(_entityManager, _transform);
var coords = _transform.ToMapCoordinates(args.Coordinates);
if (_verbs.TryGetEntityMenuEntities(coords, out var entities))
_entityMenuController.OpenRootMenu(entities);

View File

@@ -1,7 +1,6 @@
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Systems;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
namespace Content.Client.Movement.Systems;
@@ -10,8 +9,6 @@ namespace Content.Client.Movement.Systems;
/// </summary>
public sealed class ClientSpriteMovementSystem : SharedSpriteMovementSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
private EntityQuery<SpriteComponent> _spriteQuery;
public override void Initialize()

View File

@@ -1,8 +1,6 @@
using System.Numerics;
using Content.Client.Movement.Components;
using Content.Shared.Camera;
using Content.Shared.Inventory;
using Content.Shared.Movement.Systems;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Map;
@@ -16,8 +14,6 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPlayerManager _player = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly SharedContentEyeSystem _contentEye = default!;
[Dependency] private readonly IMapManager _mapManager = default!;
[Dependency] private readonly IClyde _clyde = default!;
// This value is here to make sure the user doesn't have to move their mouse
@@ -42,7 +38,7 @@ public sealed partial class EyeCursorOffsetSystem : EntitySystem
public Vector2? OffsetAfterMouse(EntityUid uid, EyeCursorOffsetComponent? component)
{
var localPlayer = _player.LocalPlayer?.ControlledEntity;
var localPlayer = _player.LocalEntity;
var mousePos = _inputManager.MouseScreenPosition;
var screenSize = _clyde.MainWindow.Size;
var minValue = MathF.Min(screenSize.X / 2, screenSize.Y / 2) * _edgeOffset;

View File

@@ -49,13 +49,17 @@ public sealed class JetpackSystem : SharedJetpackSystem
// TODO: Please don't copy-paste this I beg
// make a generic particle emitter system / actual particles instead.
var query = EntityQueryEnumerator<ActiveJetpackComponent>();
var query = EntityQueryEnumerator<ActiveJetpackComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp))
while (query.MoveNext(out var uid, out var comp, out var xform))
{
if (_timing.CurTime < comp.TargetTime)
continue;
if (_transform.InRange(xform.Coordinates, comp.LastCoordinates, comp.MaxDistance))
{
if (_timing.CurTime < comp.TargetTime)
continue;
}
comp.LastCoordinates = _transform.GetMoverCoordinates(xform.Coordinates);
comp.TargetTime = _timing.CurTime + TimeSpan.FromSeconds(comp.EffectCooldown);
CreateParticles(uid);

View File

@@ -1,47 +0,0 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
xmlns:rcd="clr-namespace:Content.Client.RCD"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Note: The min size of the window just determine how close to the edge of the screen the center of the radial menu can be placed -->
<!-- The radial menu will try to open so that its center is located where the player's cursor is currently -->
<!-- Entry layer (shows main categories) -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-walls-and-flooring'}" TargetLayer="WallsAndFlooring" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/walls_and_flooring.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-windows-and-grilles'}" TargetLayer="WindowsAndGrilles" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/windows_and_grilles.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-airlocks'}" TargetLayer="Airlocks" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/airlocks.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-electrical'}" TargetLayer="Electrical" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/multicoil.png"/>
</ui:RadialMenuTextureButtonWithSector>
<ui:RadialMenuTextureButtonWithSector SetSize="64 64" ToolTip="{Loc 'rcd-component-lighting'}" TargetLayer="Lighting" Visible="False">
<TextureRect VerticalAlignment="Center" HorizontalAlignment="Center" TextureScale="2 2" TexturePath="/Textures/Interface/Radial/RCD/lighting.png"/>
</ui:RadialMenuTextureButtonWithSector>
</ui:RadialContainer>
<!-- Walls and flooring -->
<ui:RadialContainer Name="WallsAndFlooring" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Windows and grilles -->
<ui:RadialContainer Name="WindowsAndGrilles" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Airlocks -->
<ui:RadialContainer Name="Airlocks" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Computer and machine frames -->
<ui:RadialContainer Name="Electrical" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
<!-- Lighting -->
<ui:RadialContainer Name="Lighting" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100"/>
</ui:RadialMenu>

View File

@@ -1,172 +0,0 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Popups;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Player;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
using System.Numerics;
namespace Content.Client.RCD;
[GenerateTypedNameReferences]
public sealed partial class RCDMenu : RadialMenu
{
[Dependency] private readonly EntityManager _entManager = default!;
[Dependency] private readonly IPrototypeManager _protoManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private SharedPopupSystem _popup;
private SpriteSystem _sprites;
public event Action<ProtoId<RCDPrototype>>? SendRCDSystemMessageAction;
private EntityUid _owner;
public RCDMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
_popup = _entManager.System<SharedPopupSystem>();
_sprites = _entManager.System<SpriteSystem>();
OnChildAdded += AddRCDMenuButtonOnClickActions;
}
public void SetEntity(EntityUid uid)
{
_owner = uid;
Refresh();
}
public void Refresh()
{
// Find the main radial container
var main = FindControl<RadialContainer>("Main");
// Populate secondary radial containers
if (!_entManager.TryGetComponent<RCDComponent>(_owner, out var rcd))
return;
foreach (var protoId in rcd.AvailablePrototypes)
{
if (!_protoManager.TryIndex(protoId, out var proto))
continue;
if (proto.Mode == RcdMode.Invalid)
continue;
var parent = FindControl<RadialContainer>(proto.Category);
var tooltip = Loc.GetString(proto.SetName);
if ((proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject) &&
proto.Prototype != null && _protoManager.TryIndex(proto.Prototype, out var entProto, logError: false))
{
tooltip = Loc.GetString(entProto.Name);
}
tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1));
var button = new RCDMenuButton()
{
SetSize = new Vector2(64f, 64f),
ToolTip = tooltip,
ProtoId = protoId,
};
if (proto.Sprite != null)
{
var tex = new TextureRect()
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = _sprites.Frame0(proto.Sprite),
TextureScale = new Vector2(2f, 2f),
};
button.AddChild(tex);
}
parent.AddChild(button);
// Ensure that the button that transitions the menu to the associated category layer
// is visible in the main radial container (as these all start with Visible = false)
foreach (var child in main.Children)
{
if (child is not RadialMenuTextureButton castChild)
continue;
if (castChild.TargetLayer == proto.Category)
{
castChild.Visible = true;
break;
}
}
}
// Set up menu actions
foreach (var child in Children)
{
AddRCDMenuButtonOnClickActions(child);
}
}
private static string OopsConcat(string a, string b)
{
// This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
return a + b;
}
private void AddRCDMenuButtonOnClickActions(Control control)
{
var radialContainer = control as RadialContainer;
if (radialContainer == null)
return;
foreach (var child in radialContainer.Children)
{
var castChild = child as RCDMenuButton;
if (castChild == null)
continue;
castChild.OnButtonUp += _ =>
{
SendRCDSystemMessageAction?.Invoke(castChild.ProtoId);
if (_playerManager.LocalSession?.AttachedEntity != null &&
_protoManager.TryIndex(castChild.ProtoId, out var proto))
{
var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName)));
if (proto.Mode == RcdMode.ConstructTile || proto.Mode == RcdMode.ConstructObject)
{
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
_protoManager.TryIndex(proto.Prototype, out var entProto, logError: false))
name = entProto.Name;
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
// Popup message
_popup.PopupClient(msg, _owner, _playerManager.LocalSession.AttachedEntity);
}
Close();
};
}
}
}
public sealed class RCDMenuButton : RadialMenuTextureButtonWithSector
{
public ProtoId<RCDPrototype> ProtoId { get; set; }
}

View File

@@ -1,20 +1,32 @@
using Content.Client.Popups;
using Content.Client.UserInterface.Controls;
using Content.Shared.RCD;
using Content.Shared.RCD.Components;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.UserInterface;
using Robust.Shared.Player;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.RCD;
[UsedImplicitly]
public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
private static readonly Dictionary<string, (string Tooltip, SpriteSpecifier Sprite)> PrototypesGroupingInfo
= new Dictionary<string, (string Tooltip, SpriteSpecifier Sprite)>
{
["WallsAndFlooring"] = ("rcd-component-walls-and-flooring", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/walls_and_flooring.png"))),
["WindowsAndGrilles"] = ("rcd-component-windows-and-grilles", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/windows_and_grilles.png"))),
["Airlocks"] = ("rcd-component-airlocks", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/airlocks.png"))),
["Electrical"] = ("rcd-component-electrical", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/multicoil.png"))),
["Lighting"] = ("rcd-component-lighting", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Radial/RCD/lighting.png"))),
};
private RCDMenu? _menu;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly ISharedPlayerManager _playerManager = default!;
private SimpleRadialMenu? _menu;
public RCDMenuBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
@@ -25,19 +37,107 @@ public sealed class RCDMenuBoundUserInterface : BoundUserInterface
{
base.Open();
_menu = this.CreateWindow<RCDMenu>();
_menu.SetEntity(Owner);
_menu.SendRCDSystemMessageAction += SendRCDSystemMessage;
if (!EntMan.TryGetComponent<RCDComponent>(Owner, out var rcd))
return;
// Open the menu, centered on the mouse
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
_menu = this.CreateWindow<SimpleRadialMenu>();
_menu.Track(Owner);
var models = ConvertToButtons(rcd.AvailablePrototypes);
_menu.SetButtons(models);
_menu.OpenOverMouseScreenPosition();
}
public void SendRCDSystemMessage(ProtoId<RCDPrototype> protoId)
private IEnumerable<RadialMenuNestedLayerOption> ConvertToButtons(HashSet<ProtoId<RCDPrototype>> prototypes)
{
Dictionary<string, List<RadialMenuActionOption>> buttonsByCategory = new();
foreach (var protoId in prototypes)
{
var prototype = _prototypeManager.Index(protoId);
if (!PrototypesGroupingInfo.TryGetValue(prototype.Category, out var groupInfo))
continue;
if (!buttonsByCategory.TryGetValue(prototype.Category, out var list))
{
list = new List<RadialMenuActionOption>();
buttonsByCategory.Add(prototype.Category, list);
}
var actionOption = new RadialMenuActionOption<RCDPrototype>(HandleMenuOptionClick, prototype)
{
Sprite = prototype.Sprite,
ToolTip = GetTooltip(prototype)
};
list.Add(actionOption);
}
var models = new RadialMenuNestedLayerOption[buttonsByCategory.Count];
var i = 0;
foreach (var (key, list) in buttonsByCategory)
{
var groupInfo = PrototypesGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list)
{
Sprite = groupInfo.Sprite,
ToolTip = Loc.GetString(groupInfo.Tooltip)
};
i++;
}
return models;
}
private void HandleMenuOptionClick(RCDPrototype proto)
{
// A predicted message cannot be used here as the RCD UI is closed immediately
// after this message is sent, which will stop the server from receiving it
SendMessage(new RCDSystemMessage(protoId));
SendMessage(new RCDSystemMessage(proto.ID));
if (_playerManager.LocalSession?.AttachedEntity == null)
return;
var msg = Loc.GetString("rcd-component-change-mode", ("mode", Loc.GetString(proto.SetName)));
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject)
{
var name = Loc.GetString(proto.SetName);
if (proto.Prototype != null &&
_prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
name = entProto.Name;
msg = Loc.GetString("rcd-component-change-build-mode", ("name", name));
}
// Popup message
var popup = EntMan.System<PopupSystem>();
popup.PopupClient(msg, Owner, _playerManager.LocalSession.AttachedEntity);
}
private string GetTooltip(RCDPrototype proto)
{
string tooltip;
if (proto.Mode is RcdMode.ConstructTile or RcdMode.ConstructObject
&& proto.Prototype != null
&& _prototypeManager.TryIndex(proto.Prototype, out var entProto, logError: false))
{
tooltip = Loc.GetString(entProto.Name);
}
else
{
tooltip = Loc.GetString(proto.SetName);
}
tooltip = OopsConcat(char.ToUpper(tooltip[0]).ToString(), tooltip.Remove(0, 1));
return tooltip;
}
private static string OopsConcat(string a, string b)
{
// This exists to prevent Roslyn being clever and compiling something that fails sandbox checks.
return a + b;
}
}

View File

@@ -52,7 +52,7 @@ public sealed class RotationVisualizerSystem : SharedRotationVisualsSystem
// Stop the current rotate animation and then start a new one
if (_animation.HasRunningAnimation(animationComp, animationKey))
{
_animation.Stop(animationComp, animationKey);
_animation.Stop((uid, animationComp), animationKey);
}
var animation = new Animation

View File

@@ -1,28 +1,46 @@
using Content.Client.UserInterface.Controls;
using Content.Shared.Silicons.StationAi;
using Robust.Client.UserInterface;
namespace Content.Client.Silicons.StationAi;
public sealed class StationAiBoundUserInterface : BoundUserInterface
public sealed class StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
private StationAiMenu? _menu;
public StationAiBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
private SimpleRadialMenu? _menu;
protected override void Open()
{
base.Open();
_menu = this.CreateWindow<StationAiMenu>();
_menu.Track(Owner);
_menu.OnAiRadial += args =>
var ev = new GetStationAiRadialEvent();
EntMan.EventBus.RaiseLocalEvent(Owner, ref ev);
_menu = this.CreateWindow<SimpleRadialMenu>();
_menu.Track(Owner);
var buttonModels = ConvertToButtons(ev.Actions);
_menu.SetButtons(buttonModels);
_menu.Open();
}
private IEnumerable<RadialMenuActionOption> ConvertToButtons(IReadOnlyList<StationAiRadial> actions)
{
var models = new RadialMenuActionOption[actions.Count];
for (int i = 0; i < actions.Count; i++)
{
SendPredictedMessage(new StationAiRadialMessage()
var action = actions[i];
models[i] = new RadialMenuActionOption<BaseStationAiAction>(HandleRadialMenuClick, action.Event)
{
Event = args,
});
};
Sprite = action.Sprite,
ToolTip = action.Tooltip
};
}
return models;
}
private void HandleRadialMenuClick(BaseStationAiAction p)
{
SendPredictedMessage(new StationAiRadialMessage { Event = p });
}
}

View File

@@ -1,13 +0,0 @@
<ui:RadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
<!-- Main -->
<ui:RadialContainer Name="Main" VerticalExpand="True" HorizontalExpand="True" InitialRadius="100" ReserveSpaceForHiddenChildren="False">
</ui:RadialContainer>
</ui:RadialMenu>

View File

@@ -1,126 +0,0 @@
using System.Numerics;
using Content.Client.UserInterface.Controls;
using Content.Shared.Silicons.StationAi;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Timing;
namespace Content.Client.Silicons.StationAi;
[GenerateTypedNameReferences]
public sealed partial class StationAiMenu : RadialMenu
{
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
public event Action<BaseStationAiAction>? OnAiRadial;
private EntityUid _tracked;
public StationAiMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
public void Track(EntityUid owner)
{
_tracked = owner;
if (!_entManager.EntityExists(_tracked))
{
Close();
return;
}
BuildButtons();
UpdatePosition();
}
private void BuildButtons()
{
var ev = new GetStationAiRadialEvent();
_entManager.EventBus.RaiseLocalEvent(_tracked, ref ev);
var main = FindControl<RadialContainer>("Main");
main.DisposeAllChildren();
var sprites = _entManager.System<SpriteSystem>();
foreach (var action in ev.Actions)
{
// TODO: This radial boilerplate is quite annoying
var button = new StationAiMenuButton(action.Event)
{
SetSize = new Vector2(64f, 64f),
ToolTip = action.Tooltip != null ? Loc.GetString(action.Tooltip) : null,
};
if (action.Sprite != null)
{
var texture = sprites.Frame0(action.Sprite);
var scale = Vector2.One;
if (texture.Width <= 32)
{
scale *= 2;
}
var tex = new TextureRect
{
VerticalAlignment = VAlignment.Center,
HorizontalAlignment = HAlignment.Center,
Texture = texture,
TextureScale = scale,
};
button.AddChild(tex);
}
button.OnPressed += args =>
{
OnAiRadial?.Invoke(action.Event);
Close();
};
main.AddChild(button);
}
}
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
UpdatePosition();
}
private void UpdatePosition()
{
if (!_entManager.TryGetComponent(_tracked, out TransformComponent? xform))
{
Close();
return;
}
if (!xform.Coordinates.IsValid(_entManager))
{
Close();
return;
}
var coords = _entManager.System<SpriteSystem>().GetSpriteScreenCoordinates((_tracked, null, xform));
if (!coords.IsValid)
{
Close();
return;
}
OpenScreenAt(coords.Position, _clyde);
}
}
public sealed class StationAiMenuButton(BaseStationAiAction action) : RadialMenuTextureButtonWithSector
{
public BaseStationAiAction Action = action;
}

View File

@@ -0,0 +1,5 @@
using Content.Shared.Temperature.Systems;
namespace Content.Client.Temperature.Systems;
public sealed partial class EntityHeaterSystem : SharedEntityHeaterSystem;

View File

@@ -0,0 +1,121 @@
using Content.Client.Power;
using Content.Shared.Turrets;
using Robust.Client.Animations;
using Robust.Client.GameObjects;
namespace Content.Client.Turrets;
public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
{
[Dependency] private readonly AppearanceSystem _appearance = default!;
[Dependency] private readonly AnimationPlayerSystem _animation = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeployableTurretComponent, ComponentInit>(OnComponentInit);
SubscribeLocalEvent<DeployableTurretComponent, AnimationCompletedEvent>(OnAnimationCompleted);
SubscribeLocalEvent<DeployableTurretComponent, AppearanceChangeEvent>(OnAppearanceChange);
}
private void OnComponentInit(Entity<DeployableTurretComponent> ent, ref ComponentInit args)
{
ent.Comp.DeploymentAnimation = new Animation
{
Length = TimeSpan.FromSeconds(ent.Comp.DeploymentLength),
AnimationTracks = {
new AnimationTrackSpriteFlick() {
LayerKey = DeployableTurretVisuals.Turret,
KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.DeployingState, 0f)}
},
}
};
ent.Comp.RetractionAnimation = new Animation
{
Length = TimeSpan.FromSeconds(ent.Comp.RetractionLength),
AnimationTracks = {
new AnimationTrackSpriteFlick() {
LayerKey = DeployableTurretVisuals.Turret,
KeyFrames = {new AnimationTrackSpriteFlick.KeyFrame(ent.Comp.RetractingState, 0f)}
},
}
};
}
private void OnAnimationCompleted(Entity<DeployableTurretComponent> ent, ref AnimationCompletedEvent args)
{
if (args.Key != DeployableTurretComponent.AnimationKey)
return;
if (!TryComp<SpriteComponent>(ent, out var sprite))
return;
if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state))
state = ent.Comp.VisualState;
// Convert to terminal state
var targetState = state & DeployableTurretState.Deployed;
UpdateVisuals(ent, targetState, sprite, args.AnimationPlayer);
}
private void OnAppearanceChange(Entity<DeployableTurretComponent> ent, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
if (!TryComp<AnimationPlayerComponent>(ent, out var animPlayer))
return;
if (!_appearance.TryGetData<DeployableTurretState>(ent, DeployableTurretVisuals.Turret, out var state, args.Component))
state = DeployableTurretState.Retracted;
UpdateVisuals(ent, state, args.Sprite, animPlayer);
}
private void UpdateVisuals(Entity<DeployableTurretComponent> ent, DeployableTurretState state, SpriteComponent sprite, AnimationPlayerComponent? animPlayer = null)
{
if (!Resolve(ent, ref animPlayer))
return;
if (_animation.HasRunningAnimation(ent, animPlayer, DeployableTurretComponent.AnimationKey))
return;
if (state == ent.Comp.VisualState)
return;
var targetState = state & DeployableTurretState.Deployed;
var destinationState = ent.Comp.VisualState & DeployableTurretState.Deployed;
if (targetState != destinationState)
targetState = targetState | DeployableTurretState.Retracting;
ent.Comp.VisualState = state;
// Toggle layer visibility
sprite.LayerSetVisible(DeployableTurretVisuals.Weapon, (targetState & DeployableTurretState.Deployed) > 0);
sprite.LayerSetVisible(PowerDeviceVisualLayers.Powered, HasAmmo(ent) && targetState == DeployableTurretState.Retracted);
// Change the visual state
switch (targetState)
{
case DeployableTurretState.Deploying:
_animation.Play((ent, animPlayer), (Animation)ent.Comp.DeploymentAnimation, DeployableTurretComponent.AnimationKey);
break;
case DeployableTurretState.Retracting:
_animation.Play((ent, animPlayer), (Animation)ent.Comp.RetractionAnimation, DeployableTurretComponent.AnimationKey);
break;
case DeployableTurretState.Deployed:
sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.DeployedState);
break;
case DeployableTurretState.Retracted:
sprite.LayerSetState(DeployableTurretVisuals.Turret, ent.Comp.RetractedState);
break;
}
}
}

View File

@@ -1,10 +1,10 @@
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using System.Linq;
using System.Numerics;
using Content.Shared.Input;
using Robust.Client.Graphics;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.Input;
namespace Content.Client.UserInterface.Controls;
@@ -143,11 +143,8 @@ public class RadialMenu : BaseWindow
return children.First(x => x.Visible);
}
public bool TryToMoveToNewLayer(string newLayer)
public bool TryToMoveToNewLayer(Control newLayer)
{
if (newLayer == string.Empty)
return false;
var currentLayer = GetCurrentActiveLayer();
if (currentLayer == null)
@@ -161,7 +158,7 @@ public class RadialMenu : BaseWindow
continue;
// Hide layers which are not of interest
if (result == true || child.Name != newLayer)
if (result == true || child != newLayer)
{
child.Visible = false;
}
@@ -186,6 +183,19 @@ public class RadialMenu : BaseWindow
return result;
}
public bool TryToMoveToNewLayer(string targetLayerControlName)
{
foreach (var child in Children)
{
if (child.Name == targetLayerControlName && child is RadialContainer)
{
return TryToMoveToNewLayer(child);
}
}
return false;
}
public void ReturnToPreviousLayer()
{
// Close the menu if the traversal path is empty
@@ -296,9 +306,15 @@ public sealed class RadialMenuOuterAreaButton : RadialMenuTextureButtonBase
public class RadialMenuTextureButton : RadialMenuTextureButtonBase
{
/// <summary>
/// Upon clicking this button the radial menu will be moved to the named layer
/// Upon clicking this button the radial menu will be moved to the layer of this control.
/// </summary>
public string TargetLayer { get; set; } = string.Empty;
public Control? TargetLayer { get; set; }
/// <summary>
/// Other way to set navigation to other container, as <see cref="TargetLayer"/>,
/// but using <see cref="Control.Name"/> property of target <see cref="RadialContainer"/>.
/// </summary>
public string? TargetLayerControlName { get; set; }
/// <summary>
/// A simple texture button that can move the user to a different layer within a radial menu
@@ -311,7 +327,7 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
private void OnClicked(ButtonEventArgs args)
{
if (TargetLayer == string.Empty)
if (TargetLayer == null && TargetLayerControlName == null)
return;
var parent = FindParentMultiLayerContainer(this);
@@ -319,7 +335,14 @@ public class RadialMenuTextureButton : RadialMenuTextureButtonBase
if (parent == null)
return;
parent.TryToMoveToNewLayer(TargetLayer);
if (TargetLayer != null)
{
parent.TryToMoveToNewLayer(TargetLayer);
}
else
{
parent.TryToMoveToNewLayer(TargetLayerControlName!);
}
}
private RadialMenu? FindParentMultiLayerContainer(Control control)
@@ -387,7 +410,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
private Color _hoverBorderColorSrgb = Color.ToSrgb(new Color(87, 91, 127, 128));
/// <summary>
/// Marker, that control should render border of segment. Is false by default.
/// Marker, that controls if border of segment should be rendered. Is false by default.
/// </summary>
/// <remarks>
/// By default color of border is same as color of background. Use <see cref="BorderColor"/>
@@ -400,13 +423,6 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
/// </summary>
public bool DrawBackground { get; set; } = true;
/// <summary>
/// Marker, that control should render separator lines.
/// Separator lines are used to visually separate sector of radial menu items.
/// Is true by default
/// </summary>
public bool DrawSeparators { get; set; } = true;
/// <summary>
/// Color of background in non-hovered state. Accepts RGB color, works with sRGB for DrawPrimitive internally.
/// </summary>
@@ -520,7 +536,7 @@ public class RadialMenuTextureButtonWithSector : RadialMenuTextureButton, IRadia
DrawAnnulusSector(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, borderColor, false);
}
if (!_isWholeCircle && DrawSeparators)
if (!_isWholeCircle && DrawBorder)
{
DrawSeparatorLines(handle, containerCenter, _innerRadius * UIScale, _outerRadius * UIScale, angleFrom, angleTo, SeparatorColor);
}

View File

@@ -0,0 +1,8 @@
<ui:SimpleRadialMenu xmlns="https://spacestation14.io"
xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
BackButtonStyleClass="RadialMenuBackButton"
CloseButtonStyleClass="RadialMenuCloseButton"
VerticalExpand="True"
HorizontalExpand="True"
MinSize="450 450">
</ui:SimpleRadialMenu>

View File

@@ -0,0 +1,279 @@
using Robust.Client.UserInterface;
using System.Numerics;
using Robust.Client.AutoGenerated;
using Robust.Client.Graphics;
using Robust.Shared.Utility;
using Robust.Client.GameObjects;
using Robust.Shared.Timing;
using Robust.Client.UserInterface.XAML;
using Robust.Client.Input;
namespace Content.Client.UserInterface.Controls;
[GenerateTypedNameReferences]
public partial class SimpleRadialMenu : RadialMenu
{
private EntityUid? _attachMenuToEntity;
[Dependency] private readonly IClyde _clyde = default!;
[Dependency] private readonly IEntityManager _entManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
public SimpleRadialMenu()
{
IoCManager.InjectDependencies(this);
RobustXamlLoader.Load(this);
}
public void Track(EntityUid owner)
{
_attachMenuToEntity = owner;
}
public void SetButtons(IEnumerable<RadialMenuOption> models, SimpleRadialMenuSettings? settings = null)
{
ClearExistingChildrenRadialButtons();
var sprites = _entManager.System<SpriteSystem>();
Fill(models, sprites, Children, settings ?? new SimpleRadialMenuSettings());
}
public void OpenOverMouseScreenPosition()
{
var vpSize = _clyde.ScreenSize;
OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
}
private void Fill(
IEnumerable<RadialMenuOption> models,
SpriteSystem sprites,
ICollection<Control> rootControlChildren,
SimpleRadialMenuSettings settings
)
{
var rootContainer = new RadialContainer
{
HorizontalExpand = true,
VerticalExpand = true,
InitialRadius = settings.DefaultContainerRadius,
ReserveSpaceForHiddenChildren = false,
Visible = true
};
rootControlChildren.Add(rootContainer);
foreach (var model in models)
{
if (model is RadialMenuNestedLayerOption nestedMenuModel)
{
var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings);
linkButton.Visible = true;
rootContainer.AddChild(linkButton);
}
else
{
var rootButtons = ConvertToButton(model, sprites, settings, false);
rootContainer.AddChild(rootButtons);
}
}
}
private RadialMenuTextureButton RecursiveContainerExtraction(
SpriteSystem sprites,
ICollection<Control> rootControlChildren,
RadialMenuNestedLayerOption model,
SimpleRadialMenuSettings settings
)
{
var container = new RadialContainer
{
HorizontalExpand = true,
VerticalExpand = true,
InitialRadius = model.ContainerRadius!.Value,
ReserveSpaceForHiddenChildren = false,
Visible = false
};
foreach (var nested in model.Nested)
{
if (nested is RadialMenuNestedLayerOption nestedMenuModel)
{
var linkButton = RecursiveContainerExtraction(sprites, rootControlChildren, nestedMenuModel, settings);
container.AddChild(linkButton);
}
else
{
var button = ConvertToButton(nested, sprites, settings, false);
container.AddChild(button);
}
}
rootControlChildren.Add(container);
var thisLayerLinkButton = ConvertToButton(model, sprites, settings, true);
thisLayerLinkButton.TargetLayer = container;
return thisLayerLinkButton;
}
private RadialMenuTextureButton ConvertToButton(
RadialMenuOption model,
SpriteSystem sprites,
SimpleRadialMenuSettings settings,
bool haveNested
)
{
var button = settings.UseSectors
? ConvertToButtonWithSector(model, settings)
: new RadialMenuTextureButton();
button.SetSize = new Vector2(64f, 64f);
button.ToolTip = model.ToolTip;
if (model.Sprite != null)
{
var scale = Vector2.One;
var texture = sprites.Frame0(model.Sprite);
if (texture.Width <= 32)
{
scale *= 2;
}
button.TextureNormal = texture;
button.Scale = scale;
}
if (model is RadialMenuActionOption actionOption)
{
button.OnPressed += _ =>
{
actionOption.OnPressed?.Invoke();
if(!haveNested)
Close();
};
}
return button;
}
private static RadialMenuTextureButtonWithSector ConvertToButtonWithSector(RadialMenuOption model, SimpleRadialMenuSettings settings)
{
var button = new RadialMenuTextureButtonWithSector
{
DrawBorder = settings.DisplayBorders,
DrawBackground = !settings.NoBackground
};
if (model.BackgroundColor.HasValue)
{
button.BackgroundColor = model.BackgroundColor.Value;
}
if (model.HoverBackgroundColor.HasValue)
{
button.HoverBackgroundColor = model.HoverBackgroundColor.Value;
}
return button;
}
private void ClearExistingChildrenRadialButtons()
{
var toRemove = new List<Control>(ChildCount);
foreach (var child in Children)
{
if (child != ContextualButton && child != MenuOuterAreaButton)
{
toRemove.Add(child);
}
}
foreach (var control in toRemove)
{
Children.Remove(control);
}
}
#region target entity tracking
protected override void FrameUpdate(FrameEventArgs args)
{
base.FrameUpdate(args);
if (_attachMenuToEntity != null)
{
UpdatePosition();
}
}
private void UpdatePosition()
{
if (!_entManager.TryGetComponent(_attachMenuToEntity, out TransformComponent? xform))
{
Close();
return;
}
if (!xform.Coordinates.IsValid(_entManager))
{
Close();
return;
}
var coords = _entManager.System<SpriteSystem>().GetSpriteScreenCoordinates((_attachMenuToEntity.Value, null, xform));
if (!coords.IsValid)
{
Close();
return;
}
OpenScreenAt(coords.Position, _clyde);
}
#endregion
}
public abstract class RadialMenuOption
{
public string? ToolTip { get; init; }
public SpriteSpecifier? Sprite { get; init; }
public Color? BackgroundColor { get; set; }
public Color? HoverBackgroundColor { get; set; }
}
public class RadialMenuActionOption(Action onPressed) : RadialMenuOption
{
public Action OnPressed { get; } = onPressed;
}
public class RadialMenuActionOption<T>(Action<T> onPressed, T data)
: RadialMenuActionOption(onPressed: () => onPressed(data));
public class RadialMenuNestedLayerOption(IReadOnlyCollection<RadialMenuOption> nested, float containerRadius = 100)
: RadialMenuOption
{
public float? ContainerRadius { get; } = containerRadius;
public IReadOnlyCollection<RadialMenuOption> Nested { get; } = nested;
}
public class SimpleRadialMenuSettings
{
/// <summary>
/// Default container draw radius. Is going to be further affected by per sector increment.
/// </summary>
public int DefaultContainerRadius = 100;
/// <summary>
/// Marker, if sector-buttons should be used.
/// </summary>
public bool UseSectors = true;
/// <summary>
/// Marker, if border of buttons should be rendered. Can only be used when <see cref="UseSectors"/> = true.
/// </summary>
public bool DisplayBorders = true;
/// <summary>
/// Marker, if sector background should not be rendered. Can only be used when <see cref="UseSectors"/> = true.
/// </summary>
public bool NoBackground = false;
}

View File

@@ -1,16 +1,17 @@
using Content.Client.Chat.UI;
using Content.Client.Gameplay;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chat;
using Content.Shared.Chat.Prototypes;
using Content.Shared.Input;
using Content.Shared.Speech;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Client.Player;
using Robust.Client.UserInterface.Controllers;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Input.Binding;
using Robust.Shared.Prototypes;
using Robust.Shared.Utility;
namespace Content.Client.UserInterface.Systems.Emotes;
@@ -18,11 +19,19 @@ namespace Content.Client.UserInterface.Systems.Emotes;
public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayState>
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IClyde _displayManager = default!;
[Dependency] private readonly IInputManager _inputManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IPlayerManager _playerManager = default!;
private MenuButton? EmotesButton => UIManager.GetActiveUIWidgetOrNull<MenuBar.Widgets.GameTopMenuBar>()?.EmotesButton;
private EmotesMenu? _menu;
private SimpleRadialMenu? _menu;
private static readonly Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)> EmoteGroupingInfo
= new Dictionary<EmoteCategory, (string Tooltip, SpriteSpecifier Sprite)>
{
[EmoteCategory.General] = ("emote-menu-category-general", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Head/Soft/mimesoft.rsi/icon.png"))),
[EmoteCategory.Hands] = ("emote-menu-category-hands", new SpriteSpecifier.Texture(new ResPath("/Textures/Clothing/Hands/Gloves/latex.rsi/icon.png"))),
[EmoteCategory.Vocal] = ("emote-menu-category-vocal", new SpriteSpecifier.Texture(new ResPath("/Textures/Interface/Emotes/vocal.png"))),
};
public void OnStateEntered(GameplayState state)
{
@@ -42,10 +51,16 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
if (_menu == null)
{
// setup window
_menu = UIManager.CreateWindow<EmotesMenu>();
var prototypes = _prototypeManager.EnumeratePrototypes<EmotePrototype>();
var models = ConvertToButtons(prototypes);
_menu = new SimpleRadialMenu();
_menu.SetButtons(models);
_menu.Open();
_menu.OnClose += OnWindowClosed;
_menu.OnOpen += OnWindowOpen;
_menu.OnPlayEmote += OnPlayEmote;
if (EmotesButton != null)
EmotesButton.SetClickPressed(true);
@@ -56,16 +71,13 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
}
else
{
// Open the menu, centered on the mouse
var vpSize = _displayManager.ScreenSize;
_menu.OpenCenteredAt(_inputManager.MouseScreenPosition.Position / vpSize);
_menu.OpenOverMouseScreenPosition();
}
}
else
{
_menu.OnClose -= OnWindowClosed;
_menu.OnOpen -= OnWindowOpen;
_menu.OnPlayEmote -= OnPlayEmote;
if (EmotesButton != null)
EmotesButton.SetClickPressed(false);
@@ -118,8 +130,62 @@ public sealed class EmotesUIController : UIController, IOnStateChanged<GameplayS
_menu = null;
}
private void OnPlayEmote(ProtoId<EmotePrototype> protoId)
private IEnumerable<RadialMenuOption> ConvertToButtons(IEnumerable<EmotePrototype> emotePrototypes)
{
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(protoId));
var whitelistSystem = EntitySystemManager.GetEntitySystem<EntityWhitelistSystem>();
var player = _playerManager.LocalSession?.AttachedEntity;
Dictionary<EmoteCategory, List<RadialMenuOption>> emotesByCategory = new();
foreach (var emote in emotePrototypes)
{
if(emote.Category == EmoteCategory.Invalid)
continue;
// only valid emotes that have ways to be triggered by chat and player have access / no restriction on
if (emote.Category == EmoteCategory.Invalid
|| emote.ChatTriggers.Count == 0
|| !(player.HasValue && whitelistSystem.IsWhitelistPassOrNull(emote.Whitelist, player.Value))
|| whitelistSystem.IsBlacklistPass(emote.Blacklist, player.Value))
continue;
if (!emote.Available
&& EntityManager.TryGetComponent<SpeechComponent>(player.Value, out var speech)
&& !speech.AllowedEmotes.Contains(emote.ID))
continue;
if (!emotesByCategory.TryGetValue(emote.Category, out var list))
{
list = new List<RadialMenuOption>();
emotesByCategory.Add(emote.Category, list);
}
var actionOption = new RadialMenuActionOption<EmotePrototype>(HandleRadialButtonClick, emote)
{
Sprite = emote.Icon,
ToolTip = Loc.GetString(emote.Name)
};
list.Add(actionOption);
}
var models = new RadialMenuOption[emotesByCategory.Count];
var i = 0;
foreach (var (key, list) in emotesByCategory)
{
var tuple = EmoteGroupingInfo[key];
models[i] = new RadialMenuNestedLayerOption(list)
{
Sprite = tuple.Sprite,
ToolTip = Loc.GetString(tuple.Tooltip)
};
i++;
}
return models;
}
private void HandleRadialButtonClick(EmotePrototype prototype)
{
_entityManager.RaisePredictiveEvent(new PlayEmoteMessage(prototype.ID));
}
}

View File

@@ -42,6 +42,9 @@ public sealed class StorageWindow : BaseWindow
private ValueList<EntityUid> _contained = new();
private ValueList<EntityUid> _toRemove = new();
// Manually store this because you can't have a 0x0 GridContainer but we still need to add child controls for 1x1 containers.
private Vector2i _pieceGridSize;
private TextureButton? _backButton;
private bool _isDirty;
@@ -408,11 +411,14 @@ public sealed class StorageWindow : BaseWindow
_contained.Clear();
_contained.AddRange(storageComp.Container.ContainedEntities.Reverse());
var width = boundingGrid.Width + 1;
var height = boundingGrid.Height + 1;
// Build the grid representation
if (_pieceGrid.Rows - 1 != boundingGrid.Height || _pieceGrid.Columns - 1 != boundingGrid.Width)
if (_pieceGrid.Rows != _pieceGridSize.Y || _pieceGrid.Columns != _pieceGridSize.X)
{
_pieceGrid.Rows = boundingGrid.Height + 1;
_pieceGrid.Columns = boundingGrid.Width + 1;
_pieceGrid.Rows = height;
_pieceGrid.Columns = width;
_controlGrid.Clear();
for (var y = boundingGrid.Bottom; y <= boundingGrid.Top; y++)
@@ -430,6 +436,7 @@ public sealed class StorageWindow : BaseWindow
}
}
_pieceGridSize = new(width, height);
_toRemove.Clear();
// Remove entities no longer relevant / Update existing ones

View File

@@ -12,35 +12,6 @@ namespace Content.IntegrationTests.Tests.Access
[TestOf(typeof(AccessReaderComponent))]
public sealed class AccessReaderTest
{
[Test]
public async Task TestProtoTags()
{
await using var pair = await PoolManager.GetServerClient();
var server = pair.Server;
var protoManager = server.ResolveDependency<IPrototypeManager>();
var accessName = server.ResolveDependency<IComponentFactory>().GetComponentName(typeof(AccessReaderComponent));
await server.WaitAssertion(() =>
{
foreach (var ent in protoManager.EnumeratePrototypes<EntityPrototype>())
{
if (!ent.Components.TryGetComponent(accessName, out var access))
continue;
var reader = (AccessReaderComponent) access;
var allTags = reader.AccessLists.SelectMany(c => c).Union(reader.DenyTags);
foreach (var level in allTags)
{
Assert.That(protoManager.HasIndex<AccessLevelPrototype>(level), $"Invalid access level: {level} found on {ent}");
}
}
});
await pair.CleanReturnAsync();
}
[Test]
public async Task TestTags()
{

View File

@@ -1,5 +1,6 @@
using Content.IntegrationTests.Tests.Interaction;
using Content.Shared.Projectiles;
using Robust.Shared.GameObjects;
using Robust.Shared.Network;
namespace Content.IntegrationTests.Tests.Embedding;
@@ -88,4 +89,84 @@ public sealed class EmbedTest : InteractionTest
AssertExists(projectile);
await AssertEntityLookup(EmbeddableProtoId);
}
/// <summary>
/// Throws two embeddable projectiles at a target, then deletes them
/// one at a time, making sure that they are tracked correctly and that
/// the <see cref="EmbeddedContainerComponent"/> is removed once all
/// projectiles are gone.
/// </summary>
[Test]
public async Task TestDeleteWhileEmbedded()
{
// Spawn the target we're going to throw at
await SpawnTarget(TargetProtoId);
// Give the player the embeddable to throw
var projectile1 = await PlaceInHands(EmbeddableProtoId);
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile1, out var embedComp),
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent.");
// Make sure the projectile isn't already embedded into anything
Assert.That(embedComp.EmbeddedIntoUid, Is.Null,
$"Projectile already embedded into {SEntMan.ToPrettyString(embedComp.EmbeddedIntoUid)}.");
// Have the player throw the embeddable at the target
await ThrowItem();
// Give the player a second embeddable to throw
var projectile2 = await PlaceInHands(EmbeddableProtoId);
Assert.That(TryComp<EmbeddableProjectileComponent>(projectile1, out var embedComp2),
$"{EmbeddableProtoId} does not have EmbeddableProjectileComponent.");
// Wait a moment for the projectile to hit and embed
await RunSeconds(0.5f);
// Make sure the projectile is embedded into the target
Assert.That(embedComp.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
"First projectile not embedded into target.");
Assert.That(TryComp<EmbeddedContainerComponent>(out var containerComp),
"Target was not given EmbeddedContainerComponent.");
Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)),
"Target is not tracking the first projectile as embedded.");
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1),
"Target has unexpected EmbeddedObjects count.");
// Wait for the cooldown between throws
await RunSeconds(Hands.ThrowCooldown.Seconds);
// Throw the second projectile
await ThrowItem();
// Wait a moment for the second projectile to hit and embed
await RunSeconds(0.5f);
Assert.That(embedComp2.EmbeddedIntoUid, Is.EqualTo(ToServer(Target)),
"Second projectile not embedded into target");
AssertComp<EmbeddedContainerComponent>();
Assert.That(containerComp.EmbeddedObjects, Does.Contain(ToServer(projectile1)),
"Target is not tracking the second projectile as embedded.");
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(2),
"Target EmbeddedObjects count did not increase with second projectile.");
// Delete the first projectile
await Delete(projectile1);
Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(ToServer(projectile1)),
"Target did not stop tracking first projectile after it was deleted.");
Assert.That(containerComp.EmbeddedObjects, Does.Not.Contain(EntityUid.Invalid),
"Target EmbeddedObjects contains an invalid entity.");
foreach (var embedded in containerComp.EmbeddedObjects)
{
Assert.That(!SEntMan.Deleted(embedded),
"Target EmbeddedObjects contains a deleted entity.");
}
Assert.That(containerComp.EmbeddedObjects, Has.Count.EqualTo(1),
"Target EmbeddedObjects count did not decrease after deleting first projectile.");
// Delete the second projectile
await Delete(projectile2);
Assert.That(!SEntMan.HasComponent<EmbeddedContainerComponent>(ToServer(Target)),
"Target did not remove EmbeddedContainerComponent after both projectiles were deleted.");
}
}

View File

@@ -1,5 +1,4 @@
using System.Linq;
using Content.Client.Chat.UI;
using Content.Client.LateJoin;
using Robust.Client.UserInterface.CustomControls;
using Robust.Shared.ContentPack;
@@ -14,7 +13,6 @@ public sealed class UiControlTest
// You should not be adding to this.
private Type[] _ignored = new Type[]
{
typeof(EmotesMenu),
typeof(LateJoinGui),
};

View File

@@ -1,9 +1,11 @@
using Content.Server.Administration.Commands;
using Content.Server.Antag;
using Content.Server.GameTicking;
using Content.Server.GameTicking.Rules.Components;
using Content.Server.Zombies;
using Content.Shared.Administration;
using Content.Shared.Database;
using Content.Shared.Humanoid;
using Content.Shared.Mind.Components;
using Content.Shared.Roles;
using Content.Shared.Verbs;
@@ -17,6 +19,7 @@ public sealed partial class AdminVerbSystem
{
[Dependency] private readonly AntagSelectionSystem _antag = default!;
[Dependency] private readonly ZombieSystem _zombie = default!;
[Dependency] private readonly GameTicker _gameTicker = default!;
[ValidatePrototypeId<EntityPrototype>]
private const string DefaultTraitorRule = "Traitor";
@@ -36,6 +39,8 @@ public sealed partial class AdminVerbSystem
[ValidatePrototypeId<StartingGearPrototype>]
private const string PirateGearId = "PirateGear";
private readonly EntProtoId _paradoxCloneRuleId = "ParadoxCloneSpawn";
// All antag verbs have names so invokeverb works.
private void AddAntagVerbs(GetVerbsEvent<Verb> args)
{
@@ -157,5 +162,29 @@ public sealed partial class AdminVerbSystem
Message = string.Join(": ", thiefName, Loc.GetString("admin-verb-make-thief")),
};
args.Verbs.Add(thief);
var paradoxCloneName = Loc.GetString("admin-verb-text-make-paradox-clone");
Verb paradox = new()
{
Text = paradoxCloneName,
Category = VerbCategory.Antag,
Icon = new SpriteSpecifier.Rsi(new("/Textures/Interface/Misc/job_icons.rsi"), "ParadoxClone"),
Act = () =>
{
var ruleEnt = _gameTicker.AddGameRule(_paradoxCloneRuleId);
if (!TryComp<ParadoxCloneRuleComponent>(ruleEnt, out var paradoxCloneRuleComp))
return;
paradoxCloneRuleComp.OriginalBody = args.Target; // override the target player
_gameTicker.StartGameRule(ruleEnt);
},
Impact = LogImpact.High,
Message = string.Join(": ", paradoxCloneName, Loc.GetString("admin-verb-make-paradox-clone")),
};
if (HasComp<HumanoidAppearanceComponent>(args.Target)) // only humanoids can be cloned
args.Verbs.Add(paradox);
}
}

View File

@@ -81,14 +81,14 @@ public sealed class ProjectileAnomalySystem : EntitySystem
EntityCoordinates targetCoords,
float severity)
{
var mapPos = coords.ToMap(EntityManager, _xform);
var mapPos = _xform.ToMapCoordinates(coords);
var spawnCoords = _mapManager.TryFindGridAt(mapPos, out var gridUid, out _)
? coords.WithEntityId(gridUid, EntityManager)
? _xform.WithEntityId(coords, gridUid)
: new(_mapManager.GetMapEntityId(mapPos.MapId), mapPos.Position);
var ent = Spawn(component.ProjectilePrototype, spawnCoords);
var direction = targetCoords.ToMapPos(EntityManager, _xform) - mapPos.Position;
var direction = _xform.ToMapCoordinates(targetCoords).Position - mapPos.Position;
if (!TryComp<ProjectileComponent>(ent, out var comp))
return;

View File

@@ -16,7 +16,6 @@ public sealed class TechAnomalySystem : EntitySystem
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly BeamSystem _beam = default!;
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly EmagSystem _emag = default!;
public override void Initialize()
{

View File

@@ -3,6 +3,7 @@ using Content.Server.Atmos.Piping.Components;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Server.Power.Components;
using Content.Server.Power.EntitySystems;
using Content.Shared.Atmos;
using Content.Shared.Atmos.Components;
using Content.Shared.Atmos.EntitySystems;
@@ -17,6 +18,7 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
[Dependency] private readonly AtmosphereSystem _atmosphereSystem = default!;
[Dependency] private readonly SharedAmbientSoundSystem _ambientSoundSystem = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
[Dependency] private readonly PowerReceiverSystem _power = default!;
public override void Initialize()
{
@@ -25,33 +27,33 @@ public sealed class GasPressurePumpSystem : SharedGasPressurePumpSystem
SubscribeLocalEvent<GasPressurePumpComponent, AtmosDeviceUpdateEvent>(OnPumpUpdated);
}
private void OnPumpUpdated(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceUpdateEvent args)
private void OnPumpUpdated(Entity<GasPressurePumpComponent> ent, ref AtmosDeviceUpdateEvent args)
{
if (!pump.Enabled
|| (TryComp<ApcPowerReceiverComponent>(uid, out var power) && !power.Powered)
|| !_nodeContainer.TryGetNodes(uid, pump.InletName, pump.OutletName, out PipeNode? inlet, out PipeNode? outlet))
if (!ent.Comp.Enabled
|| !_power.IsPowered(ent)
|| !_nodeContainer.TryGetNodes(ent.Owner, ent.Comp.InletName, ent.Comp.OutletName, out PipeNode? inlet, out PipeNode? outlet))
{
_ambientSoundSystem.SetAmbience(uid, false);
_ambientSoundSystem.SetAmbience(ent, false);
return;
}
var outputStartingPressure = outlet.Air.Pressure;
if (outputStartingPressure >= pump.TargetPressure)
if (outputStartingPressure >= ent.Comp.TargetPressure)
{
_ambientSoundSystem.SetAmbience(uid, false);
_ambientSoundSystem.SetAmbience(ent, false);
return; // No need to pump gas if target has been reached.
}
if (inlet.Air.TotalMoles > 0 && inlet.Air.Temperature > 0)
{
// We calculate the necessary moles to transfer using our good ol' friend PV=nRT.
var pressureDelta = pump.TargetPressure - outputStartingPressure;
var pressureDelta = ent.Comp.TargetPressure - outputStartingPressure;
var transferMoles = (pressureDelta * outlet.Air.Volume) / (inlet.Air.Temperature * Atmospherics.R);
var removed = inlet.Air.Remove(transferMoles);
_atmosphereSystem.Merge(outlet.Air, removed);
_ambientSoundSystem.SetAmbience(uid, removed.TotalMoles > 0f);
_ambientSoundSystem.SetAmbience(ent, removed.TotalMoles > 0f);
}
}
}

View File

@@ -1,10 +1,8 @@
using System.Diagnostics.CodeAnalysis;
using Content.Server.Atmos.Piping.Binary.Components;
using Content.Server.Atmos.Piping.Unary.Components;
using Content.Server.NodeContainer;
using Content.Server.NodeContainer.EntitySystems;
using Content.Server.NodeContainer.Nodes;
using Content.Shared.Atmos.Piping.Unary.Components;
using Content.Shared.Construction.Components;
using JetBrains.Annotations;
using Robust.Shared.Map;
@@ -16,7 +14,6 @@ namespace Content.Server.Atmos.Piping.Unary.EntitySystems
public sealed class GasPortableSystem : EntitySystem
{
[Dependency] private readonly SharedMapSystem _mapSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly NodeContainerSystem _nodeContainer = default!;
public override void Initialize()

View File

@@ -246,7 +246,7 @@ internal sealed partial class ChatManager : IChatManager
Color? colorOverride = null;
var wrappedMessage = Loc.GetString("chat-manager-send-ooc-wrap-message", ("playerName",player.Name), ("message", FormattedMessage.EscapeText(message)));
if (_adminManager.HasAdminFlag(player, AdminFlags.Admin))
if (_adminManager.HasAdminFlag(player, AdminFlags.NameColor))
{
var prefs = _preferencesManager.GetPreferences(player.UserId);
colorOverride = prefs.AdminOOCColor;

View File

@@ -9,8 +9,17 @@ namespace Content.Server.Destructible
[RegisterComponent]
public sealed partial class DestructibleComponent : Component
{
[DataField("thresholds")]
/// <summary>
/// A list of damage thresholds for the entity;
/// includes their triggers and resultant behaviors
/// </summary>
[DataField]
public List<DamageThreshold> Thresholds = new();
/// <summary>
/// Specifies whether the entity has passed a damage threshold that causes it to break
/// </summary>
[DataField]
public bool IsBroken = false;
}
}

View File

@@ -57,6 +57,8 @@ namespace Content.Server.Destructible
/// </summary>
public void Execute(EntityUid uid, DestructibleComponent component, DamageChangedEvent args)
{
component.IsBroken = false;
foreach (var threshold in component.Thresholds)
{
if (threshold.Reached(args.Damageable, this))
@@ -96,6 +98,12 @@ namespace Content.Server.Destructible
threshold.Execute(uid, this, EntityManager, args.Origin);
}
if (threshold.OldTriggered)
{
component.IsBroken |= threshold.Behaviors.Any(b => b is DoActsBehavior doActsBehavior &&
(doActsBehavior.HasAct(ThresholdActs.Breakage) || doActsBehavior.HasAct(ThresholdActs.Destruction)));
}
// if destruction behavior (or some other deletion effect) occurred, don't run other triggers.
if (EntityManager.IsQueuedForDeletion(uid) || Deleted(uid))
return;

View File

@@ -33,7 +33,7 @@ namespace Content.Server.GameTicking
if (args.NewStatus != SessionStatus.Disconnected)
{
mind.Session = session;
_pvsOverride.AddSessionOverride(GetNetEntity(mindId.Value), session);
_pvsOverride.AddSessionOverride(mindId.Value, session);
}
DebugTools.Assert(mind.Session == session);

View File

@@ -196,7 +196,7 @@ namespace Content.Server.GameTicking
if (ev.GameMap.IsGrid)
{
var mapUid = _map.CreateMap(out mapId);
var mapUid = _map.CreateMap(out mapId, runMapInit: options?.InitializeMaps ?? false);
if (!_loader.TryLoadGrid(mapId,
ev.GameMap.MapPath,
out var grid,
@@ -557,7 +557,7 @@ namespace Content.Server.GameTicking
if (TryGetEntity(mind.OriginalOwnedEntity, out var entity) && pvsOverride)
{
_pvsOverride.AddGlobalOverride(GetNetEntity(entity.Value), recursive: true);
_pvsOverride.AddGlobalOverride(entity.Value);
}
var roles = _roles.MindGetAllRoleInfo(mindId);

View File

@@ -427,7 +427,7 @@ namespace Content.Server.GameTicking
// Ideally engine would just spawn them on grid directly I guess? Right now grid traversal is handling it during
// update which means we need to add a hack somewhere around it.
var spawn = _robustRandom.Pick(_possiblePositions);
var toMap = spawn.ToMap(EntityManager, _transform);
var toMap = _transform.ToMapCoordinates(spawn);
if (_mapManager.TryFindGridAt(toMap, out var gridUid, out _))
{

View File

@@ -22,12 +22,19 @@ public sealed partial class ParadoxCloneRuleComponent : Component
[DataField]
public EntProtoId GibProto = "MobParadoxTimed";
/// <summary>
/// Entity of the original player.
/// Gets randomly chosen from all alive players if not specified.
/// </summary>
[DataField]
public EntityUid? OriginalBody;
/// <summary>
/// Mind entity of the original player.
/// Gets assigned when cloning.
/// </summary>
[DataField]
public EntityUid? Original;
public EntityUid? OriginalMind;
/// <summary>
/// Whitelist for Objectives to be copied to the clone.

View File

@@ -47,28 +47,42 @@ public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComp
if (args.Session?.AttachedEntity is not { } spawner)
return;
// get possible targets
var allHumans = _mind.GetAliveHumans();
// we already checked when starting the gamerule, but someone might have died since then.
if (allHumans.Count == 0)
if (ent.Comp.OriginalBody != null) // target was overridden, for example by admin antag control
{
Log.Warning("Could not find any alive players to create a paradox clone from!");
return;
if (Deleted(ent.Comp.OriginalBody.Value) || !_mind.TryGetMind(ent.Comp.OriginalBody.Value, out var originalMindId, out var _))
{
Log.Warning("Could not find mind of target player to paradox clone!");
return;
}
ent.Comp.OriginalMind = originalMindId;
}
else
{
// get possible targets
var allAliveHumanoids = _mind.GetAliveHumans();
// we already checked when starting the gamerule, but someone might have died since then.
if (allAliveHumanoids.Count == 0)
{
Log.Warning("Could not find any alive players to create a paradox clone from!");
return;
}
// pick a random player
var randomHumanoidMind = _random.Pick(allAliveHumanoids);
ent.Comp.OriginalMind = randomHumanoidMind;
ent.Comp.OriginalBody = randomHumanoidMind.Comp.OwnedEntity;
}
// pick a random player
var playerToClone = _random.Pick(allHumans);
var bodyToClone = playerToClone.Comp.OwnedEntity;
if (bodyToClone == null || !_cloning.TryCloning(bodyToClone.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
if (ent.Comp.OriginalBody == null || !_cloning.TryCloning(ent.Comp.OriginalBody.Value, _transform.GetMapCoordinates(spawner), ent.Comp.Settings, out var clone))
{
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(bodyToClone)}");
Log.Error($"Unable to make a paradox clone of entity {ToPrettyString(ent.Comp.OriginalBody)}");
return;
}
var targetComp = EnsureComp<TargetOverrideComponent>(clone.Value);
targetComp.Target = playerToClone.Owner; // set the kill target
targetComp.Target = ent.Comp.OriginalMind; // set the kill target
var gibComp = EnsureComp<GibOnRoundEndComponent>(clone.Value);
gibComp.SpawnProto = ent.Comp.GibProto;
@@ -78,17 +92,16 @@ public sealed class ParadoxCloneRuleSystem : GameRuleSystem<ParadoxCloneRuleComp
_sensor.SetAllSensors(clone.Value, SuitSensorMode.SensorOff);
args.Entity = clone;
ent.Comp.Original = playerToClone.Owner;
}
private void AfterAntagEntitySelected(Entity<ParadoxCloneRuleComponent> ent, ref AfterAntagEntitySelectedEvent args)
{
if (ent.Comp.Original == null)
if (ent.Comp.OriginalMind == null)
return;
if (!_mind.TryGetMind(args.EntityUid, out var cloneMindId, out var cloneMindComp))
return;
_mind.CopyObjectives(ent.Comp.Original.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
_mind.CopyObjectives(ent.Comp.OriginalMind.Value, (cloneMindId, cloneMindComp), ent.Comp.ObjectiveWhitelist, ent.Comp.ObjectiveBlacklist);
}
}

View File

@@ -8,6 +8,7 @@ using Content.Shared.Examine;
using Content.Shared.Guardian;
using Content.Shared.Hands.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Events;
using Content.Shared.Mobs;
@@ -188,7 +189,9 @@ namespace Content.Server.Guardian
// Can only inject things with the component...
if (!HasComp<CanHostGuardianComponent>(target))
{
_popupSystem.PopupEntity(Loc.GetString("guardian-activator-invalid-target"), user, user);
var msg = Loc.GetString("guardian-activator-invalid-target", ("entity", Identity.Entity(target, EntityManager, user)));
_popupSystem.PopupEntity(msg, user, user);
return;
}

View File

@@ -39,6 +39,15 @@ namespace Content.Server.Hands.Systems
[Dependency] private readonly PullingSystem _pullingSystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
private EntityQuery<PhysicsComponent> _physicsQuery;
/// <summary>
/// Items dropped when the holder falls down will be launched in
/// a direction offset by up to this many degrees from the holder's
/// movement direction.
/// </summary>
private const float DropHeldItemsSpread = 45;
public override void Initialize()
{
base.Initialize();
@@ -60,6 +69,8 @@ namespace Content.Server.Hands.Systems
CommandBinds.Builder
.Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem))
.Register<HandsSystem>();
_physicsQuery = GetEntityQuery<PhysicsComponent>();
}
public override void Shutdown()
@@ -234,13 +245,13 @@ namespace Content.Server.Hands.Systems
private void OnDropHandItems(Entity<HandsComponent> entity, ref DropHandItemsEvent args)
{
var direction = EntityManager.TryGetComponent(entity, out PhysicsComponent? comp) ? comp.LinearVelocity / 50 : Vector2.Zero;
var dropAngle = _random.NextFloat(0.8f, 1.2f);
// If the holder doesn't have a physics component, they ain't moving
var holderVelocity = _physicsQuery.TryComp(entity, out var physics) ? physics.LinearVelocity : Vector2.Zero;
var spreadMaxAngle = Angle.FromDegrees(DropHeldItemsSpread);
var fellEvent = new FellDownEvent(entity);
RaiseLocalEvent(entity, fellEvent, false);
var worldRotation = TransformSystem.GetWorldRotation(entity).ToVec();
foreach (var hand in entity.Comp.Hands.Values)
{
if (hand.HeldEntity is not EntityUid held)
@@ -255,10 +266,26 @@ namespace Content.Server.Hands.Systems
if (!TryDrop(entity, hand, null, checkActionBlocker: false, handsComp: entity.Comp))
continue;
// Rotate the item's throw vector a bit for each item
var angleOffset = _random.NextAngle(-spreadMaxAngle, spreadMaxAngle);
// Rotate the holder's velocity vector by the angle offset to get the item's velocity vector
var itemVelocity = angleOffset.RotateVec(holderVelocity);
// Decrease the distance of the throw by a random amount
itemVelocity *= _random.NextFloat(1f);
// Heavier objects don't get thrown as far
// If the item doesn't have a physics component, it isn't going to get thrown anyway, but we'll assume infinite mass
itemVelocity *= _physicsQuery.TryComp(held, out var heldPhysics) ? heldPhysics.InvMass : 0;
// Throw at half the holder's intentional throw speed and
// vary the speed a little to make it look more interesting
var throwSpeed = entity.Comp.BaseThrowspeed * _random.NextFloat(0.45f, 0.55f);
_throwingSystem.TryThrow(held,
_random.NextAngle().RotateVec(direction / dropAngle + worldRotation / 50),
0.5f * dropAngle * _random.NextFloat(-0.9f, 1.1f),
entity, 0);
itemVelocity,
throwSpeed,
entity,
pushbackRatio: 0,
compensateFriction: false
);
}
}

View File

@@ -2,8 +2,6 @@ using System.Linq;
using Content.Server.Administration;
using Content.Server.GameTicking;
using Content.Shared.Administration;
using Content.Shared.CCVar;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.EntitySerialization;
@@ -19,7 +17,6 @@ namespace Content.Server.Mapping
{
[Dependency] private readonly IEntityManager _entities = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
public string Command => "mapping";
public string Description => Loc.GetString("cmd-mapping-desc");

View File

@@ -1,12 +1,9 @@
using System.IO;
using System.IO;
using Content.Server.Administration.Managers;
using Content.Shared.Administration;
using Content.Shared.Mapping;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.EntitySerialization;
using Robust.Shared.EntitySerialization.Systems;
using Robust.Shared.Map;
using Robust.Shared.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
@@ -19,7 +16,6 @@ public sealed class MappingManager : IPostInjectInit
{
[Dependency] private readonly IAdminManager _admin = default!;
[Dependency] private readonly ILogManager _log = default!;
[Dependency] private readonly IMapManager _map = default!;
[Dependency] private readonly IServerNetManager _net = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly IEntitySystemManager _systems = default!;

View File

@@ -1,22 +0,0 @@
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype;
namespace Content.Server.Medical.Stethoscope.Components
{
/// <summary>
/// Adds an innate verb when equipped to use a stethoscope.
/// </summary>
[RegisterComponent]
public sealed partial class StethoscopeComponent : Component
{
public bool IsActive = false;
[DataField("delay")]
public float Delay = 2.5f;
[DataField("action", customTypeSerializer: typeof(PrototypeIdSerializer<EntityPrototype>))]
public string Action = "ActionStethoscope";
[DataField("actionEntity")] public EntityUid? ActionEntity;
}
}

View File

@@ -1,18 +0,0 @@
using System.Threading;
namespace Content.Server.Medical.Components
{
/// <summary>
/// Used to let doctors use the stethoscope on people.
/// </summary>
[RegisterComponent]
public sealed partial class WearingStethoscopeComponent : Component
{
public CancellationTokenSource? CancelToken;
[DataField("delay")]
public float Delay = 2.5f;
public EntityUid Stethoscope = default!;
}
}

View File

@@ -1,153 +0,0 @@
using Content.Server.Body.Components;
using Content.Server.Medical.Components;
using Content.Server.Medical.Stethoscope.Components;
using Content.Server.Popups;
using Content.Shared.Actions;
using Content.Shared.Clothing;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Medical;
using Content.Shared.Medical.Stethoscope;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Verbs;
using Robust.Shared.Utility;
namespace Content.Server.Medical.Stethoscope
{
public sealed class StethoscopeSystem : EntitySystem
{
[Dependency] private readonly PopupSystem _popupSystem = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!;
[Dependency] private readonly MobStateSystem _mobStateSystem = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StethoscopeComponent, ClothingGotEquippedEvent>(OnEquipped);
SubscribeLocalEvent<StethoscopeComponent, ClothingGotUnequippedEvent>(OnUnequipped);
SubscribeLocalEvent<WearingStethoscopeComponent, GetVerbsEvent<InnateVerb>>(AddStethoscopeVerb);
SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
}
/// <summary>
/// Add the component the verb event subs to if the equippee is wearing the stethoscope.
/// </summary>
private void OnEquipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotEquippedEvent args)
{
component.IsActive = true;
var wearingComp = EnsureComp<WearingStethoscopeComponent>(args.Wearer);
wearingComp.Stethoscope = uid;
}
private void OnUnequipped(EntityUid uid, StethoscopeComponent component, ref ClothingGotUnequippedEvent args)
{
if (!component.IsActive)
return;
RemComp<WearingStethoscopeComponent>(args.Wearer);
component.IsActive = false;
}
/// <summary>
/// This is raised when someone with WearingStethoscopeComponent requests verbs on an item.
/// It returns if the target is not a mob.
/// </summary>
private void AddStethoscopeVerb(EntityUid uid, WearingStethoscopeComponent component, GetVerbsEvent<InnateVerb> args)
{
if (!args.CanInteract || !args.CanAccess)
return;
if (!HasComp<MobStateComponent>(args.Target))
return;
if (component.CancelToken != null)
return;
if (!TryComp<StethoscopeComponent>(component.Stethoscope, out var stetho))
return;
InnateVerb verb = new()
{
Act = () =>
{
StartListening(component.Stethoscope, uid, args.Target, stetho); // start doafter
},
Text = Loc.GetString("stethoscope-verb"),
Icon = new SpriteSpecifier.Rsi(new ("Clothing/Neck/Misc/stethoscope.rsi"), "icon"),
Priority = 2
};
args.Verbs.Add(verb);
}
private void OnStethoscopeAction(EntityUid uid, StethoscopeComponent component, StethoscopeActionEvent args)
{
StartListening(uid, args.Performer, args.Target, component);
}
private void OnGetActions(EntityUid uid, StethoscopeComponent component, GetItemActionsEvent args)
{
args.AddAction(ref component.ActionEntity, component.Action);
}
// construct the doafter and start it
private void StartListening(EntityUid scope, EntityUid user, EntityUid target, StethoscopeComponent comp)
{
_doAfterSystem.TryStartDoAfter(new DoAfterArgs(EntityManager, user, comp.Delay, new StethoscopeDoAfterEvent(), scope, target: target, used: scope)
{
NeedHand = true,
BreakOnMove = true,
});
}
private void OnDoAfter(EntityUid uid, StethoscopeComponent component, DoAfterEvent args)
{
if (args.Handled || args.Cancelled || args.Args.Target == null)
return;
ExamineWithStethoscope(args.Args.User, args.Args.Target.Value);
}
/// <summary>
/// Return a value based on the total oxyloss of the target.
/// Could be expanded in the future with reagent effects etc.
/// The loc lines are taken from the goon wiki.
/// </summary>
public void ExamineWithStethoscope(EntityUid user, EntityUid target)
{
// The mob check seems a bit redundant but (1) they could conceivably have lost it since when the doafter started and (2) I need it for .IsDead()
if (!HasComp<RespiratorComponent>(target) || !TryComp<MobStateComponent>(target, out var mobState) || _mobStateSystem.IsDead(target, mobState))
{
_popupSystem.PopupEntity(Loc.GetString("stethoscope-dead"), target, user);
return;
}
if (!TryComp<DamageableComponent>(target, out var damage))
return;
// these should probably get loc'd at some point before a non-english fork accidentally breaks a bunch of stuff that does this
if (!damage.Damage.DamageDict.TryGetValue("Asphyxiation", out var value))
return;
var message = GetDamageMessage(value);
_popupSystem.PopupEntity(Loc.GetString(message), target, user);
}
private string GetDamageMessage(FixedPoint2 totalOxyloss)
{
var msg = (int) totalOxyloss switch
{
< 20 => "stethoscope-normal",
< 60 => "stethoscope-hyper",
< 80 => "stethoscope-irregular",
_ => "stethoscope-fucked"
};
return msg;
}
}
}

View File

@@ -85,11 +85,11 @@ public sealed class MindSystem : SharedMindSystem
{
if (base.TryGetMind(user, out mindId, out mind))
{
DebugTools.Assert(_players.GetPlayerData(user).ContentData() is not { } data || data.Mind == mindId);
DebugTools.Assert(!_players.TryGetPlayerData(user, out var playerData) || playerData.ContentData() is not { } data || data.Mind == mindId);
return true;
}
DebugTools.Assert(_players.GetPlayerData(user).ContentData()?.Mind == null);
DebugTools.Assert(!_players.TryGetPlayerData(user, out var pData) || pData.ContentData()?.Mind == null);
return false;
}

View File

@@ -28,14 +28,15 @@ public sealed class RotateEyesCommand : IConsoleCommand
}
var count = 0;
foreach (var mover in entManager.EntityQuery<InputMoverComponent>(true))
var query = entManager.EntityQueryEnumerator<InputMoverComponent>();
while (query.MoveNext(out var uid, out var mover))
{
if (mover.TargetRelativeRotation.Equals(rotation))
continue;
mover.TargetRelativeRotation = rotation;
entManager.Dirty(mover);
entManager.Dirty(uid, mover);
count++;
}

View File

@@ -27,6 +27,6 @@ public sealed class BoundarySystem : EntitySystem
// If for whatever reason you want to yeet them to the other side.
// offset = new Angle(MathF.PI).RotateVec(offset);
_xform.SetWorldPosition(otherXform, center + offset);
_xform.SetWorldPosition((args.OtherEntity, otherXform), center + offset);
}
}

View File

@@ -2,6 +2,7 @@ using System.Numerics;
using Content.Server.Movement.Components;
using Content.Server.Physics.Controllers;
using Content.Shared.ActionBlocker;
using Content.Shared.Conveyor;
using Content.Shared.Gravity;
using Content.Shared.Input;
using Content.Shared.Movement.Pulling.Components;
@@ -122,6 +123,12 @@ public sealed class PullController : VirtualController
var pulled = pullerComp.Pulling;
// See update statement; this thing overwrites so many systems, DOESN'T EVEN LERP PROPERLY.
// We had a throwing version but it occasionally had issues.
// We really need the throwing version back.
if (TryComp(pulled, out ConveyedComponent? conveyed) && conveyed.Conveying)
return false;
if (!_pullableQuery.TryComp(pulled, out var pullable))
return false;
@@ -132,9 +139,9 @@ public sealed class PullController : VirtualController
// Cap the distance
var range = 2f;
var fromUserCoords = coords.WithEntityId(player, EntityManager);
var fromUserCoords = _transformSystem.WithEntityId(coords, player);
var userCoords = new EntityCoordinates(player, Vector2.Zero);
if (!_transformSystem.InRange(coords, userCoords, range))
{
var direction = fromUserCoords.Position - userCoords.Position;
@@ -150,7 +157,7 @@ public sealed class PullController : VirtualController
}
fromUserCoords = new EntityCoordinates(player, direction.Normalized() * (range - 0.01f));
coords = fromUserCoords.WithEntityId(coords.EntityId);
coords = _transformSystem.WithEntityId(fromUserCoords, coords.EntityId);
}
var moving = EnsureComp<PullMovingComponent>(pulled!.Value);
@@ -241,7 +248,7 @@ public sealed class PullController : VirtualController
var pullerXform = _xformQuery.Get(puller);
var pullerPosition = TransformSystem.GetMapCoordinates(pullerXform);
var movingTo = mover.MovingTo.ToMap(EntityManager, TransformSystem);
var movingTo = TransformSystem.ToMapCoordinates(mover.MovingTo);
if (movingTo.MapId != pullerPosition.MapId)
{
@@ -257,6 +264,13 @@ public sealed class PullController : VirtualController
continue;
}
// TODO: This whole thing is slop and really needs to be throwing again
if (TryComp(pullableEnt, out ConveyedComponent? conveyed) && conveyed.Conveying)
{
RemCompDeferred<PullMovingComponent>(pullableEnt);
continue;
}
var movingPosition = movingTo.Position;
var ownerPosition = TransformSystem.GetWorldPosition(pullableXform);

View File

@@ -0,0 +1,10 @@
namespace Content.Server.NPC.Queries.Considerations;
/// <summary>
/// Returns 1f if the target has the <see cref="StunnedComponent"/>
/// </summary>
public sealed partial class TargetIsStunnedCon : UtilityConsideration
{
}

View File

@@ -19,6 +19,7 @@ using Content.Shared.Mobs.Systems;
using Content.Shared.NPC.Systems;
using Content.Shared.Nutrition.Components;
using Content.Shared.Nutrition.EntitySystems;
using Content.Shared.Stunnable;
using Content.Shared.Tools.Systems;
using Content.Shared.Turrets;
using Content.Shared.Weapons.Melee;
@@ -360,6 +361,10 @@ public sealed class NPCUtilitySystem : EntitySystem
return 1f;
return 0f;
}
case TargetIsStunnedCon:
{
return HasComp<StunnedComponent>(targetUid) ? 1f : 0f;
}
case TurretTargetingCon:
{
if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||

View File

@@ -1,7 +1,6 @@
using Content.Server.DeviceLinking.Events;
using Content.Server.DeviceLinking.Systems;
using Content.Server.Materials;
using Content.Server.Power.Components;
using Content.Shared.Conveyor;
using Content.Shared.Destructible;
using Content.Shared.Maps;
@@ -10,7 +9,6 @@ using Content.Shared.Physics.Controllers;
using Content.Shared.Power;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Collision.Shapes;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Systems;
namespace Content.Server.Physics.Controllers;
@@ -20,7 +18,6 @@ public sealed class ConveyorController : SharedConveyorController
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly DeviceLinkSystem _signalSystem = default!;
[Dependency] private readonly MaterialReclaimerSystem _materialReclaimer = default!;
[Dependency] private readonly SharedBroadphaseSystem _broadphase = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
public override void Initialize()
@@ -40,7 +37,7 @@ public sealed class ConveyorController : SharedConveyorController
{
_signalSystem.EnsureSinkPorts(uid, component.ReversePort, component.ForwardPort, component.OffPort);
if (TryComp<PhysicsComponent>(uid, out var physics))
if (PhysicsQuery.TryComp(uid, out var physics))
{
var shape = new PolygonShape();
shape.SetAsBox(0.55f, 0.55f);
@@ -57,7 +54,7 @@ public sealed class ConveyorController : SharedConveyorController
if (MetaData(uid).EntityLifeStage >= EntityLifeStage.Terminating)
return;
if (!TryComp<PhysicsComponent>(uid, out var physics))
if (!PhysicsQuery.TryComp(uid, out var physics))
return;
_fixtures.DestroyFixture(uid, ConveyorFixture, body: physics);
@@ -87,13 +84,11 @@ public sealed class ConveyorController : SharedConveyorController
else if (args.Port == component.ForwardPort)
{
AwakenEntities(uid, component);
SetState(uid, ConveyorState.Forward, component);
}
else if (args.Port == component.ReversePort)
{
AwakenEntities(uid, component);
SetState(uid, ConveyorState.Reverse, component);
}
}
@@ -108,8 +103,10 @@ public sealed class ConveyorController : SharedConveyorController
component.State = state;
if (TryComp<PhysicsComponent>(uid, out var physics))
_broadphase.RegenerateContacts((uid, physics));
if (state != ConveyorState.Off)
{
WakeConveyed(uid);
}
UpdateAppearance(uid, component);
Dirty(uid, component);
@@ -117,29 +114,29 @@ public sealed class ConveyorController : SharedConveyorController
/// <summary>
/// Awakens sleeping entities on the conveyor belt's tile when it's turned on.
/// Fixes an issue where non-hard/sleeping entities refuse to wake up + collide if a belt is turned off and on again.
/// Need this as we might activate under CollisionWake entities and need to forcefully check them.
/// </summary>
private void AwakenEntities(EntityUid uid, ConveyorComponent component)
protected override void AwakenConveyor(Entity<TransformComponent?> ent)
{
var xformQuery = GetEntityQuery<TransformComponent>();
var bodyQuery = GetEntityQuery<PhysicsComponent>();
if (!xformQuery.TryGetComponent(uid, out var xform))
if (!XformQuery.Resolve(ent.Owner, ref ent.Comp))
return;
var xform = ent.Comp;
var beltTileRef = xform.Coordinates.GetTileRef(EntityManager, MapManager);
if (beltTileRef != null)
{
var intersecting = Lookup.GetLocalEntitiesIntersecting(beltTileRef.Value, 0f);
Intersecting.Clear();
Lookup.GetLocalEntitiesIntersecting(beltTileRef.Value.GridUid, beltTileRef.Value.GridIndices, Intersecting, 0f, flags: LookupFlags.Dynamic | LookupFlags.Sundries | LookupFlags.Approximate);
foreach (var entity in intersecting)
foreach (var entity in Intersecting)
{
if (!bodyQuery.TryGetComponent(entity, out var physics))
if (!PhysicsQuery.TryGetComponent(entity, out var physics))
continue;
if (physics.BodyType != BodyType.Static)
Physics.WakeBody(entity, body: physics);
PhysicsSystem.WakeBody(entity, body: physics);
}
}
}

View File

@@ -1,24 +0,0 @@
using Content.Server.Polymorph.Systems;
using Content.Shared.Polymorph;
using Content.Shared.Whitelist;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server.Polymorph.Components;
[RegisterComponent]
[Access(typeof(PolymorphSystem))]
public sealed partial class PolymorphOnCollideComponent : Component
{
[DataField(required: true)]
public ProtoId<PolymorphPrototype> Polymorph;
[DataField(required: true)]
public EntityWhitelist Whitelist = default!;
[DataField]
public EntityWhitelist? Blacklist;
[DataField]
public SoundSpecifier Sound = new SoundPathSpecifier("/Audio/Magic/forcewall.ogg");
}

View File

@@ -21,6 +21,7 @@ namespace Content.Server.Rotatable
[Dependency] private readonly PopupSystem _popup = default!;
[Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
[Dependency] private readonly SharedInteractionSystem _interaction = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
public override void Initialize()
{
@@ -112,7 +113,7 @@ namespace Content.Server.Rotatable
var entity = EntityManager.SpawnEntity(component.MirrorEntity, oldTransform.Coordinates);
var newTransform = EntityManager.GetComponent<TransformComponent>(entity);
newTransform.LocalRotation = oldTransform.LocalRotation;
newTransform.Anchored = false;
_transform.Unanchor(entity, newTransform);
EntityManager.DeleteEntity(uid);
}

View File

@@ -95,6 +95,10 @@ public sealed class HandTeleporterSystem : EntitySystem
var timeout = EnsureComp<PortalTimeoutComponent>(user);
timeout.EnteredPortal = null;
component.FirstPortal = Spawn(component.FirstPortalPrototype, Transform(user).Coordinates);
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.FirstPortal, out var portal))
portal.CanTeleportToOtherMaps = true;
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.FirstPortal.Value)} at {Transform(component.FirstPortal.Value).Coordinates} using {ToPrettyString(uid)}");
_audio.PlayPvs(component.NewPortalSound, uid);
}
@@ -113,6 +117,10 @@ public sealed class HandTeleporterSystem : EntitySystem
var timeout = EnsureComp<PortalTimeoutComponent>(user);
timeout.EnteredPortal = null;
component.SecondPortal = Spawn(component.SecondPortalPrototype, Transform(user).Coordinates);
if (component.AllowPortalsOnDifferentMaps && TryComp<PortalComponent>(component.SecondPortal, out var portal))
portal.CanTeleportToOtherMaps = true;
_adminLogger.Add(LogType.EntitySpawn, LogImpact.High, $"{ToPrettyString(user):player} opened {ToPrettyString(component.SecondPortal.Value)} at {Transform(component.SecondPortal.Value).Coordinates} linked to {ToPrettyString(component.FirstPortal!.Value)} using {ToPrettyString(uid)}");
_link.TryLink(component.FirstPortal!.Value, component.SecondPortal.Value, true);
_audio.PlayPvs(component.NewPortalSound, uid);

View File

@@ -1,45 +1,41 @@
using Content.Server.Power.Components;
using Content.Server.Temperature.Components;
using Content.Shared.Examine;
using Content.Shared.Placeable;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Temperature;
using Content.Shared.Verbs;
using Robust.Server.Audio;
using Content.Shared.Temperature.Components;
using Content.Shared.Temperature.Systems;
namespace Content.Server.Temperature.Systems;
/// <summary>
/// Handles <see cref="EntityHeaterComponent"/> updating and events.
/// Handles the server-only parts of <see cref="SharedEntityHeaterSystem"/>
/// </summary>
public sealed class EntityHeaterSystem : EntitySystem
public sealed class EntityHeaterSystem : SharedEntityHeaterSystem
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly TemperatureSystem _temperature = default!;
[Dependency] private readonly AudioSystem _audio = default!;
private readonly int SettingCount = Enum.GetValues(typeof(EntityHeaterSetting)).Length;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<EntityHeaterComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<EntityHeaterComponent, GetVerbsEvent<AlternativeVerb>>(OnGetVerbs);
SubscribeLocalEvent<EntityHeaterComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<EntityHeaterComponent, MapInitEvent>(OnMapInit);
}
private void OnMapInit(Entity<EntityHeaterComponent> ent, ref MapInitEvent args)
{
// Set initial power level
if (TryComp<ApcPowerReceiverComponent>(ent, out var power))
power.Load = SettingPower(ent.Comp.Setting, ent.Comp.Power);
}
public override void Update(float deltaTime)
{
var query = EntityQueryEnumerator<EntityHeaterComponent, ItemPlacerComponent, ApcPowerReceiverComponent>();
while (query.MoveNext(out var uid, out var comp, out var placer, out var power))
while (query.MoveNext(out _, out _, out var placer, out var power))
{
if (!power.Powered)
continue;
// don't divide by total entities since its a big grill
// don't divide by total entities since it's a big grill
// excess would just be wasted in the air but that's not worth simulating
// if you want a heater thermomachine just use that...
var energy = power.PowerReceived * deltaTime;
@@ -50,66 +46,17 @@ public sealed class EntityHeaterSystem : EntitySystem
}
}
private void OnExamined(EntityUid uid, EntityHeaterComponent comp, ExaminedEvent args)
/// <remarks>
/// <see cref="ApcPowerReceiverComponent"/> doesn't exist on the client, so we need
/// this server-only override to handle setting the network load.
/// </remarks>
protected override void ChangeSetting(Entity<EntityHeaterComponent> ent, EntityHeaterSetting setting, EntityUid? user = null)
{
if (!args.IsInDetailsRange)
base.ChangeSetting(ent, setting, user);
if (!TryComp<ApcPowerReceiverComponent>(ent, out var power))
return;
args.PushMarkup(Loc.GetString("entity-heater-examined", ("setting", comp.Setting)));
}
private void OnGetVerbs(EntityUid uid, EntityHeaterComponent comp, GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanAccess || !args.CanInteract)
return;
var setting = (int) comp.Setting;
setting++;
setting %= SettingCount;
var nextSetting = (EntityHeaterSetting) setting;
args.Verbs.Add(new AlternativeVerb()
{
Text = Loc.GetString("entity-heater-switch-setting", ("setting", nextSetting)),
Act = () =>
{
ChangeSetting(uid, nextSetting, comp);
_popup.PopupEntity(Loc.GetString("entity-heater-switched-setting", ("setting", nextSetting)), uid, args.User);
}
});
}
private void OnPowerChanged(EntityUid uid, EntityHeaterComponent comp, ref PowerChangedEvent args)
{
// disable heating element glowing layer if theres no power
// doesn't actually turn it off since that would be annoying
var setting = args.Powered ? comp.Setting : EntityHeaterSetting.Off;
_appearance.SetData(uid, EntityHeaterVisuals.Setting, setting);
}
private void ChangeSetting(EntityUid uid, EntityHeaterSetting setting, EntityHeaterComponent? comp = null, ApcPowerReceiverComponent? power = null)
{
if (!Resolve(uid, ref comp, ref power))
return;
comp.Setting = setting;
power.Load = SettingPower(setting, comp.Power);
_appearance.SetData(uid, EntityHeaterVisuals.Setting, setting);
_audio.PlayPvs(comp.SettingSound, uid);
}
private float SettingPower(EntityHeaterSetting setting, float max)
{
switch (setting)
{
case EntityHeaterSetting.Low:
return max / 3f;
case EntityHeaterSetting.Medium:
return max * 2f / 3f;
case EntityHeaterSetting.High:
return max;
default:
return 0f;
}
power.Load = SettingPower(setting, ent.Comp.Power);
}
}

View File

@@ -22,11 +22,18 @@ public sealed partial class ThiefUndeterminedBackpackComponent : Component
public List<int> SelectedSets = new();
[DataField]
public SoundSpecifier ApproveSound = new SoundPathSpecifier("/Audio/Effects/rustle1.ogg");
public SoundCollectionSpecifier ApproveSound = new SoundCollectionSpecifier("storageRustle");
/// <summary>
/// Max number of sets you can select.
/// </summary>
[DataField]
public int MaxSelectedSets = 2;
/// <summary>
/// What entity all the spawned items will appear inside of
/// If null, will instead drop on the ground.
/// </summary>
[DataField]
public EntProtoId? SpawnedStoragePrototype;
}

View File

@@ -1,5 +1,7 @@
using Content.Server.Thief.Components;
using Content.Shared.Hands.EntitySystems;
using Content.Shared.Item;
using Content.Shared.Storage.EntitySystems;
using Content.Shared.Thief;
using Robust.Server.GameObjects;
using Robust.Server.Audio;
@@ -17,6 +19,8 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
[Dependency] private readonly IPrototypeManager _proto = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedStorageSystem _storage = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
public override void Initialize()
{
@@ -37,6 +41,10 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
if (backpack.Comp.SelectedSets.Count != backpack.Comp.MaxSelectedSets)
return;
EntityUid? spawnedStorage = null;
if (backpack.Comp.SpawnedStoragePrototype != null)
spawnedStorage = Spawn(backpack.Comp.SpawnedStoragePrototype, _transform.GetMapCoordinates(backpack.Owner));
foreach (var i in backpack.Comp.SelectedSets)
{
var set = _proto.Index(backpack.Comp.PossibleSets[i]);
@@ -44,10 +52,20 @@ public sealed class ThiefUndeterminedBackpackSystem : EntitySystem
{
var ent = Spawn(item, _transform.GetMapCoordinates(backpack.Owner));
if (TryComp<ItemComponent>(ent, out var itemComponent))
_transform.DropNextTo(ent, backpack.Owner);
{
if (spawnedStorage != null)
_storage.Insert(spawnedStorage.Value, ent, out _, playSound: false);
else
_transform.DropNextTo(ent, backpack.Owner);
}
}
}
_audio.PlayPvs(backpack.Comp.ApproveSound, backpack.Owner);
if (spawnedStorage != null)
_hands.TryPickupAnyHand(args.Actor, spawnedStorage.Value);
// Play the sound on coordinates of the backpack/toolbox. The reason being, since we immediately delete it, the sound gets deleted alongside it.
_audio.PlayPvs(backpack.Comp.ApproveSound, Transform(backpack.Owner).Coordinates);
QueueDel(backpack);
}
private void OnChangeSet(Entity<ThiefUndeterminedBackpackComponent> backpack, ref ThiefBackpackChangeSetMessage args)

View File

@@ -0,0 +1,175 @@
using Content.Server.Destructible;
using Content.Server.DeviceNetwork;
using Content.Server.DeviceNetwork.Components;
using Content.Server.DeviceNetwork.Systems;
using Content.Server.NPC.HTN;
using Content.Server.NPC.HTN.PrimitiveTasks.Operators.Combat.Ranged;
using Content.Server.Power.Components;
using Content.Server.Repairable;
using Content.Shared.Destructible;
using Content.Shared.DeviceNetwork;
using Content.Shared.Power;
using Content.Shared.Turrets;
using Content.Shared.Weapons.Ranged.Events;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Timing;
namespace Content.Server.Turrets;
public sealed partial class DeployableTurretSystem : SharedDeployableTurretSystem
{
[Dependency] private readonly HTNSystem _htn = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedAudioSystem _audio = default!;
[Dependency] private readonly DeviceNetworkSystem _deviceNetwork = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<DeployableTurretComponent, AmmoShotEvent>(OnAmmoShot);
SubscribeLocalEvent<DeployableTurretComponent, ChargeChangedEvent>(OnChargeChanged);
SubscribeLocalEvent<DeployableTurretComponent, PowerChangedEvent>(OnPowerChanged);
SubscribeLocalEvent<DeployableTurretComponent, BreakageEventArgs>(OnBroken);
SubscribeLocalEvent<DeployableTurretComponent, RepairedEvent>(OnRepaired);
SubscribeLocalEvent<DeployableTurretComponent, BeforeBroadcastAttemptEvent>(OnBeforeBroadcast);
}
private void OnAmmoShot(Entity<DeployableTurretComponent> ent, ref AmmoShotEvent args)
{
UpdateAmmoStatus(ent);
}
private void OnChargeChanged(Entity<DeployableTurretComponent> ent, ref ChargeChangedEvent args)
{
UpdateAmmoStatus(ent);
}
private void OnPowerChanged(Entity<DeployableTurretComponent> ent, ref PowerChangedEvent args)
{
UpdateAmmoStatus(ent);
}
private void OnBroken(Entity<DeployableTurretComponent> ent, ref BreakageEventArgs args)
{
if (TryComp<AppearanceComponent>(ent, out var appearance))
_appearance.SetData(ent, DeployableTurretVisuals.Broken, true, appearance);
SetState(ent, false);
}
private void OnRepaired(Entity<DeployableTurretComponent> ent, ref RepairedEvent args)
{
if (TryComp<AppearanceComponent>(ent, out var appearance))
_appearance.SetData(ent, DeployableTurretVisuals.Broken, false, appearance);
}
private void OnBeforeBroadcast(Entity<DeployableTurretComponent> ent, ref BeforeBroadcastAttemptEvent args)
{
if (!TryComp<DeviceNetworkComponent>(ent, out var deviceNetwork))
return;
var recipientDeviceNetworks = new HashSet<DeviceNetworkComponent>();
// Only broadcast to connected devices
foreach (var recipient in deviceNetwork.DeviceLists)
{
if (!TryComp<DeviceNetworkComponent>(recipient, out var recipientDeviceNetwork))
continue;
recipientDeviceNetworks.Add(recipientDeviceNetwork);
}
if (recipientDeviceNetworks.Count > 0)
args.ModifiedRecipients = recipientDeviceNetworks;
}
private void SendStateUpdateToDeviceNetwork(Entity<DeployableTurretComponent> ent)
{
if (!TryComp<DeviceNetworkComponent>(ent, out var device))
return;
var payload = new NetworkPayload
{
[DeviceNetworkConstants.Command] = DeviceNetworkConstants.CmdUpdatedState,
[DeviceNetworkConstants.CmdUpdatedState] = GetTurretState(ent)
};
_deviceNetwork.QueuePacket(ent, null, payload, device: device);
}
protected override void SetState(Entity<DeployableTurretComponent> ent, bool enabled, EntityUid? user = null)
{
if (ent.Comp.Enabled == enabled)
return;
base.SetState(ent, enabled, user);
DirtyField(ent, ent.Comp, nameof(DeployableTurretComponent.Enabled));
// Determine how much time is remaining in the current animation and the one next in queue
var animTimeRemaining = MathF.Max((float)(ent.Comp.AnimationCompletionTime - _timing.CurTime).TotalSeconds, 0f);
var animTimeNext = ent.Comp.Enabled ? ent.Comp.DeploymentLength : ent.Comp.RetractionLength;
// End/restart any tasks the NPC was doing
// Delay the resumption of any tasks based on the total animation length (plus a buffer)
var planCooldown = animTimeRemaining + animTimeNext + 0.5f;
if (TryComp<HTNComponent>(ent, out var htn))
_htn.SetHTNEnabled((ent, htn), ent.Comp.Enabled, planCooldown);
// Play audio
_audio.PlayPvs(ent.Comp.Enabled ? ent.Comp.DeploymentSound : ent.Comp.RetractionSound, ent, new AudioParams { Volume = -10f });
}
private void UpdateAmmoStatus(Entity<DeployableTurretComponent> ent)
{
if (!HasAmmo(ent))
SetState(ent, false);
}
private DeployableTurretState GetTurretState(Entity<DeployableTurretComponent> ent, DestructibleComponent? destructable = null, HTNComponent? htn = null)
{
Resolve(ent, ref destructable, ref htn);
if (destructable?.IsBroken == true)
return DeployableTurretState.Broken;
if (htn == null || !HasAmmo(ent))
return DeployableTurretState.Disabled;
if (htn.Plan?.CurrentTask.Operator is GunOperator)
return DeployableTurretState.Firing;
if (ent.Comp.AnimationCompletionTime > _timing.CurTime)
return ent.Comp.Enabled ? DeployableTurretState.Deploying : DeployableTurretState.Retracting;
return ent.Comp.Enabled ? DeployableTurretState.Deployed : DeployableTurretState.Retracted;
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<DeployableTurretComponent, DestructibleComponent, HTNComponent>();
while (query.MoveNext(out var uid, out var deployableTurret, out var destructible, out var htn))
{
// Check if the turret state has changed since the last update,
// and if it has, inform the device network
var ent = new Entity<DeployableTurretComponent>(uid, deployableTurret);
var newState = GetTurretState(ent, destructible, htn);
if (newState != deployableTurret.CurrentState)
{
deployableTurret.CurrentState = newState;
DirtyField(uid, deployableTurret, nameof(DeployableTurretComponent.CurrentState));
SendStateUpdateToDeviceNetwork(ent);
if (TryComp<AppearanceComponent>(ent, out var appearance))
_appearance.SetData(ent, DeployableTurretVisuals.Turret, newState, appearance);
}
}
}
}

View File

@@ -132,6 +132,16 @@ public sealed partial class ZombieSystem
melee.Angle = 0.0f;
melee.HitSound = zombiecomp.BiteSound;
DirtyFields(target, melee, null, fields:
[
nameof(MeleeWeaponComponent.Animation),
nameof(MeleeWeaponComponent.WideAnimation),
nameof(MeleeWeaponComponent.AltDisarm),
nameof(MeleeWeaponComponent.Range),
nameof(MeleeWeaponComponent.Angle),
nameof(MeleeWeaponComponent.HitSound),
]);
if (mobState.CurrentState == MobState.Alive)
{
// Groaning when damaged

View File

@@ -3,7 +3,7 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Atmos.Components;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)]
public sealed partial class GasPressurePumpComponent : Component
{
[DataField, AutoNetworkedField]

View File

@@ -5,18 +5,15 @@ using Content.Shared.Atmos.Piping.Binary.Components;
using Content.Shared.Atmos.Piping.Components;
using Content.Shared.Database;
using Content.Shared.Examine;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.Components;
using Content.Shared.Power.EntitySystems;
using Content.Shared.UserInterface;
namespace Content.Shared.Atmos.EntitySystems;
public abstract class SharedGasPressurePumpSystem : EntitySystem
{
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly SharedPowerReceiverSystem _receiver = default!;
[Dependency] protected readonly SharedUserInterfaceSystem UserInterfaceSystem = default!;
@@ -36,62 +33,71 @@ public abstract class SharedGasPressurePumpSystem : EntitySystem
SubscribeLocalEvent<GasPressurePumpComponent, ExaminedEvent>(OnExamined);
}
private void OnExamined(EntityUid uid, GasPressurePumpComponent pump, ExaminedEvent args)
private void OnExamined(Entity<GasPressurePumpComponent> ent, ref ExaminedEvent args)
{
if (!Transform(uid).Anchored)
if (!Transform(ent).Anchored)
return;
if (Loc.TryGetString("gas-pressure-pump-system-examined", out var str,
if (Loc.TryGetString("gas-pressure-pump-system-examined",
out var str,
("statusColor", "lightblue"), // TODO: change with pressure?
("pressure", pump.TargetPressure)
("pressure", ent.Comp.TargetPressure)
))
{
args.PushMarkup(str);
}
}
private void OnInit(EntityUid uid, GasPressurePumpComponent pump, ComponentInit args)
private void OnInit(Entity<GasPressurePumpComponent> ent, ref ComponentInit args)
{
UpdateAppearance(uid, pump);
UpdateAppearance(ent);
}
private void OnPowerChanged(EntityUid uid, GasPressurePumpComponent component, ref PowerChangedEvent args)
private void OnPowerChanged(Entity<GasPressurePumpComponent> ent, ref PowerChangedEvent args)
{
UpdateAppearance(uid, component);
UpdateAppearance(ent);
}
private void UpdateAppearance(EntityUid uid, GasPressurePumpComponent? pump = null, AppearanceComponent? appearance = null)
private void UpdateAppearance(Entity<GasPressurePumpComponent, AppearanceComponent?> ent)
{
if (!Resolve(uid, ref pump, ref appearance, false))
if (!Resolve(ent, ref ent.Comp2, false))
return;
var pumpOn = pump.Enabled && _receiver.IsPowered(uid);
Appearance.SetData(uid, PumpVisuals.Enabled, pumpOn, appearance);
var pumpOn = ent.Comp1.Enabled && _receiver.IsPowered(ent.Owner);
_appearance.SetData(ent, PumpVisuals.Enabled, pumpOn, ent.Comp2);
}
private void OnToggleStatusMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpToggleStatusMessage args)
private void OnToggleStatusMessage(Entity<GasPressurePumpComponent> ent, ref GasPressurePumpToggleStatusMessage args)
{
pump.Enabled = args.Enabled;
_adminLogger.Add(LogType.AtmosPowerChanged, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(uid):device} to {args.Enabled}");
Dirty(uid, pump);
UpdateAppearance(uid, pump);
ent.Comp.Enabled = args.Enabled;
_adminLogger.Add(LogType.AtmosPowerChanged,
LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the power on {ToPrettyString(ent):device} to {args.Enabled}");
Dirty(ent);
UpdateAppearance(ent);
UpdateUi(ent);
}
private void OnOutputPressureChangeMessage(EntityUid uid, GasPressurePumpComponent pump, GasPressurePumpChangeOutputPressureMessage args)
private void OnOutputPressureChangeMessage(Entity<GasPressurePumpComponent> ent, ref GasPressurePumpChangeOutputPressureMessage args)
{
pump.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
_adminLogger.Add(LogType.AtmosPressureChanged, LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(uid):device} to {args.Pressure}kPa");
Dirty(uid, pump);
ent.Comp.TargetPressure = Math.Clamp(args.Pressure, 0f, Atmospherics.MaxOutputPressure);
_adminLogger.Add(LogType.AtmosPressureChanged,
LogImpact.Medium,
$"{ToPrettyString(args.Actor):player} set the pressure on {ToPrettyString(ent):device} to {args.Pressure}kPa");
Dirty(ent);
UpdateUi(ent);
}
private void OnPumpLeaveAtmosphere(EntityUid uid, GasPressurePumpComponent pump, ref AtmosDeviceDisabledEvent args)
private void OnPumpLeaveAtmosphere(Entity<GasPressurePumpComponent> ent, ref AtmosDeviceDisabledEvent args)
{
pump.Enabled = false;
Dirty(uid, pump);
UpdateAppearance(uid, pump);
ent.Comp.Enabled = false;
Dirty(ent);
UpdateAppearance(ent);
UserInterfaceSystem.CloseUi(uid, GasPressurePumpUiKey.Key);
UserInterfaceSystem.CloseUi(ent.Owner, GasPressurePumpUiKey.Key);
}
protected virtual void UpdateUi(Entity<GasPressurePumpComponent> ent)
{
}
}

View File

@@ -146,13 +146,13 @@ public sealed partial class CCVars
/// The delay for which two votekicks are allowed to be made by separate people, in seconds.
/// </summary>
public static readonly CVarDef<float> VotekickTimeout =
CVarDef.Create("votekick.timeout", 120f, CVar.SERVERONLY);
CVarDef.Create("votekick.timeout", 60f, CVar.SERVERONLY);
/// <summary>
/// Sets the duration of the votekick vote timer.
/// </summary>
public static readonly CVarDef<int>
VotekickTimer = CVarDef.Create("votekick.timer", 60, CVar.SERVERONLY);
VotekickTimer = CVarDef.Create("votekick.timer", 45, CVar.SERVERONLY);
/// <summary>
/// Config for how many hours playtime a player must have to get protection from the Raider votekick type when playing as an antag.

View File

@@ -1,3 +1,4 @@
using System.Linq;
using System.Numerics;
using Content.Shared.EntityTable;
using Robust.Shared.Containers;
@@ -39,7 +40,8 @@ public sealed class ContainerFillSystem : EntitySystem
var ent = Spawn(proto, coords);
if (!_containerSystem.Insert(ent, container, containerXform: xform))
{
Log.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}.");
var alreadyContained = container.ContainedEntities.Count > 0 ? string.Join("\n", container.ContainedEntities.Select(e => $"\t - {EntityManager.ToPrettyString(e)}")) : "< empty >";
Log.Error($"Entity {ToPrettyString(uid)} with a {nameof(ContainerFillComponent)} failed to insert an entity: {ToPrettyString(ent)}.\nCurrent contents:\n{alreadyContained}");
_transform.AttachToGridOrMap(ent);
break;
}
@@ -72,7 +74,8 @@ public sealed class ContainerFillSystem : EntitySystem
var spawn = Spawn(proto, coords);
if (!_containerSystem.Insert(spawn, container, containerXform: xform))
{
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.");
var alreadyContained = container.ContainedEntities.Count > 0 ? string.Join("\n", container.ContainedEntities.Select(e => $"\t - {EntityManager.ToPrettyString(e)}")) : "< empty >";
Log.Error($"Entity {ToPrettyString(ent)} with a {nameof(EntityTableContainerFillComponent)} failed to insert an entity: {ToPrettyString(spawn)}.\nCurrent contents:\n{alreadyContained}");
_transform.AttachToGridOrMap(spawn);
break;
}

View File

@@ -3,11 +3,15 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Conveyor;
/// <summary>
/// Indicates this entity is currently being conveyed.
/// Indicates this entity is currently contacting a conveyor and will subscribe to events as appropriate.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
public sealed partial class ConveyedComponent : Component
{
[ViewVariables, AutoNetworkedField]
public List<EntityUid> Colliding = new();
// TODO: Delete if pulling gets fixed.
/// <summary>
/// True if currently conveying.
/// </summary>
[DataField, AutoNetworkedField]
public bool Conveying;
}

View File

@@ -4,6 +4,7 @@ using Content.Shared.Administration.Components;
using Content.Shared.Administration.Logs;
using Content.Shared.Alert;
using Content.Shared.Buckle.Components;
using Content.Shared.CombatMode;
using Content.Shared.Cuffs.Components;
using Content.Shared.Database;
using Content.Shared.DoAfter;
@@ -53,6 +54,7 @@ namespace Content.Shared.Cuffs
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedTransformSystem _transform = default!;
[Dependency] private readonly UseDelaySystem _delay = default!;
[Dependency] private readonly SharedCombatModeSystem _combatMode = default!;
public override void Initialize()
{
@@ -717,10 +719,31 @@ namespace Content.Shared.Cuffs
}
}
var shoved = false;
// if combat mode is on, shove the person.
if (_combatMode.IsInCombatMode(user) && target != user && user != null)
{
var eventArgs = new DisarmedEvent { Target = target, Source = user.Value, PushProbability = 1};
RaiseLocalEvent(target, eventArgs);
shoved = true;
}
if (cuffable.CuffedHandCount == 0)
{
if (user != null)
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
{
if (shoved)
{
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-push-success-message",
("otherName", Identity.Name(user.Value, EntityManager, user))),
user.Value,
user.Value);
}
else
{
_popup.PopupClient(Loc.GetString("cuffable-component-remove-cuffs-success-message"), user.Value, user.Value);
}
}
if (target != user && user != null)
{

View File

@@ -44,4 +44,46 @@ public sealed partial class DamageOnInteractComponent : Component
/// </summary>
[DataField, AutoNetworkedField]
public bool IsDamageActive = true;
/// <summary>
/// Whether the thing should be thrown from its current position when they interact with the entity
/// </summary>
[DataField]
public bool Throw = false;
/// <summary>
/// The speed applied to the thing when it is thrown
/// </summary>
[DataField]
public int ThrowSpeed = 10;
/// <summary>
/// Time between being able to interact with this entity
/// </summary>
[DataField]
public uint InteractTimer = 0;
/// <summary>
/// Tracks the last time this entity was interacted with, but only if the interaction resulted in the user taking damage
/// </summary>
[DataField]
public TimeSpan LastInteraction = TimeSpan.Zero;
/// <summary>
/// Tracks the time that this entity can be interacted with, but only if the interaction resulted in the user taking damage
/// </summary>
[DataField]
public TimeSpan NextInteraction = TimeSpan.Zero;
/// <summary>
/// Probability that the user will be stunned when they interact with with this entity and took damage
/// </summary>
[DataField]
public float StunChance = 0.0f;
/// <summary>
/// Duration, in seconds, of the stun applied to the user when they interact with the entity and took damage
/// </summary>
[DataField]
public float StunSeconds = 0.0f;
}

View File

@@ -4,9 +4,15 @@ using Content.Shared.Database;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.Popups;
using Robust.Shared.Random;
using Content.Shared.Throwing;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Network;
using Robust.Shared.Timing;
using Content.Shared.Random;
using Content.Shared.Movement.Pulling.Components;
using Content.Shared.Effects;
using Content.Shared.Stunnable;
namespace Content.Shared.Damage.Systems;
@@ -17,6 +23,10 @@ public sealed class DamageOnInteractSystem : EntitySystem
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly InventorySystem _inventorySystem = default!;
[Dependency] private readonly ThrowingSystem _throwingSystem = default!;
[Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
[Dependency] private readonly SharedStunSystem _stun = default!;
public override void Initialize()
{
@@ -35,6 +45,13 @@ public sealed class DamageOnInteractSystem : EntitySystem
/// <param name="args">Contains the user that interacted with the entity</param>
private void OnHandInteract(Entity<DamageOnInteractComponent> entity, ref InteractHandEvent args)
{
// Stop the interaction if the user attempts to interact with the object before the timer is finished
if (_gameTiming.CurTime < entity.Comp.NextInteraction)
{
args.Handled = true;
return;
}
if (!entity.Comp.IsDamageActive)
return;
@@ -47,9 +64,8 @@ public sealed class DamageOnInteractSystem : EntitySystem
// or checking the entity for the comp itself if the inventory didn't work
if (protectiveEntity.Comp == null && TryComp<DamageOnInteractProtectionComponent>(args.User, out var protectiveComp))
{
protectiveEntity = (args.User, protectiveComp);
}
// if protectiveComp isn't null after all that, it means the user has protection,
// so let's calculate how much they resist
@@ -59,17 +75,31 @@ public sealed class DamageOnInteractSystem : EntitySystem
}
}
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
totalDamage = _damageableSystem.TryChangeDamage(args.User, totalDamage, origin: args.Target);
if (totalDamage != null && totalDamage.AnyPositive())
{
// Record this interaction and determine when a user is allowed to interact with this entity again
entity.Comp.LastInteraction = _gameTiming.CurTime;
entity.Comp.NextInteraction = _gameTiming.CurTime + TimeSpan.FromSeconds(entity.Comp.InteractTimer);
args.Handled = true;
_adminLogger.Add(LogType.Damaged, $"{ToPrettyString(args.User):user} injured their hand by interacting with {ToPrettyString(args.Target):target} and received {totalDamage.GetTotal():damage} damage");
_audioSystem.PlayPredicted(entity.Comp.InteractSound, args.Target, args.User);
if (entity.Comp.PopupText != null)
_popupSystem.PopupClient(Loc.GetString(entity.Comp.PopupText), args.User, args.User);
// Attempt to paralyze the user after they have taken damage
if (_random.Prob(entity.Comp.StunChance))
_stun.TryParalyze(args.User, TimeSpan.FromSeconds(entity.Comp.StunSeconds), true);
}
// Check if the entity's Throw bool is false, or if the entity has the PullableComponent, then if the entity is currently being pulled.
// BeingPulled must be checked because the entity will be spastically thrown around without this.
if (!entity.Comp.Throw || !TryComp<PullableComponent>(entity, out var pullComp) || pullComp.BeingPulled)
return;
_throwingSystem.TryThrow(entity, _random.NextVector2(), entity.Comp.ThrowSpeed, doSpin: true);
}
public void SetIsDamageActiveTo(Entity<DamageOnInteractComponent> entity, bool mode)

View File

@@ -296,7 +296,7 @@ namespace Content.Shared.Damage
DamageChanged(uid, component, new DamageSpecifier());
}
public void SetDamageModifierSetId(EntityUid uid, string damageModifierSetId, DamageableComponent? comp = null)
public void SetDamageModifierSetId(EntityUid uid, string? damageModifierSetId, DamageableComponent? comp = null)
{
if (!_damageableQuery.Resolve(uid, ref comp))
return;

View File

@@ -67,6 +67,8 @@ public partial class InventorySystem
SubscribeLocalEvent<InventoryComponent, RefreshEquipmentHudEvent<ShowCriminalRecordIconsComponent>>(RefRelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<EquipmentVerb>>(OnGetEquipmentVerbs);
SubscribeLocalEvent<InventoryComponent, GetVerbsEvent<InnateVerb>>(OnGetInnateVerbs);
}
protected void RefRelayInventoryEvent<T>(EntityUid uid, InventoryComponent component, ref T args) where T : IInventoryRelayEvent
@@ -121,6 +123,17 @@ public partial class InventorySystem
}
}
private void OnGetInnateVerbs(EntityUid uid, InventoryComponent component, GetVerbsEvent<InnateVerb> args)
{
// Automatically relay stripping related verbs to all equipped clothing.
var ev = new InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>(args);
var enumerator = new InventorySlotEnumerator(component, SlotFlags.WITHOUT_POCKET);
while (enumerator.NextItem(out var item))
{
RaiseLocalEvent(item, ev);
}
}
}
/// <summary>

View File

@@ -23,7 +23,7 @@ namespace Content.Shared.Maps
return null;
mapManager ??= IoCManager.Resolve<IMapManager>();
var pos = coordinates.ToMap(entityManager, entityManager.System<SharedTransformSystem>());
var pos = entityManager.System<SharedTransformSystem>().ToMapCoordinates(coordinates);
if (!mapManager.TryFindGridAt(pos, out _, out var grid))
return null;

View File

@@ -0,0 +1,31 @@
using Content.Shared.FixedPoint;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
namespace Content.Shared.Medical.Stethoscope.Components;
/// <summary>
/// Adds a verb and action that allows the user to listen to the entity's breathing.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class StethoscopeComponent : Component
{
/// <summary>
/// Time between each use of the stethoscope.
/// </summary>
[DataField]
public TimeSpan Delay = TimeSpan.FromSeconds(1.75);
/// <summary>
/// Last damage that was measured. Used to indicate if breathing is improving or getting worse.
/// </summary>
[DataField]
public FixedPoint2? LastMeasuredDamage;
[DataField]
public EntProtoId Action = "ActionStethoscope";
[DataField]
public EntityUid? ActionEntity;
}

View File

@@ -1,7 +0,0 @@
using Content.Shared.Actions;
namespace Content.Shared.Medical.Stethoscope;
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent
{
}

View File

@@ -0,0 +1,148 @@
using Content.Shared.Actions;
using Content.Shared.Damage;
using Content.Shared.DoAfter;
using Content.Shared.FixedPoint;
using Content.Shared.Inventory;
using Content.Shared.Medical.Stethoscope.Components;
using Content.Shared.Mobs.Components;
using Content.Shared.Mobs.Systems;
using Content.Shared.Popups;
using Content.Shared.Verbs;
using Robust.Shared.Containers;
namespace Content.Shared.Medical.Stethoscope;
public sealed class StethoscopeSystem : EntitySystem
{
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
[Dependency] private readonly MobStateSystem _mobState = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
// The damage type to "listen" for with the stethoscope.
private const string DamageToListenFor = "Asphyxiation";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<StethoscopeComponent, InventoryRelayedEvent<GetVerbsEvent<InnateVerb>>>(AddStethoscopeVerb);
SubscribeLocalEvent<StethoscopeComponent, GetItemActionsEvent>(OnGetActions);
SubscribeLocalEvent<StethoscopeComponent, StethoscopeActionEvent>(OnStethoscopeAction);
SubscribeLocalEvent<StethoscopeComponent, StethoscopeDoAfterEvent>(OnDoAfter);
}
private void OnGetActions(Entity<StethoscopeComponent> ent, ref GetItemActionsEvent args)
{
args.AddAction(ref ent.Comp.ActionEntity, ent.Comp.Action);
}
private void OnStethoscopeAction(Entity<StethoscopeComponent> ent, ref StethoscopeActionEvent args)
{
StartListening(ent, args.Target);
}
private void AddStethoscopeVerb(Entity<StethoscopeComponent> ent, ref InventoryRelayedEvent<GetVerbsEvent<InnateVerb>> args)
{
if (!args.Args.CanInteract || !args.Args.CanAccess)
return;
if (!HasComp<MobStateComponent>(args.Args.Target))
return;
var target = args.Args.Target;
InnateVerb verb = new()
{
Act = () => StartListening(ent, target),
Text = Loc.GetString("stethoscope-verb"),
IconEntity = GetNetEntity(ent),
Priority = 2,
};
args.Args.Verbs.Add(verb);
}
private void StartListening(Entity<StethoscopeComponent> ent, EntityUid target)
{
if (!_container.TryGetContainingContainer((ent, null, null), out var container))
return;
_doAfter.TryStartDoAfter(new DoAfterArgs(EntityManager, container.Owner, ent.Comp.Delay, new StethoscopeDoAfterEvent(), ent, target: target, used: ent)
{
DuplicateCondition = DuplicateConditions.SameEvent,
BreakOnMove = true,
Hidden = true,
BreakOnHandChange = false,
});
}
private void OnDoAfter(Entity<StethoscopeComponent> ent, ref StethoscopeDoAfterEvent args)
{
var target = args.Target;
if (args.Handled || target == null || args.Cancelled)
{
ent.Comp.LastMeasuredDamage = null;
return;
}
ExamineWithStethoscope(ent, args.Args.User, target.Value);
args.Repeat = true;
}
private void ExamineWithStethoscope(Entity<StethoscopeComponent> stethoscope, EntityUid user, EntityUid target)
{
// TODO: Add check for respirator component when it gets moved to shared.
// If the mob is dead or cannot asphyxiation damage, the popup shows nothing.
if (!TryComp<MobStateComponent>(target, out var mobState) ||
!TryComp<DamageableComponent>(target, out var damageComp) ||
_mobState.IsDead(target, mobState) ||
!damageComp.Damage.DamageDict.TryGetValue(DamageToListenFor, out var asphyxDmg))
{
_popup.PopupPredicted(Loc.GetString("stethoscope-nothing"), target, user);
stethoscope.Comp.LastMeasuredDamage = null;
return;
}
var absString = GetAbsoluteDamageString(asphyxDmg);
// Don't show the change if this is the first time listening.
if (stethoscope.Comp.LastMeasuredDamage == null)
{
_popup.PopupPredicted(absString, target, user);
}
else
{
var deltaString = GetDeltaDamageString(stethoscope.Comp.LastMeasuredDamage.Value, asphyxDmg);
_popup.PopupPredicted(Loc.GetString("stethoscope-combined-status", ("absolute", absString), ("delta", deltaString)), target, user);
}
stethoscope.Comp.LastMeasuredDamage = asphyxDmg;
}
private string GetAbsoluteDamageString(FixedPoint2 asphyxDmg)
{
var msg = (int) asphyxDmg switch
{
< 10 => "stethoscope-normal",
< 30 => "stethoscope-raggedy",
< 60 => "stethoscope-hyper",
< 80 => "stethoscope-irregular",
_ => "stethoscope-fucked",
};
return Loc.GetString(msg);
}
private string GetDeltaDamageString(FixedPoint2 lastDamage, FixedPoint2 currentDamage)
{
if (lastDamage > currentDamage)
return Loc.GetString("stethoscope-delta-improving");
if (lastDamage < currentDamage)
return Loc.GetString("stethoscope-delta-worsening");
return Loc.GetString("stethoscope-delta-steady");
}
}
public sealed partial class StethoscopeActionEvent : EntityTargetActionEvent;

View File

@@ -4,6 +4,4 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Medical;
[Serializable, NetSerializable]
public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent
{
}
public sealed partial class StethoscopeDoAfterEvent : SimpleDoAfterEvent;

View File

@@ -1,4 +1,5 @@
using Robust.Shared.GameStates;
using Robust.Shared.Map;
namespace Content.Shared.Movement.Components;
@@ -9,5 +10,10 @@ namespace Content.Shared.Movement.Components;
public sealed partial class ActiveJetpackComponent : Component
{
public float EffectCooldown = 0.3f;
public float MaxDistance = 0.7f;
public EntityCoordinates LastCoordinates;
public TimeSpan TargetTime = TimeSpan.Zero;
}

View File

@@ -32,7 +32,7 @@ namespace Content.Shared.Movement.Components
/// <summary>
/// Should our velocity be applied to our parent?
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("toParent")]
[DataField]
public bool ToParent = false;
public GameTick LastInputTick;
@@ -43,6 +43,12 @@ namespace Content.Shared.Movement.Components
public MoveButtons HeldMoveButtons = MoveButtons.None;
// I don't know if we even need this networked? It's mostly so conveyors can calculate properly.
/// <summary>
/// Direction to move this tick.
/// </summary>
public Vector2 WishDir;
/// <summary>
/// Entity our movement is relative to.
/// </summary>
@@ -65,7 +71,6 @@ namespace Content.Shared.Movement.Components
/// If we traverse on / off a grid then set a timer to update our relative inputs.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[ViewVariables(VVAccess.ReadWrite)]
public TimeSpan LerpTarget;
public const float LerpTime = 1.0f;

View File

@@ -155,7 +155,6 @@ public abstract partial class SharedMoverController : VirtualController
return;
}
UsedMobMovement[uid] = true;
// Specifically don't use mover.Owner because that may be different to the actual physics body being moved.
var weightless = _gravity.IsWeightless(physicsUid, physicsComponent, xform);
@@ -203,20 +202,21 @@ public abstract partial class SharedMoverController : VirtualController
var total = walkDir * walkSpeed + sprintDir * sprintSpeed;
var parentRotation = GetParentGridAngle(mover);
var worldTotal = _relativeMovement ? parentRotation.RotateVec(total) : total;
var wishDir = _relativeMovement ? parentRotation.RotateVec(total) : total;
DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), worldTotal.Length()));
DebugTools.Assert(MathHelper.CloseToPercent(total.Length(), wishDir.Length()));
var velocity = physicsComponent.LinearVelocity;
float friction;
float weightlessModifier;
float accel;
var velocity = physicsComponent.LinearVelocity;
// Whether we use weightless friction or not.
if (weightless)
{
if (gridComp == null && !MapGridQuery.HasComp(xform.GridUid))
friction = moveSpeedComponent?.OffGridFriction ?? MovementSpeedModifierComponent.DefaultOffGridFriction;
else if (worldTotal != Vector2.Zero && touching)
else if (wishDir != Vector2.Zero && touching)
friction = moveSpeedComponent?.WeightlessFriction ?? MovementSpeedModifierComponent.DefaultWeightlessFriction;
else
friction = moveSpeedComponent?.WeightlessFrictionNoInput ?? MovementSpeedModifierComponent.DefaultWeightlessFrictionNoInput;
@@ -226,7 +226,7 @@ public abstract partial class SharedMoverController : VirtualController
}
else
{
if (worldTotal != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
if (wishDir != Vector2.Zero || moveSpeedComponent?.FrictionNoInput == null)
{
friction = tileDef?.MobFriction ?? moveSpeedComponent?.Friction ?? MovementSpeedModifierComponent.DefaultFriction;
}
@@ -242,14 +242,27 @@ public abstract partial class SharedMoverController : VirtualController
var minimumFrictionSpeed = moveSpeedComponent?.MinimumFrictionSpeed ?? MovementSpeedModifierComponent.DefaultMinimumFrictionSpeed;
Friction(minimumFrictionSpeed, frameTime, friction, ref velocity);
if (worldTotal != Vector2.Zero)
wishDir *= weightlessModifier;
if (!weightless || touching)
Accelerate(ref velocity, in wishDir, accel, frameTime);
SetWishDir((uid, mover), wishDir);
PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
// Ensures that players do not spiiiiiiin
PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
// Handle footsteps at the end
if (total != Vector2.Zero)
{
if (!NoRotateQuery.HasComponent(uid))
{
// TODO apparently this results in a duplicate move event because "This should have its event run during
// island solver"??. So maybe SetRotation needs an argument to avoid raising an event?
var worldRot = _transform.GetWorldRotation(xform);
_transform.SetLocalRotation(xform, xform.LocalRotation + worldTotal.ToWorldAngle() - worldRot);
_transform.SetLocalRotation(xform, xform.LocalRotation + wishDir.ToWorldAngle() - worldRot);
}
if (!weightless && MobMoverQuery.TryGetComponent(uid, out var mobMover) &&
@@ -272,16 +285,23 @@ public abstract partial class SharedMoverController : VirtualController
}
}
}
}
worldTotal *= weightlessModifier;
public Vector2 GetWishDir(Entity<InputMoverComponent?> mover)
{
if (!MoverQuery.Resolve(mover.Owner, ref mover.Comp, false))
return Vector2.Zero;
if (!weightless || touching)
Accelerate(ref velocity, in worldTotal, accel, frameTime);
return mover.Comp.WishDir;
}
PhysicsSystem.SetLinearVelocity(physicsUid, velocity, body: physicsComponent);
public void SetWishDir(Entity<InputMoverComponent> mover, Vector2 wishDir)
{
if (mover.Comp.WishDir.Equals(wishDir))
return;
// Ensures that players do not spiiiiiiin
PhysicsSystem.SetAngularVelocity(physicsUid, 0, body: physicsComponent);
mover.Comp.WishDir = wishDir;
Dirty(mover);
}
public void LerpRotation(EntityUid uid, InputMoverComponent mover, float frameTime)
@@ -317,7 +337,7 @@ public abstract partial class SharedMoverController : VirtualController
}
}
private void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
public void Friction(float minimumFrictionSpeed, float frameTime, float friction, ref Vector2 velocity)
{
var speed = velocity.Length();
@@ -338,7 +358,10 @@ public abstract partial class SharedMoverController : VirtualController
velocity *= newSpeed;
}
private void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
/// <summary>
/// Adjusts the current velocity to the target velocity based on the specified acceleration.
/// </summary>
public static void Accelerate(ref Vector2 currentVelocity, in Vector2 velocity, float accel, float frameTime)
{
var wishDir = velocity != Vector2.Zero ? velocity.Normalized() : Vector2.Zero;
var wishSpeed = velocity.Length();

View File

@@ -2,124 +2,211 @@
using Content.Shared.Conveyor;
using Content.Shared.Gravity;
using Content.Shared.Magic;
using Content.Shared.Movement.Components;
using Content.Shared.Movement.Events;
using Content.Shared.Movement.Systems;
using Robust.Shared.Collections;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Physics;
using Robust.Shared.Physics.Components;
using Robust.Shared.Physics.Controllers;
using Robust.Shared.Physics.Events;
using Robust.Shared.Physics.Systems;
using Robust.Shared.Utility;
using Robust.Shared.Threading;
namespace Content.Shared.Physics.Controllers;
public abstract class SharedConveyorController : VirtualController
{
[Dependency] protected readonly IMapManager MapManager = default!;
[Dependency] private readonly IParallelManager _parallel = default!;
[Dependency] private readonly CollisionWakeSystem _wake = default!;
[Dependency] protected readonly EntityLookupSystem Lookup = default!;
[Dependency] private readonly SharedMapSystem _maps = default!;
[Dependency] protected readonly SharedPhysicsSystem Physics = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly FixtureSystem _fixtures = default!;
[Dependency] private readonly SharedGravitySystem _gravity = default!;
[Dependency] private readonly SharedMoverController _mover = default!;
protected const string ConveyorFixture = "conveyor";
private EntityQuery<MapGridComponent> _gridQuery;
private EntityQuery<TransformComponent> _xformQuery;
private ConveyorJob _job;
private ValueList<EntityUid> _ents = new();
private HashSet<Entity<ConveyorComponent>> _conveyors = new();
private EntityQuery<ConveyorComponent> _conveyorQuery;
private EntityQuery<ConveyedComponent> _conveyedQuery;
protected EntityQuery<PhysicsComponent> PhysicsQuery;
protected EntityQuery<TransformComponent> XformQuery;
protected HashSet<EntityUid> Intersecting = new();
public override void Initialize()
{
_gridQuery = GetEntityQuery<MapGridComponent>();
_xformQuery = GetEntityQuery<TransformComponent>();
_job = new ConveyorJob(this);
_conveyorQuery = GetEntityQuery<ConveyorComponent>();
_conveyedQuery = GetEntityQuery<ConveyedComponent>();
PhysicsQuery = GetEntityQuery<PhysicsComponent>();
XformQuery = GetEntityQuery<TransformComponent>();
UpdatesAfter.Add(typeof(SharedMoverController));
SubscribeLocalEvent<ConveyedComponent, TileFrictionEvent>(OnConveyedFriction);
SubscribeLocalEvent<ConveyedComponent, ComponentStartup>(OnConveyedStartup);
SubscribeLocalEvent<ConveyedComponent, ComponentShutdown>(OnConveyedShutdown);
SubscribeLocalEvent<ConveyorComponent, StartCollideEvent>(OnConveyorStartCollide);
SubscribeLocalEvent<ConveyorComponent, EndCollideEvent>(OnConveyorEndCollide);
SubscribeLocalEvent<ConveyorComponent, ComponentStartup>(OnConveyorStartup);
base.Initialize();
}
private void OnConveyorStartCollide(EntityUid uid, ConveyorComponent component, ref StartCollideEvent args)
private void OnConveyedFriction(Entity<ConveyedComponent> ent, ref TileFrictionEvent args)
{
// Conveyed entities don't get friction, they just get wishdir applied so will inherently slowdown anyway.
args.Modifier = 0f;
}
private void OnConveyedStartup(Entity<ConveyedComponent> ent, ref ComponentStartup args)
{
// We need waking / sleeping to work and don't want collisionwake interfering with us.
_wake.SetEnabled(ent.Owner, false);
}
private void OnConveyedShutdown(Entity<ConveyedComponent> ent, ref ComponentShutdown args)
{
_wake.SetEnabled(ent.Owner, true);
}
private void OnConveyorStartup(Entity<ConveyorComponent> ent, ref ComponentStartup args)
{
AwakenConveyor(ent.Owner);
}
/// <summary>
/// Forcefully awakens all entities near the conveyor.
/// </summary>
protected virtual void AwakenConveyor(Entity<TransformComponent?> ent)
{
}
/// <summary>
/// Wakes all conveyed entities contacting this conveyor.
/// </summary>
protected void WakeConveyed(EntityUid conveyorUid)
{
var contacts = PhysicsSystem.GetContacts(conveyorUid);
while (contacts.MoveNext(out var contact))
{
var other = contact.OtherEnt(conveyorUid);
if (_conveyedQuery.HasComp(other))
{
PhysicsSystem.WakeBody(other);
}
}
}
private void OnConveyorStartCollide(Entity<ConveyorComponent> conveyor, ref StartCollideEvent args)
{
var otherUid = args.OtherEntity;
if (!args.OtherFixture.Hard || args.OtherBody.BodyType == BodyType.Static || component.State == ConveyorState.Off)
if (!args.OtherFixture.Hard || args.OtherBody.BodyType == BodyType.Static)
return;
var conveyed = EnsureComp<ConveyedComponent>(otherUid);
if (conveyed.Colliding.Contains(uid))
return;
conveyed.Colliding.Add(uid);
Dirty(otherUid, conveyed);
}
private void OnConveyorEndCollide(Entity<ConveyorComponent> ent, ref EndCollideEvent args)
{
if (!TryComp(args.OtherEntity, out ConveyedComponent? conveyed))
return;
if (!conveyed.Colliding.Remove(ent.Owner))
return;
Dirty(args.OtherEntity, conveyed);
EnsureComp<ConveyedComponent>(otherUid);
}
public override void UpdateBeforeSolve(bool prediction, float frameTime)
{
base.UpdateBeforeSolve(prediction, frameTime);
var query = EntityQueryEnumerator<ConveyedComponent, TransformComponent, PhysicsComponent>();
_ents.Clear();
_job.Prediction = prediction;
_job.Conveyed.Clear();
while (query.MoveNext(out var uid, out var comp, out var xform, out var physics))
var query = EntityQueryEnumerator<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent>();
while (query.MoveNext(out var uid, out var comp, out var fixtures, out var physics, out var xform))
{
if (TryConvey((uid, comp, physics, xform), prediction, frameTime))
continue;
_ents.Add(uid);
_job.Conveyed.Add(((uid, comp, fixtures, physics, xform), Vector2.Zero, false));
}
foreach (var ent in _ents)
_parallel.ProcessNow(_job, _job.Conveyed.Count);
foreach (var ent in _job.Conveyed)
{
RemComp<ConveyedComponent>(ent);
if (!ent.Entity.Comp3.Predict && prediction)
continue;
var physics = ent.Entity.Comp3;
var velocity = physics.LinearVelocity;
var targetDir = ent.Direction;
// If mob is moving with the conveyor then combine the directions.
var wishDir = _mover.GetWishDir(ent.Entity.Owner);
if (Vector2.Dot(wishDir, targetDir) > 0f)
{
targetDir += wishDir;
}
if (ent.Result)
{
SetConveying(ent.Entity.Owner, ent.Entity.Comp1, targetDir.LengthSquared() > 0f);
// We apply friction here so when we push items towards the center of the conveyor they don't go overspeed.
// We also don't want this to apply to mobs as they apply their own friction and otherwise
// they'll go too slow.
if (!_mover.UsedMobMovement.TryGetValue(ent.Entity.Owner, out var usedMob) || !usedMob)
{
_mover.Friction(0f, frameTime: frameTime, friction: 5f, ref velocity);
}
SharedMoverController.Accelerate(ref velocity, targetDir, 20f, frameTime);
}
else if (!_mover.UsedMobMovement.TryGetValue(ent.Entity.Owner, out var usedMob) || !usedMob)
{
// Need friction to outweigh the movement as it will bounce a bit against the wall.
// This facilitates being able to sleep entities colliding into walls.
_mover.Friction(0f, frameTime: frameTime, friction: 40f, ref velocity);
}
PhysicsSystem.SetLinearVelocity(ent.Entity.Owner, velocity, wakeBody: false);
if (!IsConveyed((ent.Entity.Owner, ent.Entity.Comp2)))
{
RemComp<ConveyedComponent>(ent.Entity.Owner);
}
}
}
private bool TryConvey(Entity<ConveyedComponent, PhysicsComponent, TransformComponent> entity, bool prediction, float frameTime)
private void SetConveying(EntityUid uid, ConveyedComponent conveyed, bool value)
{
var physics = entity.Comp2;
var xform = entity.Comp3;
var contacting = entity.Comp1.Colliding.Count > 0;
if (conveyed.Conveying == value)
return;
if (!contacting)
return false;
conveyed.Conveying = value;
Dirty(uid, conveyed);
}
/// <summary>
/// Gets the conveying direction for an entity.
/// </summary>
/// <returns>False if we should no longer be considered actively conveyed.</returns>
private bool TryConvey(Entity<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent> entity,
bool prediction,
out Vector2 direction)
{
direction = Vector2.Zero;
var fixtures = entity.Comp2;
var physics = entity.Comp3;
var xform = entity.Comp4;
if (!physics.Awake)
return true;
// Client moment
if (!physics.Predict && prediction)
return true;
if (physics.BodyType == BodyType.Static)
return false;
if (!_gridQuery.TryComp(xform.GridUid, out var grid))
return true;
var gridTile = _maps.TileIndicesFor(xform.GridUid.Value, grid, xform.Coordinates);
_conveyors.Clear();
// Check for any conveyors on the attached tile.
Lookup.GetLocalEntitiesIntersecting(xform.GridUid.Value, gridTile, _conveyors);
DebugTools.Assert(_conveyors.Count <= 1);
// No more conveyors.
if (_conveyors.Count == 0)
if (xform.GridUid == null)
return true;
if (physics.BodyStatus == BodyStatus.InAir ||
@@ -130,48 +217,93 @@ public abstract class SharedConveyorController : VirtualController
Entity<ConveyorComponent> bestConveyor = default;
var bestSpeed = 0f;
var contacts = PhysicsSystem.GetContacts((entity.Owner, fixtures));
var transform = PhysicsSystem.GetPhysicsTransform(entity.Owner);
var anyConveyors = false;
foreach (var conveyor in _conveyors)
while (contacts.MoveNext(out var contact))
{
if (conveyor.Comp.Speed > bestSpeed && CanRun(conveyor))
if (!contact.IsTouching)
continue;
// Check if our center is over their fixture otherwise ignore it.
var other = contact.OtherEnt(entity.Owner);
// Check for blocked, if so then we can't convey at all and just try to sleep
// Otherwise we may just keep pushing it into the wall
if (!_conveyorQuery.TryComp(other, out var conveyor))
continue;
anyConveyors = true;
var otherFixture = contact.OtherFixture(entity.Owner);
var otherTransform = PhysicsSystem.GetPhysicsTransform(other);
// Check if our center is over the conveyor, otherwise ignore it.
if (!_fixtures.TestPoint(otherFixture.Item2.Shape, otherTransform, transform.Position))
continue;
if (conveyor.Speed > bestSpeed && CanRun(conveyor))
{
bestSpeed = conveyor.Comp.Speed;
bestConveyor = conveyor;
bestSpeed = conveyor.Speed;
bestConveyor = (other, conveyor);
}
}
// If we have no touching contacts we shouldn't be using conveyed anyway so nuke it.
if (!anyConveyors)
return true;
if (bestSpeed == 0f || bestConveyor == default)
return true;
var comp = bestConveyor.Comp!;
var conveyorXform = _xformQuery.GetComponent(bestConveyor.Owner);
var conveyorPos = conveyorXform.LocalPosition;
var conveyorRot = conveyorXform.LocalRotation;
var conveyorXform = XformQuery.GetComponent(bestConveyor.Owner);
var (conveyorPos, conveyorRot) = TransformSystem.GetWorldPositionRotation(conveyorXform);
conveyorRot += bestConveyor.Comp!.Angle;
if (comp.State == ConveyorState.Reverse)
conveyorRot += MathF.PI;
var direction = conveyorRot.ToWorldVec();
var conveyorDirection = conveyorRot.ToWorldVec();
direction = conveyorDirection;
var localPos = xform.LocalPosition;
var itemRelative = conveyorPos - localPos;
var itemRelative = conveyorPos - transform.Position;
direction = Convey(direction, bestSpeed, itemRelative);
localPos += Convey(direction, bestSpeed, frameTime, itemRelative);
// Do a final check for hard contacts so if we're conveying into a wall then NOOP.
contacts = PhysicsSystem.GetContacts((entity.Owner, fixtures));
TransformSystem.SetLocalPosition(entity, localPos, xform);
while (contacts.MoveNext(out var contact))
{
if (!contact.Hard || !contact.IsTouching)
continue;
// Force it awake for collisionwake reasons.
Physics.SetAwake((entity, physics), true);
Physics.SetSleepTime(physics, 0f);
var other = contact.OtherEnt(entity.Owner);
var otherBody = contact.OtherBody(entity.Owner);
// If the blocking body is dynamic then don't ignore it for this.
if (otherBody.BodyType != BodyType.Static)
continue;
var otherTransform = PhysicsSystem.GetPhysicsTransform(other);
var dotProduct = Vector2.Dot(otherTransform.Position - transform.Position, direction);
// TODO: This should probably be based on conveyor speed, this is mainly so we don't
// go to sleep when conveying and colliding with tables perpendicular to the conveyance direction.
if (dotProduct > 1.5f)
{
direction = Vector2.Zero;
return false;
}
}
return true;
}
private static Vector2 Convey(Vector2 direction, float speed, float frameTime, Vector2 itemRelative)
private static Vector2 Convey(Vector2 direction, float speed, Vector2 itemRelative)
{
if (speed == 0 || direction.Length() == 0)
if (speed == 0 || direction.LengthSquared() == 0)
return Vector2.Zero;
/*
@@ -190,15 +322,15 @@ public abstract class SharedConveyorController : VirtualController
if (r.Length() < 0.1)
{
var velocity = direction * speed;
return velocity * frameTime;
return velocity;
}
else
{
// Give a slight nudge in the direction of the conveyor to prevent
// to collidable objects (e.g. crates) on the locker from getting stuck
// pushing each other when rounding a corner.
var velocity = (r + direction*0.2f).Normalized() * speed;
return velocity * frameTime;
var velocity = (r + direction).Normalized() * speed;
return velocity;
}
}
@@ -206,4 +338,55 @@ public abstract class SharedConveyorController : VirtualController
{
return component.State != ConveyorState.Off && component.Powered;
}
private record struct ConveyorJob : IParallelRobustJob
{
public int BatchSize => 16;
public List<(Entity<ConveyedComponent, FixturesComponent, PhysicsComponent, TransformComponent> Entity, Vector2 Direction, bool Result)> Conveyed = new();
public SharedConveyorController System;
public bool Prediction;
public ConveyorJob(SharedConveyorController controller)
{
System = controller;
}
public void Execute(int index)
{
var convey = Conveyed[index];
var result = System.TryConvey(
(convey.Entity.Owner, convey.Entity.Comp1, convey.Entity.Comp2, convey.Entity.Comp3, convey.Entity.Comp4),
Prediction, out var direction);
Conveyed[index] = (convey.Entity, direction, result);
}
}
/// <summary>
/// Checks an entity's contacts to see if it's still being conveyed.
/// </summary>
private bool IsConveyed(Entity<FixturesComponent?> ent)
{
if (!Resolve(ent.Owner, ref ent.Comp))
return false;
var contacts = PhysicsSystem.GetContacts(ent.Owner);
while (contacts.MoveNext(out var contact))
{
if (!contact.IsTouching)
continue;
var other = contact.OtherEnt(ent.Owner);
if (_conveyorQuery.HasComp(other))
return true;
}
return false;
}
}

View File

@@ -39,6 +39,7 @@ public abstract partial class SharedProjectileSystem : EntitySystem
SubscribeLocalEvent<EmbeddableProjectileComponent, ThrowDoHitEvent>(OnEmbedThrowDoHit);
SubscribeLocalEvent<EmbeddableProjectileComponent, ActivateInWorldEvent>(OnEmbedActivate);
SubscribeLocalEvent<EmbeddableProjectileComponent, RemoveEmbeddedProjectileEvent>(OnEmbedRemove);
SubscribeLocalEvent<EmbeddableProjectileComponent, ComponentShutdown>(OnEmbeddableCompShutdown);
SubscribeLocalEvent<EmbeddedContainerComponent, EntityTerminatingEvent>(OnEmbeddableTermination);
}
@@ -75,6 +76,11 @@ public abstract partial class SharedProjectileSystem : EntitySystem
_hands.TryPickupAnyHand(args.User, embeddable);
}
private void OnEmbeddableCompShutdown(Entity<EmbeddableProjectileComponent> embeddable, ref ComponentShutdown arg)
{
EmbedDetach(embeddable, embeddable.Comp);
}
private void OnEmbedThrowDoHit(Entity<EmbeddableProjectileComponent> embeddable, ref ThrowDoHitEvent args)
{
if (!embeddable.Comp.EmbedOnThrow)
@@ -130,16 +136,21 @@ public abstract partial class SharedProjectileSystem : EntitySystem
if (!Resolve(uid, ref component))
return;
if (component.DeleteOnRemove)
{
QueueDel(uid);
return;
}
if (component.EmbeddedIntoUid is not null)
{
if (TryComp<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value, out var embeddedContainer))
{
embeddedContainer.EmbeddedObjects.Remove(uid);
Dirty(component.EmbeddedIntoUid.Value, embeddedContainer);
if (embeddedContainer.EmbeddedObjects.Count == 0)
RemCompDeferred<EmbeddedContainerComponent>(component.EmbeddedIntoUid.Value);
}
}
if (component.DeleteOnRemove && _net.IsServer)
{
QueueDel(uid);
return;
}
var xform = Transform(uid);

View File

@@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using Content.Shared.Actions.Events;
using Content.Shared.IdentityManagement;
using Content.Shared.Interaction.Events;
@@ -122,6 +121,14 @@ public abstract partial class SharedStationAiSystem
if (ev.Actor == ev.Target)
return;
// no need to show menu if device is not powered.
if (!PowerReceiver.IsPowered(ev.Target))
{
ShowDeviceNotRespondingPopup(ev.Actor);
ev.Cancel();
return;
}
if (TryComp(ev.Actor, out StationAiHeldComponent? aiComp) &&
(!TryComp(ev.Target, out StationAiWhitelistComponent? whitelistComponent) ||
!ValidateAi((ev.Actor, aiComp))))
@@ -150,7 +157,8 @@ public abstract partial class SharedStationAiSystem
private void OnTargetVerbs(Entity<StationAiWhitelistComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
{
if (!args.CanComplexInteract
|| !HasComp<StationAiHeldComponent>(args.User))
|| !HasComp<StationAiHeldComponent>(args.User)
|| !args.CanInteract)
{
return;
}
@@ -166,13 +174,6 @@ public abstract partial class SharedStationAiSystem
Text = isOpen ? Loc.GetString("ai-close") : Loc.GetString("ai-open"),
Act = () =>
{
// no need to show menu if device is not powered.
if (!PowerReceiver.IsPowered(ent.Owner))
{
ShowDeviceNotRespondingPopup(user);
return;
}
if (isOpen)
{
_uiSystem.CloseUi(ent.Owner, AiUi.Key, user);

View File

@@ -1,4 +1,6 @@
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
namespace Content.Shared.Sound.Components;
@@ -8,10 +10,9 @@ namespace Content.Shared.Sound.Components;
/// </summary>
public abstract partial class BaseEmitSoundComponent : Component
{
public static readonly AudioParams DefaultParams = AudioParams.Default.WithVolume(-2f);
[AutoNetworkedField]
[ViewVariables(VVAccess.ReadWrite)]
/// <summary>
/// The <see cref="SoundSpecifier"/> to play.
/// </summary>
[DataField(required: true)]
public SoundSpecifier? Sound;
@@ -22,3 +23,15 @@ public abstract partial class BaseEmitSoundComponent : Component
[DataField]
public bool Positional;
}
/// <summary>
/// Represents the state of <see cref="BaseEmitSoundComponent"/>.
/// </summary>
/// <remarks>This is obviously very cursed, but since the BaseEmitSoundComponent is abstract, we cannot network it.
/// AutoGenerateComponentState attribute won't work here, and since everything revolves around inheritance for some fucking reason,
/// there's no better way of doing this.</remarks>
[Serializable, NetSerializable]
public struct EmitSoundComponentState(SoundSpecifier? sound) : IComponentState
{
public SoundSpecifier? Sound { get; } = sound;
}

View File

@@ -17,6 +17,6 @@ public sealed partial class EmitSoundOnActivateComponent : BaseEmitSoundComponen
/// otherwise this might enable sound spamming, as use-delays are only initiated if the interaction was
/// handled.
/// </remarks>
[DataField("handle")]
[DataField]
public bool Handle = true;
}

View File

@@ -11,13 +11,12 @@ public sealed partial class EmitSoundOnCollideComponent : BaseEmitSoundComponent
/// <summary>
/// Minimum velocity required for the sound to play.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("minVelocity")]
[DataField("minVelocity")]
public float MinimumVelocity = 3f;
/// <summary>
/// To avoid sound spam add a cooldown to it.
/// </summary>
[ViewVariables(VVAccess.ReadWrite), DataField("nextSound", customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoPausedField]
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextSound;
}

View File

@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
/// Simple sound emitter that emits sound on entity drop
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnDropComponent : BaseEmitSoundComponent
{
}
public sealed partial class EmitSoundOnDropComponent : BaseEmitSoundComponent;

View File

@@ -1,5 +1,4 @@
using Content.Shared.Whitelist;
using Robust.Shared.Prototypes;
using Robust.Shared.GameStates;
namespace Content.Shared.Sound.Components;
@@ -10,6 +9,9 @@ namespace Content.Shared.Sound.Components;
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnInteractUsingComponent : BaseEmitSoundComponent
{
/// <summary>
/// The <see cref="EntityWhitelist"/> for the entities that can use this item.
/// </summary>
[DataField(required: true)]
public EntityWhitelist Whitelist = new();
}

View File

@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
/// Simple sound emitter that emits sound on LandEvent
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnLandComponent : BaseEmitSoundComponent
{
}
public sealed partial class EmitSoundOnLandComponent : BaseEmitSoundComponent;

View File

@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
/// Simple sound emitter that emits sound on entity pickup
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnPickupComponent : BaseEmitSoundComponent
{
}
public sealed partial class EmitSoundOnPickupComponent : BaseEmitSoundComponent;

View File

@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
/// Simple sound emitter that emits sound on entity spawn.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnSpawnComponent : BaseEmitSoundComponent
{
}
public sealed partial class EmitSoundOnSpawnComponent : BaseEmitSoundComponent;

View File

@@ -6,6 +6,4 @@ namespace Content.Shared.Sound.Components;
/// Simple sound emitter that emits sound on ThrowEvent
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class EmitSoundOnThrowComponent : BaseEmitSoundComponent
{
}
public sealed partial class EmitSoundOnThrowComponent : BaseEmitSoundComponent;

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