diff --git a/Content.Client/Actions/ClientActionsComponent.cs b/Content.Client/Actions/ClientActionsComponent.cs index 6df12b2581..3caab80cc6 100644 --- a/Content.Client/Actions/ClientActionsComponent.cs +++ b/Content.Client/Actions/ClientActionsComponent.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; using Content.Client.Actions.Assignments; using Content.Client.Actions.UI; using Content.Client.Hands; using Content.Client.Inventory; -using Content.Client.Items.UI; +using Content.Client.Items.Managers; using Content.Shared.Actions.Components; using Content.Shared.Actions.Prototypes; using Robust.Client.GameObjects; @@ -26,12 +25,13 @@ namespace Content.Client.Actions public const byte Slots = 10; [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; [ComponentDependency] private readonly HandsComponent? _handsComponent = null; [ComponentDependency] private readonly ClientInventoryComponent? _inventoryComponent = null; private ActionsUI? _ui; - private readonly List _highlightingItemSlots = new(); + private EntityUid _highlightedEntity; /// /// Current assignments for all hotbars / slots for this entity. @@ -225,26 +225,8 @@ namespace Content.Client.Actions { StopHighlightingItemSlots(); - // figure out if it's in hand or inventory and highlight it - foreach (var hand in _handsComponent!.Gui!.Hands) - { - if (hand.HeldItem != item || hand.HandButton == null) continue; - _highlightingItemSlots.Add(hand.HandButton); - hand.HandButton.Highlight(true); - return; - } - - foreach (var (slot, slotItem) in _inventoryComponent!.AllSlots) - { - if (slotItem != item) continue; - foreach (var itemSlotButton in - _inventoryComponent.InterfaceController.GetItemSlotButtons(slot)) - { - _highlightingItemSlots.Add(itemSlotButton); - itemSlotButton.Highlight(true); - } - return; - } + _highlightedEntity = item.Uid; + _itemSlotManager.HighlightEntity(item.Uid); } /// @@ -252,11 +234,11 @@ namespace Content.Client.Actions /// public void StopHighlightingItemSlots() { - foreach (var itemSlot in _highlightingItemSlots) - { - itemSlot.Highlight(false); - } - _highlightingItemSlots.Clear(); + if (_highlightedEntity == default) + return; + + _itemSlotManager.UnHighlightEntity(_highlightedEntity); + _highlightedEntity = default; } public void ToggleActionsMenu() diff --git a/Content.Client/Cloning/MagbootsComponent.cs b/Content.Client/Clothing/MagbootsComponent.cs similarity index 87% rename from Content.Client/Cloning/MagbootsComponent.cs rename to Content.Client/Clothing/MagbootsComponent.cs index dce398cf52..fb03315318 100644 --- a/Content.Client/Cloning/MagbootsComponent.cs +++ b/Content.Client/Clothing/MagbootsComponent.cs @@ -1,7 +1,7 @@ -using Content.Shared.Clothing; +using Content.Shared.Clothing; using Robust.Shared.GameObjects; -namespace Content.Client.Cloning +namespace Content.Client.Clothing { [RegisterComponent] public sealed class MagbootsComponent : SharedMagbootsComponent diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index b975c28673..95a7ec0596 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -81,7 +81,6 @@ namespace Content.Client.Entry factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); - factory.RegisterClass(); factory.RegisterClass(); factory.RegisterClass(); diff --git a/Content.Client/Hands/HandVirtualPullItemStatus.xaml b/Content.Client/Hands/HandVirtualPullItemStatus.xaml new file mode 100644 index 0000000000..9fac1e0d08 --- /dev/null +++ b/Content.Client/Hands/HandVirtualPullItemStatus.xaml @@ -0,0 +1,3 @@ + + diff --git a/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs b/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs new file mode 100644 index 0000000000..f4324f6e41 --- /dev/null +++ b/Content.Client/Hands/HandVirtualPullItemStatus.xaml.cs @@ -0,0 +1,13 @@ +using Robust.Client.UserInterface; +using Robust.Client.UserInterface.XAML; + +namespace Content.Client.Hands +{ + public sealed class HandVirtualPullItemStatus : Control + { + public HandVirtualPullItemStatus() + { + RobustXamlLoader.Load(this); + } + } +} diff --git a/Content.Client/Hands/HandsComponent.cs b/Content.Client/Hands/HandsComponent.cs index 0299384e33..4276ca1e9c 100644 --- a/Content.Client/Hands/HandsComponent.cs +++ b/Content.Client/Hands/HandsComponent.cs @@ -1,15 +1,8 @@ using System.Collections.Generic; -using Content.Client.Animations; -using Content.Client.HUD; using Content.Shared.Hands.Components; using Content.Shared.Item; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Network; -using Robust.Shared.Players; -using Robust.Shared.Timing; -using Robust.Shared.ViewVariables; namespace Content.Client.Hands { @@ -18,16 +11,7 @@ namespace Content.Client.Hands [ComponentReference(typeof(SharedHandsComponent))] public class HandsComponent : SharedHandsComponent { - [Dependency] private readonly IGameHud _gameHud = default!; - - [ViewVariables] - public HandsGui? Gui { get; private set; } - - protected override void OnRemove() - { - ClearGui(); - base.OnRemove(); - } + public HandsGui? Gui { get; set; } public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) { @@ -38,94 +22,23 @@ namespace Content.Client.Hands foreach (var handState in state.Hands) { - var newHand = new Hand(handState.Name, handState.Enabled, handState.Location); + var newHand = new Hand(handState.Name, handState.Location); Hands.Add(newHand); } + ActiveHand = state.ActiveHand; UpdateHandContainers(); UpdateHandVisualizer(); - UpdateHandsGuiState(); - } - - public void SettupGui() - { - if (Gui == null) - { - Gui = new HandsGui(); - _gameHud.HandsContainer.AddChild(Gui); - Gui.HandClick += args => OnHandClick(args.HandClicked); - Gui.HandActivate += args => OnActivateInHand(args.HandUsed); - UpdateHandsGuiState(); - } - } - - public void ClearGui() - { - Gui?.Dispose(); - Gui = null; - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel netChannel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, netChannel, session); - - switch (message) - { - case PickupAnimationMessage msg: - RunPickupAnimation(msg); - break; - } + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandsModifiedMessage { Hands = this }); } public override void HandsModified() { - base.HandsModified(); - UpdateHandContainers(); UpdateHandVisualizer(); - UpdateHandsGuiState(); - } - private void OnHandClick(string handClicked) - { - if (!TryGetHand(handClicked, out var pressedHand)) - return; - - if (!TryGetActiveHand(out var activeHand)) - return; - - var pressedEntity = pressedHand.HeldEntity; - var activeEntity = activeHand.HeldEntity; - - if (pressedHand == activeHand && activeEntity != null) - { - SendNetworkMessage(new UseInHandMsg()); //use item in hand - return; - } - - if (pressedHand != activeHand && pressedEntity == null) - { - SendNetworkMessage(new ClientChangedHandMsg(pressedHand.Name)); //swap hand - return; - } - - if (pressedHand != activeHand && pressedEntity != null && activeEntity != null) - { - SendNetworkMessage(new ClientAttackByInHandMsg(pressedHand.Name)); //use active item on held item - return; - } - - if (pressedHand != activeHand && pressedEntity != null && activeEntity == null) - { - SendNetworkMessage(new MoveItemFromHandMsg(pressedHand.Name)); //move item in hand to active hand - return; - } - } - - private void OnActivateInHand(string handActivated) - { - SendNetworkMessage(new ActivateInHandMsg(handActivated)); + base.HandsModified(); } public void UpdateHandContainers() @@ -149,27 +62,10 @@ namespace Content.Client.Hands appearance.SetData(HandsVisuals.VisualState, GetHandsVisualState()); } - public void UpdateHandsGuiState() - { - Gui?.SetState(GetHandsGuiState()); - } - - private HandsGuiState GetHandsGuiState() - { - var handStates = new List(); - - foreach (var hand in ReadOnlyHands) - { - var handState = new GuiHand(hand.Name, hand.Location, hand.HeldEntity, hand.Enabled); - handStates.Add(handState); - } - return new HandsGuiState(handStates, ActiveHand); - } - private HandsVisualState GetHandsVisualState() { var hands = new List(); - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { if (hand.HeldEntity == null) continue; @@ -182,16 +78,5 @@ namespace Content.Client.Hands } return new(hands); } - - private void RunPickupAnimation(PickupAnimationMessage msg) - { - if (!Owner.EntityManager.TryGetEntity(msg.EntityUid, out var entity)) - return; - - if (!IoCManager.Resolve().IsFirstTimePredicted) - return; - - ReusableAnimations.AnimateEntityPickup(entity, msg.InitialPosition, msg.PickupDirection); - } } } diff --git a/Content.Client/Hands/HandsGui.xaml b/Content.Client/Hands/HandsGui.xaml new file mode 100644 index 0000000000..d39c044821 --- /dev/null +++ b/Content.Client/Hands/HandsGui.xaml @@ -0,0 +1,6 @@ + + + + + + diff --git a/Content.Client/Hands/HandsGui.cs b/Content.Client/Hands/HandsGui.xaml.cs similarity index 68% rename from Content.Client/Hands/HandsGui.cs rename to Content.Client/Hands/HandsGui.xaml.cs index ff0fa47b23..63590882bf 100644 --- a/Content.Client/Hands/HandsGui.cs +++ b/Content.Client/Hands/HandsGui.xaml.cs @@ -1,87 +1,84 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Linq; using Content.Client.HUD; using Content.Client.Items.Managers; using Content.Client.Items.UI; using Content.Client.Resources; -using Content.Shared; using Content.Shared.CCVar; using Content.Shared.Hands.Components; -using Content.Shared.Input; +using Robust.Client.AutoGenerated; using Robust.Client.Graphics; -using Robust.Client.Player; using Robust.Client.ResourceManagement; using Robust.Client.UserInterface; -using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; using Robust.Shared.Configuration; using Robust.Shared.GameObjects; using Robust.Shared.Input; using Robust.Shared.IoC; using Robust.Shared.Timing; using Robust.Shared.ViewVariables; -using static Robust.Client.UserInterface.Controls.BoxContainer; namespace Content.Client.Hands { - public class HandsGui : Control + [GenerateTypedNameReferences] + public sealed partial class HandsGui : Control { [Dependency] private readonly IResourceCache _resourceCache = default!; [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; [Dependency] private readonly IGameHud _gameHud = default!; [Dependency] private readonly INetConfigurationManager _configManager = default!; + private readonly HandsSystem _handsSystem; + private readonly HandsComponent _handsComponent; + private Texture StorageTexture => _gameHud.GetHudTexture("back.png"); private Texture BlockedTexture => _resourceCache.GetTexture("/Textures/Interface/Inventory/blocked.png"); private ItemStatusPanel StatusPanel { get; } - private BoxContainer HandsContainer { get; } - - [ViewVariables] - public IReadOnlyList Hands => _hands; - private List _hands = new(); + [ViewVariables] private GuiHand[] _hands = Array.Empty(); private string? ActiveHand { get; set; } - public Action? HandClick; //TODO: Move to Eventbus - - public Action? HandActivate; //TODO: Move to Eventbus - - public HandsGui() + public HandsGui(HandsComponent hands, HandsSystem handsSystem) { - IoCManager.InjectDependencies(this); - _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); + _handsComponent = hands; + _handsSystem = handsSystem; - AddChild(new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - SeparationOverride = 0, - HorizontalAlignment = HAlignment.Center, - Children = - { - new BoxContainer - { - Orientation = LayoutOrientation.Vertical, - Children = - { - (StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle)), - (HandsContainer = new BoxContainer - { - Orientation = LayoutOrientation.Horizontal, - HorizontalAlignment = HAlignment.Center - }), - } - }, - } - }); + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + StatusPanel = ItemStatusPanel.FromSide(HandLocation.Middle); + StatusContainer.AddChild(StatusPanel); + StatusPanel.SetPositionFirst(); } - public void SetState(HandsGuiState state) + protected override void EnteredTree() { + base.EnteredTree(); + + _handsSystem.GuiStateUpdated += HandsSystemOnGuiStateUpdated; + _configManager.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); + + HandsSystemOnGuiStateUpdated(); + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _handsSystem.GuiStateUpdated -= HandsSystemOnGuiStateUpdated; + _configManager.UnsubValueChanged(CCVars.HudTheme, UpdateHudTheme); + } + + private void HandsSystemOnGuiStateUpdated() + { + var state = _handsSystem.GetGuiState(); + ActiveHand = state.ActiveHand; _hands = state.GuiHands; + Array.Sort(_hands, HandOrderComparer.Instance); UpdateGui(); } @@ -97,12 +94,15 @@ namespace Content.Client.Hands var handName = hand.Name; newButton.OnPressed += args => OnHandPressed(args, handName); - newButton.OnStoragePressed += args => OnStoragePressed(handName); - - newButton.Blocked.Visible = !hand.Enabled; + newButton.OnStoragePressed += _ => OnStoragePressed(handName); _itemSlotManager.SetItemSlot(newButton, hand.HeldItem); + + // Show blocked overlay if hand is pulling. + newButton.Blocked.Visible = + hand.HeldItem != null && hand.HeldItem.HasComponent(); } + if (TryGetActiveHand(out var activeHand)) { activeHand.HandButton.SetActiveHand(true); @@ -114,7 +114,7 @@ namespace Content.Client.Hands { if (args.Function == EngineKeyFunctions.UIClick) { - HandClick?.Invoke(new HandClickEventArgs(handName)); + _handsSystem.UIHandClick(_handsComponent, handName); } else if (TryGetHand(handName, out var hand)) { @@ -124,7 +124,7 @@ namespace Content.Client.Hands private void OnStoragePressed(string handName) { - HandActivate?.Invoke(new HandActivateEventArgs(handName)); + _handsSystem.UIHandActivate(handName); } private bool TryGetActiveHand([NotNullWhen(true)] out GuiHand? activeHand) @@ -145,6 +145,7 @@ namespace Content.Client.Hands if (hand.Name == handName) foundHand = hand; } + return foundHand != null; } @@ -153,7 +154,9 @@ namespace Content.Client.Hands base.FrameUpdate(args); foreach (var hand in _hands) + { _itemSlotManager.UpdateCooldown(hand.HandButton, hand.HeldItem); + } } private HandButton MakeHandButton(HandLocation buttonLocation) @@ -173,23 +176,31 @@ namespace Content.Client.Hands UpdateGui(); } - public class HandClickEventArgs + private sealed class HandOrderComparer : IComparer { - public string HandClicked { get; } + public static readonly HandOrderComparer Instance = new(); - public HandClickEventArgs(string handClicked) + public int Compare(GuiHand? x, GuiHand? y) { - HandClicked = handClicked; - } - } + if (ReferenceEquals(x, y)) return 0; + if (ReferenceEquals(null, y)) return 1; + if (ReferenceEquals(null, x)) return -1; - public class HandActivateEventArgs - { - public string HandUsed { get; } + var orderX = Map(x.HandLocation); + var orderY = Map(y.HandLocation); - public HandActivateEventArgs(string handUsed) - { - HandUsed = handUsed; + return orderX.CompareTo(orderY); + + static int Map(HandLocation loc) + { + return loc switch + { + HandLocation.Left => 3, + HandLocation.Middle => 2, + HandLocation.Right => 1, + _ => throw new ArgumentOutOfRangeException(nameof(loc), loc, null) + }; + } } } } @@ -203,7 +214,7 @@ namespace Content.Client.Hands /// The set of hands to be displayed. /// [ViewVariables] - public List GuiHands { get; } = new(); + public GuiHand[] GuiHands { get; } /// /// The name of the currently active hand. @@ -211,7 +222,7 @@ namespace Content.Client.Hands [ViewVariables] public string? ActiveHand { get; } - public HandsGuiState(List guiHands, string? activeHand = null) + public HandsGuiState(GuiHand[] guiHands, string? activeHand = null) { GuiHands = guiHands; ActiveHand = activeHand; @@ -247,18 +258,11 @@ namespace Content.Client.Hands [ViewVariables] public HandButton HandButton { get; set; } = default!; - /// - /// If this hand can be used by the player. - /// - [ViewVariables] - public bool Enabled { get; } - - public GuiHand(string name, HandLocation handLocation, IEntity? heldItem, bool enabled) + public GuiHand(string name, HandLocation handLocation, IEntity? heldItem) { Name = name; HandLocation = handLocation; HeldItem = heldItem; - Enabled = enabled; } } } diff --git a/Content.Client/Hands/HandsSystem.cs b/Content.Client/Hands/HandsSystem.cs deleted file mode 100644 index 4ba3a73c6f..0000000000 --- a/Content.Client/Hands/HandsSystem.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Content.Shared.Hands; -using Content.Shared.Hands.Components; -using Content.Shared.Input; -using Robust.Client.GameObjects; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.Input.Binding; -using Robust.Shared.Map; -using Robust.Shared.Players; - -namespace Content.Client.Hands -{ - internal sealed class HandsSystem : SharedHandsSystem - { - public override void Initialize() - { - base.Initialize(); - - SubscribeLocalEvent((_, component, _) => component.SettupGui()); - SubscribeLocalEvent((_, component, _) => component.ClearGui()); - - CommandBinds.Builder - .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(SwapHandsPressed)) - .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) - .Register(); - } - - public override void Shutdown() - { - CommandBinds.Unregister(); - base.Shutdown(); - } - - private void SwapHandsPressed(ICommonSession? session) - { - if (session == null) - return; - - var player = session.AttachedEntity; - - if (player == null) - return; - - if (!player.TryGetComponent(out SharedHandsComponent? hands)) - return; - - if (!hands.TryGetSwapHandsResult(out var nextHand)) - return; - - EntityManager.RaisePredictiveEvent(new RequestSetHandEvent(nextHand)); - } - - private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - if (session == null) - return false; - - var player = session.AttachedEntity; - - if (player == null) - return false; - - if (!player.TryGetComponent(out SharedHandsComponent? hands)) - return false; - - var activeHand = hands.ActiveHand; - - if (activeHand == null) - return false; - - EntityManager.RaisePredictiveEvent(new RequestDropHeldEntityEvent(activeHand, coords)); - return true; - } - - protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) - { - component.HandsModified(); - } - } -} diff --git a/Content.Client/Hands/Systems/HandVirtualPullSystem.cs b/Content.Client/Hands/Systems/HandVirtualPullSystem.cs new file mode 100644 index 0000000000..0f47efd9dc --- /dev/null +++ b/Content.Client/Hands/Systems/HandVirtualPullSystem.cs @@ -0,0 +1,18 @@ +using Content.Client.Items; +using Content.Shared.Hands.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Client.Hands +{ + [UsedImplicitly] + public sealed class HandVirtualPullSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + Subs.ItemStatus(_ => new HandVirtualPullItemStatus()); + } + } +} diff --git a/Content.Client/Hands/Systems/HandsSystem.cs b/Content.Client/Hands/Systems/HandsSystem.cs new file mode 100644 index 0000000000..5f2e40f2e5 --- /dev/null +++ b/Content.Client/Hands/Systems/HandsSystem.cs @@ -0,0 +1,149 @@ +using System; +using System.Linq; +using Content.Client.Animations; +using Content.Client.HUD; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using JetBrains.Annotations; +using Robust.Client.GameObjects; +using Robust.Client.Player; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; +using Robust.Shared.IoC; +using Robust.Shared.Timing; + +namespace Content.Client.Hands +{ + [UsedImplicitly] + public sealed class HandsSystem : SharedHandsSystem + { + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IGameHud _gameHud = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + + public event Action? GuiStateUpdated; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandlePlayerAttached); + SubscribeLocalEvent(HandlePlayerDetached); + SubscribeLocalEvent(HandleCompRemove); + SubscribeLocalEvent(HandleHandsModified); + + SubscribeNetworkEvent(HandlePickupAnimation); + } + + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + private void HandleHandsModified(HandsModifiedMessage ev) + { + if (ev.Hands.Owner == _playerManager.LocalPlayer?.ControlledEntity) + GuiStateUpdated?.Invoke(); + } + + protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) + { + if (uid == _playerManager.LocalPlayer?.ControlledEntity?.Uid) + GuiStateUpdated?.Invoke(); + } + + private void HandlePickupAnimation(PickupAnimationMessage msg) + { + if (!EntityManager.TryGetEntity(msg.EntityUid, out var entity)) + return; + + if (!_gameTiming.IsFirstTimePredicted) + return; + + ReusableAnimations.AnimateEntityPickup(entity, msg.InitialPosition, msg.PickupDirection); + } + + public HandsGuiState GetGuiState() + { + var player = _playerManager.LocalPlayer?.ControlledEntity; + + if (player == null || !player.TryGetComponent(out HandsComponent? hands)) + return new HandsGuiState(Array.Empty()); + + var states = hands.Hands + .Select(hand => new GuiHand(hand.Name, hand.Location, hand.HeldEntity)) + .ToArray(); + + return new HandsGuiState(states, hands.ActiveHand); + } + + public void UIHandClick(HandsComponent hands, string handName) + { + if (!hands.TryGetHand(handName, out var pressedHand)) + return; + + if (!hands.TryGetActiveHand(out var activeHand)) + return; + + var pressedEntity = pressedHand.HeldEntity; + var activeEntity = activeHand.HeldEntity; + + if (pressedHand == activeHand && activeEntity != null) + { + // use item in hand + // it will always be attack_self() in my heart. + RaiseNetworkEvent(new UseInHandMsg()); + return; + } + + if (pressedHand != activeHand && pressedEntity == null) + { + // change active hand + RaiseNetworkEvent(new RequestSetHandEvent(handName)); + return; + } + + if (pressedHand != activeHand && pressedEntity != null && activeEntity != null) + { + // use active item on held item + RaiseNetworkEvent(new ClientInteractUsingInHandMsg(pressedHand.Name)); + return; + } + + if (pressedHand != activeHand && pressedEntity != null && activeEntity == null) + { + // use active item on held item + RaiseNetworkEvent(new MoveItemFromHandMsg(pressedHand.Name)); + } + } + + public void UIHandActivate(string handName) + { + RaiseNetworkEvent (new ActivateInHandMsg(handName)); + } + + private void HandlePlayerAttached(EntityUid uid, HandsComponent component, PlayerAttachedEvent args) + { + component.Gui = new HandsGui(component, this); + _gameHud.HandsContainer.AddChild(component.Gui); + } + + private static void HandlePlayerDetached(EntityUid uid, HandsComponent component, PlayerDetachedEvent args) + { + ClearGui(component); + } + + private static void HandleCompRemove(EntityUid uid, HandsComponent component, ComponentRemove args) + { + ClearGui(component); + } + + private static void ClearGui(HandsComponent comp) + { + comp.Gui?.Orphan(); + comp.Gui = null; + } + } +} diff --git a/Content.Client/Items/ItemStatusMessages.cs b/Content.Client/Items/ItemStatusMessages.cs new file mode 100644 index 0000000000..93ebaeabe0 --- /dev/null +++ b/Content.Client/Items/ItemStatusMessages.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using Robust.Client.UserInterface; +using Robust.Shared.GameObjects; + +namespace Content.Client.Items +{ + public sealed class ItemStatusCollectMessage : EntityEventArgs + { + public List Controls = new(); + } + + public static class ItemStatusRegisterExt + { + /// + /// Register an item status control for a component. + /// + /// The handle from within entity system initialize. + /// A delegate to create the actual control. + /// The type of component for which this control should be made. + public static void ItemStatus( + this EntitySystem.Subscriptions subs, + Func createControl) + where TComp : IComponent + { + subs.SubscribeLocalEvent((uid, _, args) => + { + args.Controls.Add(createControl(uid)); + }); + } + } +} diff --git a/Content.Client/Items/Managers/IItemSlotManager.cs b/Content.Client/Items/Managers/IItemSlotManager.cs index 127eedc1b4..bbebaf55b4 100644 --- a/Content.Client/Items/Managers/IItemSlotManager.cs +++ b/Content.Client/Items/Managers/IItemSlotManager.cs @@ -1,4 +1,5 @@ -using Content.Client.Items.UI; +using System; +using Content.Client.Items.UI; using Robust.Client.UserInterface; using Robust.Shared.GameObjects; @@ -10,5 +11,21 @@ namespace Content.Client.Items.Managers void UpdateCooldown(ItemSlotButton? cooldownTexture, IEntity? entity); bool SetItemSlot(ItemSlotButton button, IEntity? entity); void HoverInSlot(ItemSlotButton button, IEntity? entity, bool fits); + event Action? EntityHighlightedUpdated; + bool IsHighlighted(EntityUid uid); + + /// + /// Highlight all slot controls that contain the specified entity. + /// + /// The UID of the entity to highlight. + /// + void HighlightEntity(EntityUid uid); + + /// + /// Remove highlighting for the specified entity. + /// + /// The UID of the entity to unhighlight. + /// + void UnHighlightEntity(EntityUid uid); } } diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index 5fd3f0fbe0..3435913914 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -1,8 +1,11 @@ +using System; +using System.Collections.Generic; using Content.Client.Examine; using Content.Client.Items.UI; using Content.Client.Storage; using Content.Client.Verbs; using Content.Shared.Cooldown; +using Content.Shared.Hands.Components; using Content.Shared.Input; using Robust.Client.GameObjects; using Robust.Client.Graphics; @@ -28,6 +31,11 @@ namespace Content.Client.Items.Managers [Dependency] private readonly IEntityManager _entityManager = default!; [Dependency] private readonly IEyeManager _eyeManager = default!; [Dependency] private readonly IMapManager _mapManager = default!; + [Dependency] private readonly IComponentManager _componentManager = default!; + + private readonly HashSet _highlightEntities = new(); + + public event Action? EntityHighlightedUpdated; public bool SetItemSlot(ItemSlotButton button, IEntity? entity) { @@ -38,13 +46,26 @@ namespace Content.Client.Items.Managers } else { - if (!entity.TryGetComponent(out ISpriteComponent? sprite)) + ISpriteComponent? sprite; + if (entity.TryGetComponent(out HandVirtualPullComponent? virtPull) + && _componentManager.TryGetComponent(virtPull.PulledEntity, out ISpriteComponent pulledSprite)) + { + sprite = pulledSprite; + } + else if (!entity.TryGetComponent(out sprite)) + { return false; + } button.ClearHover(); button.SpriteView.Sprite = sprite; button.StorageButton.Visible = entity.HasComponent(); } + + button.Entity = entity?.Uid ?? default; + + // im lazy + button.UpdateSlotHighlighted(); return true; } @@ -145,5 +166,38 @@ namespace Content.Client.Items.Managers button.HoverSpriteView.Sprite = hoverSprite; } + + public bool IsHighlighted(EntityUid uid) + { + return _highlightEntities.Contains(uid); + } + + public void HighlightEntity(EntityUid uid) + { + if (!_highlightEntities.Add(uid)) + return; + + EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, true)); + } + + public void UnHighlightEntity(EntityUid uid) + { + if (!_highlightEntities.Remove(uid)) + return; + + EntityHighlightedUpdated?.Invoke(new EntitySlotHighlightedEventArgs(uid, false)); + } + } + + public readonly struct EntitySlotHighlightedEventArgs + { + public EntitySlotHighlightedEventArgs(EntityUid entity, bool newHighlighted) + { + Entity = entity; + NewHighlighted = newHighlighted; + } + + public EntityUid Entity { get; } + public bool NewHighlighted { get; } } } diff --git a/Content.Client/Items/UI/ItemSlotButton.cs b/Content.Client/Items/UI/ItemSlotButton.cs index 8ffbdc5cf8..201fa28f5c 100644 --- a/Content.Client/Items/UI/ItemSlotButton.cs +++ b/Content.Client/Items/UI/ItemSlotButton.cs @@ -1,18 +1,24 @@ using System; using Content.Client.Cooldown; +using Content.Client.Items.Managers; using Content.Client.Stylesheets; using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; +using Robust.Shared.GameObjects; using Robust.Shared.Input; +using Robust.Shared.IoC; using Robust.Shared.Maths; namespace Content.Client.Items.UI { - public class ItemSlotButton : Control + public class ItemSlotButton : Control, IEntityEventSubscriber { private const string HighlightShader = "SelectionOutlineInrange"; + [Dependency] private readonly IItemSlotManager _itemSlotManager = default!; + + public EntityUid Entity { get; set; } public TextureRect Button { get; } public SpriteView SpriteView { get; } public SpriteView HoverSpriteView { get; } @@ -32,6 +38,8 @@ namespace Content.Client.Items.UI public ItemSlotButton(Texture texture, Texture storageTexture, string textureName) { + IoCManager.InjectDependencies(this); + MinSize = (64, 64); TextureName = textureName; @@ -101,6 +109,31 @@ namespace Content.Client.Items.UI }); } + protected override void EnteredTree() + { + base.EnteredTree(); + + _itemSlotManager.EntityHighlightedUpdated += HandleEntitySlotHighlighted; + UpdateSlotHighlighted(); + } + + protected override void ExitedTree() + { + base.ExitedTree(); + + _itemSlotManager.EntityHighlightedUpdated -= HandleEntitySlotHighlighted; + } + + private void HandleEntitySlotHighlighted(EntitySlotHighlightedEventArgs entitySlotHighlightedEventArgs) + { + UpdateSlotHighlighted(); + } + + public void UpdateSlotHighlighted() + { + Highlight(_itemSlotManager.IsHighlighted(Entity)); + } + public void ClearHover() { if (EntityHover) diff --git a/Content.Client/Items/UI/ItemStatusPanel.cs b/Content.Client/Items/UI/ItemStatusPanel.cs index d880d7ea49..d1dbd714c6 100644 --- a/Content.Client/Items/UI/ItemStatusPanel.cs +++ b/Content.Client/Items/UI/ItemStatusPanel.cs @@ -8,7 +8,9 @@ using Robust.Client.Graphics; using Robust.Client.UserInterface; using Robust.Client.UserInterface.Controls; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Maths; +using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; using static Content.Client.IoC.StaticIoC; @@ -18,6 +20,8 @@ namespace Content.Client.Items.UI { public class ItemStatusPanel : Control { + [Dependency] private readonly IEntityManager _entityManager = default!; + [ViewVariables] private readonly List<(IItemStatus, Control)> _activeStatusComponents = new(); @@ -33,6 +37,8 @@ namespace Content.Client.Items.UI public ItemStatusPanel(Texture texture, StyleBox.Margin cutout, StyleBox.Margin flat, Label.AlignMode textAlign) { + IoCManager.InjectDependencies(this); + var panel = new StyleBoxTexture { Texture = texture @@ -117,6 +123,13 @@ namespace Content.Client.Items.UI return new ItemStatusPanel(ResC.GetTexture(texture), cutOut, flat, textAlign); } + protected override void FrameUpdate(FrameEventArgs args) + { + base.FrameUpdate(args); + + UpdateItemName(); + } + public void Update(IEntity? entity) { if (entity == null) @@ -131,12 +144,29 @@ namespace Content.Client.Items.UI { _entity = entity; BuildNewEntityStatus(); - _itemNameLabel.Text = entity.Name; + + UpdateItemName(); } _panel.Visible = true; } + private void UpdateItemName() + { + if (_entity == null) + return; + + if (_entity.TryGetComponent(out HandVirtualPullComponent? virtualPull) + && _entityManager.TryGetEntity(virtualPull.PulledEntity, out var pulledEnt)) + { + _itemNameLabel.Text = pulledEnt.Name; + } + else + { + _itemNameLabel.Text = _entity.Name; + } + } + private void ClearOldStatus() { _statusContents.RemoveAllChildren(); @@ -162,6 +192,14 @@ namespace Content.Client.Items.UI _activeStatusComponents.Add((statusComponent, control)); } + + var collectMsg = new ItemStatusCollectMessage(); + _entity.EntityManager.EventBus.RaiseLocalEvent(_entity.Uid, collectMsg); + + foreach (var control in collectMsg.Controls) + { + _statusContents.AddChild(control); + } } } } diff --git a/Content.Client/Kitchen/KitchenSpikeComponent.cs b/Content.Client/Kitchen/Components/KitchenSpikeComponent.cs similarity index 88% rename from Content.Client/Kitchen/KitchenSpikeComponent.cs rename to Content.Client/Kitchen/Components/KitchenSpikeComponent.cs index efae8b89e6..58f271d63f 100644 --- a/Content.Client/Kitchen/KitchenSpikeComponent.cs +++ b/Content.Client/Kitchen/Components/KitchenSpikeComponent.cs @@ -2,7 +2,7 @@ using Content.Shared.DragDrop; using Content.Shared.Kitchen.Components; using Robust.Shared.GameObjects; -namespace Content.Client.Kitchen +namespace Content.Client.Kitchen.Components { [RegisterComponent] internal sealed class KitchenSpikeComponent : SharedKitchenSpikeComponent diff --git a/Content.Client/Kitchen/Components/MicrowaveComponent.cs b/Content.Client/Kitchen/Components/MicrowaveComponent.cs new file mode 100644 index 0000000000..862a8b6de0 --- /dev/null +++ b/Content.Client/Kitchen/Components/MicrowaveComponent.cs @@ -0,0 +1,17 @@ +using Content.Shared.Kitchen.Components; +using Content.Shared.Sound; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Serialization.Manager.Attributes; + +namespace Content.Client.Kitchen.Components +{ + [RegisterComponent] + public class MicrowaveComponent : SharedMicrowaveComponent + { + public IPlayingAudioStream? PlayingStream { get; set; } + + [DataField("loopingSound")] + public SoundSpecifier LoopingSound = new SoundPathSpecifier("/Audio/Machines/microwave_loop.ogg"); + } +} diff --git a/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs b/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs new file mode 100644 index 0000000000..6a18a0feac --- /dev/null +++ b/Content.Client/Kitchen/EntitySystems/MicrowaveSystem.cs @@ -0,0 +1,31 @@ +using System; +using Content.Client.Kitchen.Components; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.Player; + +namespace Content.Client.Kitchen.EntitySystems +{ + public class MicrowaveSystem : EntitySystem + { + public void StartSoundLoop(MicrowaveComponent microwave) + { + StopSoundLoop(microwave); + + microwave.PlayingStream = SoundSystem.Play(Filter.Local(), microwave.LoopingSound.GetSound(), microwave.Owner, + AudioParams.Default.WithAttenuation(1).WithMaxDistance(5).WithLoop(true)); + } + + public void StopSoundLoop(MicrowaveComponent microwave) + { + try + { + microwave.PlayingStream?.Stop(); + } + catch (Exception _) + { + // TODO: HOLY SHIT EXPOSE SOME DISPOSED PROPERTY ON PLAYING STREAM OR SOMETHING. + } + } + } +} diff --git a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs b/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs index 97d2bf0e7c..c0a3a68e8c 100644 --- a/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs +++ b/Content.Client/Kitchen/Visualizers/MicrowaveVisualizer.cs @@ -1,10 +1,10 @@ -using Content.Client.Sound; +using Content.Client.Kitchen.Components; +using Content.Client.Kitchen.EntitySystems; using Content.Shared.Kitchen.Components; using Content.Shared.Power; -using Content.Shared.Sound; using JetBrains.Annotations; using Robust.Client.GameObjects; -using Robust.Shared.Audio; +using Robust.Shared.GameObjects; using Robust.Shared.Log; namespace Content.Client.Kitchen.Visualizers @@ -17,35 +17,33 @@ namespace Content.Client.Kitchen.Visualizers base.OnChangeData(component); var sprite = component.Owner.GetComponent(); - var loopingSoundComponent = component.Owner.GetComponentOrNull(); + var microwaveComponent = component.Owner.GetComponentOrNull(); if (!component.TryGetData(PowerDeviceVisuals.VisualState, out MicrowaveVisualState state)) { state = MicrowaveVisualState.Idle; } + // The only reason we get the entity system so late is so that tests don't fail... Amazing, huh? switch (state) { case MicrowaveVisualState.Broken: sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mwb"); - loopingSoundComponent?.StopAllSounds(); + if(microwaveComponent != null) + EntitySystem.Get().StopSoundLoop(microwaveComponent); break; case MicrowaveVisualState.Idle: sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw"); sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_unlit"); - loopingSoundComponent?.StopAllSounds(); + if(microwaveComponent != null) + EntitySystem.Get().StopSoundLoop(microwaveComponent); break; case MicrowaveVisualState.Cooking: sprite.LayerSetState(MicrowaveVisualizerLayers.Base, "mw"); sprite.LayerSetState(MicrowaveVisualizerLayers.BaseUnlit, "mw_running_unlit"); - var audioParams = AudioParams.Default; - audioParams.Loop = true; - var scheduledSound = new ScheduledSound(); - scheduledSound.Filename = "/Audio/Machines/microwave_loop.ogg"; - scheduledSound.AudioParams = audioParams; - loopingSoundComponent?.StopAllSounds(); - loopingSoundComponent?.AddScheduledSound(scheduledSound); + if(microwaveComponent != null) + EntitySystem.Get().StartSoundLoop(microwaveComponent); break; default: diff --git a/Content.Client/Light/Components/ExpendableLightComponent.cs b/Content.Client/Light/Components/ExpendableLightComponent.cs index 5531379084..37eb37ee13 100644 --- a/Content.Client/Light/Components/ExpendableLightComponent.cs +++ b/Content.Client/Light/Components/ExpendableLightComponent.cs @@ -1,4 +1,5 @@ using Content.Shared.Light.Component; +using Robust.Shared.Audio; using Robust.Shared.GameObjects; namespace Content.Client.Light.Components @@ -9,6 +10,6 @@ namespace Content.Client.Light.Components [RegisterComponent] public class ExpendableLightComponent : SharedExpendableLightComponent { - + public IPlayingAudioStream? PlayingStream { get; set; } } } diff --git a/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs b/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs index 3a21d11b1a..59e11983e8 100644 --- a/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs +++ b/Content.Client/Light/Visualizers/ExpendableLightVisualizer.cs @@ -1,7 +1,10 @@ -using Content.Client.Light.Components; +using System; +using Content.Client.Light.Components; using Content.Shared.Light.Component; using JetBrains.Annotations; using Robust.Client.GameObjects; +using Robust.Shared.Audio; +using Robust.Shared.Player; namespace Content.Client.Light.Visualizers { @@ -17,7 +20,7 @@ namespace Content.Client.Light.Visualizers return; } - if (component.TryGetData(ExpendableLightVisuals.State, out string lightBehaviourID)) + if (component.TryGetData(ExpendableLightVisuals.Behavior, out string lightBehaviourID)) { if (component.Owner.TryGetComponent(out var lightBehaviour)) { @@ -33,6 +36,35 @@ namespace Content.Client.Light.Visualizers } } } + + void TryStopStream(IPlayingAudioStream? stream) + { + try + { + stream?.Stop(); + } + catch (Exception _) + { + // TODO: HOLY SHIT EXPOSE SOME DISPOSED PROPERTY ON PLAYING STREAM OR SOMETHING. + } + } + + if (component.TryGetData(ExpendableLightVisuals.State, out ExpendableLightState state) + && component.Owner.TryGetComponent(out var expendableLight)) + { + switch (state) + { + case ExpendableLightState.Lit: + TryStopStream(expendableLight.PlayingStream); + expendableLight.PlayingStream = SoundSystem.Play(Filter.Local(), expendableLight.LoopedSound, + expendableLight.Owner, SharedExpendableLightComponent.LoopedSoundParams.WithLoop(true)); + break; + + case ExpendableLightState.Dead: + TryStopStream(expendableLight.PlayingStream); + break; + } + } } } } diff --git a/Content.Client/Sound/LoopingSoundComponent.cs b/Content.Client/Sound/LoopingSoundComponent.cs deleted file mode 100644 index cd2d2e2812..0000000000 --- a/Content.Client/Sound/LoopingSoundComponent.cs +++ /dev/null @@ -1,105 +0,0 @@ -using System.Collections.Generic; -using Content.Shared.Physics; -using Content.Shared.Sound; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Players; -using Robust.Shared.Random; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Client.Sound -{ - [RegisterComponent] - public class LoopingSoundComponent : SharedLoopingSoundComponent - { - [Dependency] private readonly IRobustRandom _random = default!; - - private readonly Dictionary _audioStreams = new(); - - [DataField("schedules", true)] - private List _scheduledSounds - { - set => value.ForEach(AddScheduledSound); - get => new(); - } - - public override void StopAllSounds() - { - foreach (var kvp in _audioStreams) - { - kvp.Key.Play = false; - kvp.Value.Stop(); - } - _audioStreams.Clear(); - } - - public override void StopScheduledSound(string filename) - { - foreach (var kvp in _audioStreams) - { - if (kvp.Key.Filename != filename) continue; - kvp.Key.Play = false; - kvp.Value.Stop(); - _audioStreams.Remove(kvp.Key); - } - } - - public override void AddScheduledSound(ScheduledSound schedule) - { - Play(schedule); - } - - public void Play(ScheduledSound schedule) - { - if (!schedule.Play) return; - - Owner.SpawnTimer((int) schedule.Delay + (_random.Next((int) schedule.RandomDelay)),() => - { - if (!schedule.Play) return; // We make sure this hasn't changed. - - if (!_audioStreams.ContainsKey(schedule)) - { - _audioStreams.Add(schedule, SoundSystem.Play(Filter.Local(), schedule.Filename, Owner, schedule.AudioParams)!); - } - else - { - _audioStreams[schedule] = SoundSystem.Play(Filter.Local(), schedule.Filename, Owner, schedule.AudioParams)!; - } - - if (schedule.Times == 0) return; - - if (schedule.Times > 0) schedule.Times--; - - Play(schedule); - }); - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - switch (message) - { - case ScheduledSoundMessage msg: - AddScheduledSound(msg.Schedule); - break; - - case StopSoundScheduleMessage msg: - StopScheduledSound(msg.Filename); - break; - - case StopAllSoundsMessage _: - StopAllSounds(); - break; - } - } - - protected override void Initialize() - { - base.Initialize(); - SoundSystem.OcclusionCollisionMask = (int) CollisionGroup.Impassable; - } - } -} diff --git a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs index 497e735a82..82bdf16910 100644 --- a/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs +++ b/Content.IntegrationTests/Tests/GameObjects/Components/Mobs/ActionsComponentTests.cs @@ -52,7 +52,6 @@ namespace Content.IntegrationTests.Tests.GameObjects.Components.Mobs - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Content.Server/Body/Behavior/LiverBehavior.cs b/Content.Server/Body/Behavior/LiverBehavior.cs index 82e12b29ce..95897b320a 100644 --- a/Content.Server/Body/Behavior/LiverBehavior.cs +++ b/Content.Server/Body/Behavior/LiverBehavior.cs @@ -17,6 +17,11 @@ namespace Content.Server.Body.Behavior private float _accumulatedFrameTime; + /// + /// Delay time that determines how often to metabolise blood contents (in seconds). + /// + private float _updateIntervalSeconds = 1.0f; + /// /// Whether the liver is functional. /// @@ -63,13 +68,13 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime += frameTime; - // Update at most once per second - if (_accumulatedFrameTime < 1) + // Update at most once every _updateIntervalSeconds + if (_accumulatedFrameTime < _updateIntervalSeconds) { return; } - _accumulatedFrameTime -= 1; + _accumulatedFrameTime -= _updateIntervalSeconds; if (!Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { @@ -90,6 +95,10 @@ namespace Content.Server.Body.Behavior continue; } + // How much reagent is available to metabolise? + // This needs to be passed to other functions that have metabolism rate information, such that they don't "overmetabolise" a reagent. + var availableReagent = bloodstream.Solution.Solution.GetReagentQuantity(reagent.ReagentId); + //TODO BODY Check if it's a Toxin. If volume < _toxinTolerance, just remove it. If greater, add damage = volume * _toxinLethality //TODO BODY Check if it has BoozePower > 0. Affect drunkenness, apply damage. Proposed formula (SS13-derived): damage = sqrt(volume) * BoozePower^_alcoholExponent * _alcoholLethality / 10 //TODO BODY Liver failure. @@ -99,8 +108,9 @@ namespace Content.Server.Body.Behavior // Run metabolism code for each reagent foreach (var metabolizable in prototype.Metabolism) { - var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, frameTime); + var reagentDelta = metabolizable.Metabolize(Body.Owner, reagent.ReagentId, _updateIntervalSeconds, availableReagent); bloodstream.Solution.TryRemoveReagent(reagent.ReagentId, reagentDelta); + availableReagent -= reagentDelta; } } } diff --git a/Content.Server/Body/Behavior/StomachBehavior.cs b/Content.Server/Body/Behavior/StomachBehavior.cs index a6c903efe9..813b8a3a5e 100644 --- a/Content.Server/Body/Behavior/StomachBehavior.cs +++ b/Content.Server/Body/Behavior/StomachBehavior.cs @@ -30,6 +30,8 @@ namespace Content.Server.Body.Behavior /// public override void Update(float frameTime) { + + // Do not metabolise if the organ does not have a body. if (Body == null) { return; @@ -45,7 +47,9 @@ namespace Content.Server.Body.Behavior _accumulatedFrameTime -= 1; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solution) || + // Note that "Owner" should be the organ that has this behaviour/mechanism, and it should have a dedicated + // solution container. "Body.Owner" is something else, and may have more than one solution container. + if (!Owner.TryGetComponent(out SolutionContainerComponent? solution) || !Body.Owner.TryGetComponent(out BloodstreamComponent? bloodstream)) { return; @@ -61,8 +65,19 @@ namespace Content.Server.Body.Behavior delta.Increment(1); if (delta.Lifetime > _digestionDelay) { - solution.TryRemoveReagent(delta.ReagentId, delta.Quantity); - transferSolution.AddReagent(delta.ReagentId, delta.Quantity); + // This reagent has been in the somach long enough, TRY to transfer it. + // But first, check if the reagent still exists, and how much is left. + // Some poor spessman may have washed down a potassium snack with some water. + if (solution.Solution.ContainsReagent(delta.ReagentId, out ReagentUnit quantity)){ + + if (quantity > delta.Quantity) { + quantity = delta.Quantity; + } + + solution.TryRemoveReagent(delta.ReagentId, quantity); + transferSolution.AddReagent(delta.ReagentId, quantity); + } + _reagentDeltas.Remove(delta); } } @@ -133,10 +148,10 @@ namespace Content.Server.Body.Behavior public bool TryTransferSolution(Solution solution) { - if (Body == null || !CanTransferSolution(solution)) + if (Owner == null || !CanTransferSolution(solution)) return false; - if (!Body.Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) + if (!Owner.TryGetComponent(out SolutionContainerComponent? solutionComponent)) { return false; } diff --git a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs index 3b32776c85..255200f103 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultDrink.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultDrink.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,28 +9,27 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for drink reagents. Attempts to find a ThirstComponent on the target, - /// and to update it's thirst values. + /// and to update it's thirst values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultDrink : IMetabolizable + public class DefaultDrink : DefaultMetabolizable { - //Rate of metabolism in units / second - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - //How much thirst is satiated when 1u of the reagent is metabolized [DataField("hydrationFactor")] public float HydrationFactor { get; set; } = 30.0f; //Remove reagent at set rate, satiate thirst if a ThirstComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - var metabolismAmount = MetabolismRate * tickTime; + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a ThirstComponent, hydrate them. if (solutionEntity.TryGetComponent(out ThirstComponent? thirst)) - thirst.UpdateThirst(metabolismAmount.Float() * HydrationFactor); + thirst.UpdateThirst(amountMetabolized.Float() * HydrationFactor); //Return amount of reagent to be removed, remove reagent regardless of ThirstComponent presence - return metabolismAmount; + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/DefaultFood.cs b/Content.Server/Chemistry/Metabolism/DefaultFood.cs index aeba8ed95d..fafcf58dd1 100644 --- a/Content.Server/Chemistry/Metabolism/DefaultFood.cs +++ b/Content.Server/Chemistry/Metabolism/DefaultFood.cs @@ -1,4 +1,4 @@ -using Content.Server.Nutrition.Components; +using Content.Server.Nutrition.Components; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Metabolizable; using Content.Shared.Chemistry.Reagent; @@ -9,30 +9,30 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for food reagents. Attempts to find a HungerComponent on the target, - /// and to update it's hunger values. + /// and to update it's hunger values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class DefaultFood : IMetabolizable + public class DefaultFood : DefaultMetabolizable { - /// - /// Rate of metabolism in units / second - /// - [DataField("rate")] public ReagentUnit MetabolismRate { get; private set; } = ReagentUnit.New(1.0); /// /// How much hunger is satiated when 1u of the reagent is metabolized /// [DataField("nutritionFactor")] public float NutritionFactor { get; set; } = 30.0f; - //Remove reagent at set rate, satiate hunger if a HungerComponent can be found - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) - { - var metabolismAmount = MetabolismRate * tickTime; - if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) - hunger.UpdateFood(metabolismAmount.Float() * NutritionFactor); - //Return amount of reagent to be removed, remove reagent regardless of HungerComponent presence - return metabolismAmount; + //Remove reagent at set rate, satiate hunger if a HungerComponent can be found + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) + { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // If metabolizing entity has a HungerComponent, feed them. + if (solutionEntity.TryGetComponent(out HungerComponent? hunger)) + hunger.UpdateFood(amountMetabolized.Float() * NutritionFactor); + + //Return amount of reagent to be removed. Reagent is removed regardless of HungerComponent presence + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs index 45bbff68d7..bbf2534505 100644 --- a/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs +++ b/Content.Server/Chemistry/Metabolism/HealthChangeMetabolism.cs @@ -10,16 +10,11 @@ namespace Content.Server.Chemistry.Metabolism { /// /// Default metabolism for medicine reagents. Attempts to find a DamageableComponent on the target, - /// and to update its damage values. + /// and to update its damage values. Inherits metabolisation rate logic from DefaultMetabolizable. /// [DataDefinition] - public class HealthChangeMetabolism : IMetabolizable + public class HealthChangeMetabolism : DefaultMetabolizable { - /// - /// How much of the reagent should be metabolized each sec. - /// - [DataField("rate")] - public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); /// /// How much damage is changed when 1u of the reagent is metabolized. @@ -41,14 +36,23 @@ namespace Content.Server.Chemistry.Metabolism /// /// /// + /// Reagent available to be metabolized. /// - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public override ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { + // use DefaultMetabolism to determine how much reagent we should metabolize + var amountMetabolized = base.Metabolize(solutionEntity, reagentId, tickTime, availableReagent); + + // how much does this much reagent heal for + var healthChangeAmount = HealthChange * amountMetabolized.Float(); + if (solutionEntity.TryGetComponent(out IDamageableComponent? health)) { - health.ChangeDamage(DamageType, (int)HealthChange, true); - float decHealthChange = (float) (HealthChange - (int) HealthChange); - _accumulatedHealth += decHealthChange; + // Heal damage by healthChangeAmmount, rounding down to nearest integer + health.ChangeDamage(DamageType, (int) healthChangeAmount, true); + + // Store decimal remainder of healthChangeAmmount in _accumulatedHealth + _accumulatedHealth += (healthChangeAmount - (int) healthChangeAmount); if (_accumulatedHealth >= 1) { @@ -62,7 +66,7 @@ namespace Content.Server.Chemistry.Metabolism _accumulatedHealth += 1; } } - return MetabolismRate; + return amountMetabolized; } } } diff --git a/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs index 3218e4f983..f509fa0d56 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/AddToSolutionReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class AddToSolutionReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs index 9078eef2d1..860df2c423 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/ExtinguishReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class ExtinguishReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs index 5242565e0e..8c71a8904d 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/FlammableReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class FlammableReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs b/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs index 44774ffcc9..8005384562 100644 --- a/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs +++ b/Content.Server/Chemistry/ReagentEntityReactions/WashCreamPieReaction.cs @@ -10,7 +10,6 @@ using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototy namespace Content.Server.Chemistry.ReagentEntityReactions { [UsedImplicitly] - [DataDefinition] public class WashCreamPieReaction : ReagentEntityReaction { [DataField("reagents", true, customTypeSerializer:typeof(PrototypeIdHashSetSerializer))] diff --git a/Content.Server/Fluids/Components/SpillableComponent.cs b/Content.Server/Fluids/Components/SpillableComponent.cs index 79e1f267a7..bd5dca6d3d 100644 --- a/Content.Server/Fluids/Components/SpillableComponent.cs +++ b/Content.Server/Fluids/Components/SpillableComponent.cs @@ -2,6 +2,7 @@ using Content.Shared.ActionBlocker; using Content.Shared.Chemistry.Reagent; using Content.Shared.Chemistry.Solution.Components; using Content.Shared.DragDrop; +using Content.Shared.Interaction; using Content.Shared.Interaction.Events; using Content.Shared.Notification.Managers; using Content.Shared.Verbs; diff --git a/Content.Server/Hands/Components/HandsComponent.cs b/Content.Server/Hands/Components/HandsComponent.cs index 45eda82c41..c6e2ac4896 100644 --- a/Content.Server/Hands/Components/HandsComponent.cs +++ b/Content.Server/Hands/Components/HandsComponent.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -10,7 +11,6 @@ using Content.Shared.Audio; using Content.Shared.Body.Part; using Content.Shared.Hands.Components; using Content.Shared.Notification.Managers; -using Content.Shared.Physics.Pull; using Content.Shared.Pulling.Components; using Content.Shared.Sound; using Robust.Server.GameObjects; @@ -20,9 +20,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Map; -using Robust.Shared.Network; using Robust.Shared.Player; -using Robust.Shared.Players; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Server.Hands.Components @@ -39,48 +37,6 @@ namespace Content.Server.Hands.Components int IDisarmedAct.Priority => int.MaxValue; // We want this to be the last disarm act to run. - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - switch (message) - { - case PullAttemptMessage msg: - AttemptPull(msg); - break; - case PullStartedMessage: - StartPulling(); - break; - case PullStoppedMessage: - StopPulling(); - break; - } - } - - public override void HandleNetworkMessage(ComponentMessage message, INetChannel channel, ICommonSession? session = null) - { - base.HandleNetworkMessage(message, channel, session); - - switch (message) - { - case ClientChangedHandMsg msg: - ActiveHand = msg.HandName; - break; - case ClientAttackByInHandMsg msg: - InteractHandWithActiveHand(msg.HandName); - break; - case UseInHandMsg: - UseActiveHeldEntity(); - break; - case ActivateInHandMsg msg: - ActivateHeldEntity(msg.HandName); - break; - case MoveItemFromHandMsg msg: - TryMoveHeldEntityToActiveHand(msg.HandName); - break; - } - } - protected override void OnHeldEntityRemovedFromHand(IEntity heldEntity, HandState handState) { if (heldEntity.TryGetComponent(out ItemComponent? item)) @@ -145,7 +101,8 @@ namespace Content.Server.Hands.Components if (pickupDirection == initialPosition.ToMapPos(Owner.EntityManager)) return; - SendNetworkMessage(new PickupAnimationMessage(entity.Uid, pickupDirection, initialPosition)); + Owner.EntityManager.EntityNetManager!.SendSystemNetworkMessage( + new PickupAnimationMessage(entity.Uid, pickupDirection, initialPosition)); } #region Pull/Disarm @@ -155,9 +112,17 @@ namespace Content.Server.Hands.Components if (args.Part.PartType != BodyPartType.Hand) return; - var handLocation = ReadOnlyHands.Count == 0 ? HandLocation.Right : HandLocation.Left; //TODO: make hand body part have a handlocation? + // If this annoys you, which it should. + // Ping Smugleaf. + var location = args.Part.Symmetry switch + { + BodyPartSymmetry.None => HandLocation.Middle, + BodyPartSymmetry.Left => HandLocation.Left, + BodyPartSymmetry.Right => HandLocation.Right, + _ => throw new ArgumentOutOfRangeException() + }; - AddHand(args.Slot, handLocation); + AddHand(args.Slot, location); } void IBodyPartRemoved.BodyPartRemoved(BodyPartRemovedEventArgs args) @@ -209,41 +174,13 @@ namespace Content.Server.Hands.Components return pullable.TryStopPull(); } - private void AttemptPull(PullAttemptMessage msg) - { - if (!ReadOnlyHands.Any(hand => hand.Enabled)) - { - msg.Cancelled = true; - } - } - - private void StartPulling() - { - var firstFreeHand = Hands.FirstOrDefault(hand => hand.Enabled); - - if (firstFreeHand == null) - return; - - DisableHand(firstFreeHand); - } - - private void StopPulling() - { - var firstOccupiedHand = Hands.FirstOrDefault(hand => !hand.Enabled); - - if (firstOccupiedHand == null) - return; - - EnableHand(firstOccupiedHand); - } - #endregion #region Old public methods - public IEnumerable HandNames => ReadOnlyHands.Select(h => h.Name); + public IEnumerable HandNames => Hands.Select(h => h.Name); - public int Count => ReadOnlyHands.Count; + public int Count => Hands.Count; /// /// Returns a list of all hand names, with the active hand being first. @@ -253,9 +190,9 @@ namespace Content.Server.Hands.Components if (ActiveHand != null) yield return ActiveHand; - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { - if (hand.Name == ActiveHand || !hand.Enabled) + if (hand.Name == ActiveHand) continue; yield return hand.Name; diff --git a/Content.Server/Hands/Systems/HandVirtualPullSystem.cs b/Content.Server/Hands/Systems/HandVirtualPullSystem.cs new file mode 100644 index 0000000000..f95a4747ca --- /dev/null +++ b/Content.Server/Hands/Systems/HandVirtualPullSystem.cs @@ -0,0 +1,57 @@ +using Content.Server.Pulling; +using Content.Shared.Hands; +using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Server.Hands +{ + [UsedImplicitly] + public sealed class HandVirtualPullSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(HandlePullerDropped); + SubscribeLocalEvent(HandlePullerUnequipped); + + SubscribeLocalEvent(HandleBeforeInteract); + } + + private static void HandleBeforeInteract( + EntityUid uid, + HandVirtualPullComponent component, + BeforeInteractEvent args) + { + // No interactions with a virtual pull, please. + args.Handled = true; + } + + // If the virtual pull gets removed from the hands for any reason, cancel the pull and delete it. + private void HandlePullerUnequipped(EntityUid uid, HandVirtualPullComponent component, UnequippedHandEvent args) + { + MaybeDelete(component, args.User); + } + + private void HandlePullerDropped(EntityUid uid, HandVirtualPullComponent component, DroppedEvent args) + { + MaybeDelete(component, args.User); + } + + private void MaybeDelete(HandVirtualPullComponent comp, IEntity? user) + { + var pulled = comp.PulledEntity; + + if (!ComponentManager.TryGetComponent(pulled, out PullableComponent? pullable)) + return; + + if (pullable.Puller != user) + return; + + pullable.TryStopPull(user); + comp.Owner.QueueDelete(); + } + } +} diff --git a/Content.Server/Hands/HandsSystem.cs b/Content.Server/Hands/Systems/HandsSystem.cs similarity index 58% rename from Content.Server/Hands/HandsSystem.cs rename to Content.Server/Hands/Systems/HandsSystem.cs index 4a76d07101..7e6adca66c 100644 --- a/Content.Server/Hands/HandsSystem.cs +++ b/Content.Server/Hands/Systems/HandsSystem.cs @@ -13,9 +13,9 @@ using Content.Shared.Hands; using Content.Shared.Hands.Components; using Content.Shared.Input; using Content.Shared.Notification.Managers; +using Content.Shared.Physics.Pull; using JetBrains.Annotations; using Robust.Server.Player; -using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Input.Binding; using Robust.Shared.IoC; @@ -23,6 +23,7 @@ using Robust.Shared.Localization; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Players; +using Robust.Shared.Utility; using static Content.Shared.Inventory.EquipmentSlotDefines; namespace Content.Server.Hands @@ -38,15 +39,126 @@ namespace Content.Server.Hands base.Initialize(); SubscribeLocalEvent(HandleExamined); + SubscribeNetworkEvent(HandleActivateInHand); + SubscribeNetworkEvent(HandleInteractUsingInHand); + SubscribeNetworkEvent(HandleUseInHand); + SubscribeNetworkEvent(HandleMoveItemFromHand); + + SubscribeLocalEvent(HandlePullAttempt); + SubscribeLocalEvent(HandlePullStarted); + SubscribeLocalEvent(HandlePullStopped); CommandBinds.Builder .Bind(ContentKeyFunctions.ActivateItemInHand, InputCmdHandler.FromDelegate(HandleActivateItem)) .Bind(ContentKeyFunctions.ThrowItemInHand, new PointerInputCmdHandler(HandleThrowItem)) .Bind(ContentKeyFunctions.SmartEquipBackpack, InputCmdHandler.FromDelegate(HandleSmartEquipBackpack)) .Bind(ContentKeyFunctions.SmartEquipBelt, InputCmdHandler.FromDelegate(HandleSmartEquipBelt)) + .Bind(ContentKeyFunctions.SwapHands, InputCmdHandler.FromDelegate(SwapHandsPressed, handle: false)) + .Bind(ContentKeyFunctions.Drop, new PointerInputCmdHandler(DropPressed)) .Register(); } + private static void HandlePullAttempt(EntityUid uid, HandsComponent component, PullAttemptMessage args) + { + // Cancel pull if all hands full. + if (component.Hands.All(hand => !hand.IsEmpty)) + args.Cancelled = true; + } + + private void HandlePullStarted(EntityUid uid, HandsComponent component, PullStartedMessage args) + { + foreach (var handName in component.ActivePriorityEnumerable()) + { + var hand = component.GetHand(handName); + if (!hand.IsEmpty) + continue; + + var pos = component.Owner.Transform.Coordinates; + var virtualPull = EntityManager.SpawnEntity("HandVirtualPull", pos); + var virtualPullComp = virtualPull.GetComponent(); + virtualPullComp.PulledEntity = args.Pulled.Owner.Uid; + component.PutEntityIntoHand(hand, virtualPull); + return; + } + + DebugTools.Assert("Unable to find available hand when starting pulling??"); + } + + private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStoppedMessage args) + { + // Try find hand that is doing this pull. + // and clear it. + foreach (var hand in component.Hands) + { + if (hand.HeldEntity == null + || !hand.HeldEntity.TryGetComponent(out HandVirtualPullComponent? virtualPull) + || virtualPull.PulledEntity != args.Pulled.Owner.Uid) + continue; + + hand.HeldEntity.Delete(); + break; + } + } + + private void SwapHandsPressed(ICommonSession? session) + { + var player = session?.AttachedEntity; + + if (player == null) + return; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return; + + if (!hands.TryGetSwapHandsResult(out var nextHand)) + return; + + hands.ActiveHand = nextHand; + } + + private bool DropPressed(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + var player = session?.AttachedEntity; + + if (player == null) + return false; + + if (!player.TryGetComponent(out SharedHandsComponent? hands)) + return false; + + var activeHand = hands.ActiveHand; + + if (activeHand == null) + return false; + + hands.TryDropHand(activeHand, coords); + return false; + } + + private void HandleMoveItemFromHand(MoveItemFromHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.TryMoveHeldEntityToActiveHand(msg.HandName); + } + + private void HandleUseInHand(UseInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.UseActiveHeldEntity(); + } + + private void HandleInteractUsingInHand(ClientInteractUsingInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.InteractHandWithActiveHand(msg.HandName); + } + public override void Shutdown() { base.Shutdown(); @@ -54,6 +166,14 @@ namespace Content.Server.Hands CommandBinds.Unregister(); } + private void HandleActivateInHand(ActivateInHandMsg msg, EntitySessionEventArgs args) + { + if (!TryGetHandsComp(args.SenderSession, out var hands)) + return; + + hands.ActivateHeldEntity(msg.HandName); + } + protected override void DropAllItemsInHands(IEntity entity, bool doMobChecks = true) { base.DropAllItemsInHands(entity, doMobChecks); @@ -75,25 +195,21 @@ namespace Content.Server.Hands } } - protected override void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args) - { - component.Dirty(); - } - - private bool TryGetHandsComp(ICommonSession? session, [NotNullWhen(true)] out SharedHandsComponent? hands) + private static bool TryGetHandsComp( + ICommonSession? session, + [NotNullWhen(true)] out SharedHandsComponent? hands) { hands = default; if (session is not IPlayerSession playerSession) return false; - var playerEnt = playerSession?.AttachedEntity; + var playerEnt = playerSession.AttachedEntity; if (playerEnt == null || !playerEnt.IsValid()) return false; - playerEnt.TryGetComponent(out hands); - return hands != null; + return playerEnt.TryGetComponent(out hands); } private void HandleActivateItem(ICommonSession? session) diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index 61a5ce724e..6908b146b5 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -387,6 +387,18 @@ namespace Content.Server.Interaction return false; } + private async Task InteractDoBefore( + IEntity user, + IEntity used, + IEntity? target, + EntityCoordinates clickLocation, + bool canReach) + { + var ev = new BeforeInteractEvent(user, used, target, clickLocation, canReach); + RaiseLocalEvent(used.Uid, ev, false); + return ev.Handled; + } + /// /// Uses a item/object on an entity /// Finds components with the InteractUsing interface and calls their function @@ -397,6 +409,9 @@ namespace Content.Server.Interaction if (!_actionBlockerSystem.CanInteract(user)) return; + if (await InteractDoBefore(user, used, target, clickLocation, true)) + return; + // all interactions should only happen when in range / unobstructed, so no range check is needed var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); RaiseLocalEvent(target.Uid, interactUsingEvent); @@ -696,6 +711,9 @@ namespace Content.Server.Interaction /// public async Task InteractUsingRanged(IEntity user, IEntity used, IEntity? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { + if (await InteractDoBefore(user, used, inRangeUnobstructed ? target : null, clickLocation, false)) + return true; + if (target != null) { var rangedMsg = new RangedInteractEvent(user, used, target, clickLocation); @@ -715,10 +733,7 @@ namespace Content.Server.Interaction } } - if (inRangeUnobstructed) - return await InteractDoAfter(user, used, target, clickLocation, false); - else - return await InteractDoAfter(user, used, null, clickLocation, false); + return await InteractDoAfter(user, used, inRangeUnobstructed ? target : null, clickLocation, false); } public void DoAttack(IEntity user, EntityCoordinates coordinates, bool wideAttack, EntityUid targetUid = default) diff --git a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs index c58c5b32ee..2b110578c1 100644 --- a/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs +++ b/Content.Server/Kitchen/EntitySystems/ReagentGrinderSystem.cs @@ -195,6 +195,9 @@ namespace Content.Server.Kitchen.EntitySystems while (_uiUpdateQueue.TryDequeue(out var comp)) { + if(comp.Deleted) + continue; + bool canJuice = false; bool canGrind = false; if (comp.BeakerContainer.ContainedEntity != null) diff --git a/Content.Server/Light/Components/ExpendableLightComponent.cs b/Content.Server/Light/Components/ExpendableLightComponent.cs index 39671b0e86..87a098f66d 100644 --- a/Content.Server/Light/Components/ExpendableLightComponent.cs +++ b/Content.Server/Light/Components/ExpendableLightComponent.cs @@ -20,8 +20,6 @@ namespace Content.Server.Light.Components [RegisterComponent] public class ExpendableLightComponent : SharedExpendableLightComponent, IUse { - private static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f); - /// /// Status of light, whether or not it is emitting light. /// @@ -77,18 +75,20 @@ namespace Content.Server.Light.Components private void UpdateVisualizer() { + _appearance?.SetData(ExpendableLightVisuals.State, CurrentState); + switch (CurrentState) { case ExpendableLightState.Lit: - _appearance?.SetData(ExpendableLightVisuals.State, TurnOnBehaviourID); + _appearance?.SetData(ExpendableLightVisuals.Behavior, TurnOnBehaviourID); break; case ExpendableLightState.Fading: - _appearance?.SetData(ExpendableLightVisuals.State, FadeOutBehaviourID); + _appearance?.SetData(ExpendableLightVisuals.Behavior, FadeOutBehaviourID); break; case ExpendableLightState.Dead: - _appearance?.SetData(ExpendableLightVisuals.State, string.Empty); + _appearance?.SetData(ExpendableLightVisuals.Behavior, string.Empty); break; } } @@ -100,12 +100,6 @@ namespace Content.Server.Light.Components switch (CurrentState) { case ExpendableLightState.Lit: - - if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopingSound)) - { - loopingSound.Play(LoopedSound, LoopedSoundParams); - } - if (LitSound.TryGetSound(out var litSound)) { SoundSystem.Play(Filter.Pvs(Owner), litSound, Owner); @@ -131,11 +125,6 @@ namespace Content.Server.Light.Components SoundSystem.Play(Filter.Pvs(Owner), dieSound, Owner); } - if (LoopedSound != string.Empty && Owner.TryGetComponent(out var loopSound)) - { - loopSound.StopAllSounds(); - } - sprite.LayerSetState(0, IconStateSpent); sprite.LayerSetShader(0, "shaded"); sprite.LayerSetVisible(1, false); diff --git a/Content.Server/Power/EntitySystems/PowerNetSystem.cs b/Content.Server/Power/EntitySystems/PowerNetSystem.cs index 84b65289b5..d8eee5d111 100644 --- a/Content.Server/Power/EntitySystems/PowerNetSystem.cs +++ b/Content.Server/Power/EntitySystems/PowerNetSystem.cs @@ -19,7 +19,6 @@ namespace Content.Server.Power.EntitySystems private readonly HashSet _powerNetReconnectQueue = new(); private readonly HashSet _apcNetReconnectQueue = new(); - private int _nextId = 1; private readonly BatteryRampPegSolver _solver = new(); public override void Initialize() @@ -50,7 +49,7 @@ namespace Content.Server.Power.EntitySystems private void ApcPowerReceiverShutdown(EntityUid uid, ApcPowerReceiverComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void ApcPowerReceiverPaused( @@ -68,7 +67,7 @@ namespace Content.Server.Power.EntitySystems private void BatteryShutdown(EntityUid uid, PowerNetworkBatteryComponent component, ComponentShutdown args) { - _powerState.Batteries.Remove(component.NetworkBattery.Id); + _powerState.Batteries.Free(component.NetworkBattery.Id); } private static void BatteryPaused(EntityUid uid, PowerNetworkBatteryComponent component, EntityPausedEvent args) @@ -83,7 +82,7 @@ namespace Content.Server.Power.EntitySystems private void PowerConsumerShutdown(EntityUid uid, PowerConsumerComponent component, ComponentShutdown args) { - _powerState.Loads.Remove(component.NetworkLoad.Id); + _powerState.Loads.Free(component.NetworkLoad.Id); } private static void PowerConsumerPaused(EntityUid uid, PowerConsumerComponent component, EntityPausedEvent args) @@ -98,7 +97,7 @@ namespace Content.Server.Power.EntitySystems private void PowerSupplierShutdown(EntityUid uid, PowerSupplierComponent component, ComponentShutdown args) { - _powerState.Supplies.Remove(component.NetworkSupply.Id); + _powerState.Supplies.Free(component.NetworkSupply.Id); } private static void PowerSupplierPaused(EntityUid uid, PowerSupplierComponent component, EntityPausedEvent args) @@ -113,7 +112,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyPowerNet(PowerNet powerNet) { - _powerState.Networks.Remove(powerNet.NetworkNode.Id); + _powerState.Networks.Free(powerNet.NetworkNode.Id); } public void QueueReconnectPowerNet(PowerNet powerNet) @@ -128,7 +127,7 @@ namespace Content.Server.Power.EntitySystems public void DestroyApcNet(ApcNet apcNet) { - _powerState.Networks.Remove(apcNet.NetworkNode.Id); + _powerState.Networks.Free(apcNet.NetworkNode.Id); } public void QueueReconnectApcNet(ApcNet apcNet) @@ -213,26 +212,22 @@ namespace Content.Server.Power.EntitySystems private void AllocLoad(PowerState.Load load) { - load.Id = AllocId(); - _powerState.Loads.Add(load.Id, load); + _powerState.Loads.Allocate(out load.Id) = load; } private void AllocSupply(PowerState.Supply supply) { - supply.Id = AllocId(); - _powerState.Supplies.Add(supply.Id, supply); + _powerState.Supplies.Allocate(out supply.Id) = supply; } private void AllocBattery(PowerState.Battery battery) { - battery.Id = AllocId(); - _powerState.Batteries.Add(battery.Id, battery); + _powerState.Batteries.Allocate(out battery.Id) = battery; } private void AllocNetwork(PowerState.Network network) { - network.Id = AllocId(); - _powerState.Networks.Add(network.Id, network); + _powerState.Networks.Allocate(out network.Id) = network; } private static void DoReconnectApcNet(ApcNet net) @@ -296,11 +291,6 @@ namespace Content.Server.Power.EntitySystems battery.NetworkBattery.LinkedNetworkDischarging = netNode.Id; } } - - private PowerState.NodeId AllocId() - { - return new(_nextId++); - } } /// diff --git a/Content.Server/Power/Pow3r/GraphWalkSolver.cs b/Content.Server/Power/Pow3r/GraphWalkSolver.cs deleted file mode 100644 index 981aad8120..0000000000 --- a/Content.Server/Power/Pow3r/GraphWalkSolver.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using static Content.Server.Power.Pow3r.PowerState; - -namespace Content.Server.Power.Pow3r -{ - /// - /// Partial implementation of full-graph-walking power solving under pow3r. - /// Concept described at https://hackmd.io/@ss14/lowpower - /// - /// - /// Many features like batteries, cycle detection, join handling, etc... are not implemented at all. - /// Seriously, this implementation barely works. Ah well. - /// is better. - /// - public class GraphWalkSolver : IPowerSolver - { - public void Tick(float frameTime, PowerState state) - { - foreach (var load in state.Loads.Values) - { - load.ReceivingPower = 0; - } - - foreach (var supply in state.Supplies.Values) - { - supply.CurrentSupply = 0; - } - - foreach (var network in state.Networks.Values) - { - // Clear some stuff. - network.LocalDemandMet = 0; - - // Add up demands in network. - network.LocalDemandTotal = network.Loads - .Select(l => state.Loads[l]) - .Where(c => c.Enabled) - .Sum(c => c.DesiredPower); - - // Add up supplies in network. - var availableSupplySum = 0f; - var maxSupplySum = 0f; - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled) - continue; - - var rampMax = supply.SupplyRampPosition + supply.SupplyRampTolerance; - var effectiveSupply = Math.Min(rampMax, supply.MaxSupply); - supply.EffectiveMaxSupply = effectiveSupply; - availableSupplySum += effectiveSupply; - maxSupplySum += supply.MaxSupply; - } - - network.AvailableSupplyTotal = availableSupplySum; - network.TheoreticalSupplyTotal = maxSupplySum; - } - - // Sort networks by tree height so that suppliers that have less possible loads go FIRST. - // Idea being that a backup generator on a small subnet should do more work - // so that a larger generator that covers more networks can put its power elsewhere. - var sortedByHeight = state.Networks.Values.OrderBy(v => TotalSubLoadCount(state, v)).ToArray(); - - // Go over every network with supply to send power. - foreach (var network in sortedByHeight) - { - // Find all loads recursively, and sum them up. - var subNets = new List(); - var totalDemand = 0f; - GetLoadingNetworksRecursively(state, network, subNets, ref totalDemand); - - if (totalDemand == 0) - continue; - - // Calculate power delivered. - var power = Math.Min(totalDemand, network.AvailableSupplyTotal); - - // Distribute load across supplies in network. - foreach (var supplyId in network.Supplies) - { - var supply = state.Supplies[supplyId]; - if (!supply.Enabled) - continue; - - if (supply.EffectiveMaxSupply != 0) - { - var ratio = supply.EffectiveMaxSupply / network.AvailableSupplyTotal; - - supply.CurrentSupply = ratio * power; - } - else - { - supply.CurrentSupply = 0; - } - - if (supply.MaxSupply != 0) - { - var ratio = supply.MaxSupply / network.TheoreticalSupplyTotal; - - supply.SupplyRampTarget = ratio * totalDemand; - } - else - { - supply.SupplyRampTarget = 0; - } - } - - // Distribute supply across subnet loads. - foreach (var subNet in subNets) - { - var rem = subNet.RemainingDemand; - var ratio = rem / totalDemand; - - subNet.LocalDemandMet += ratio * power; - } - } - - // Distribute power across loads in networks. - foreach (var network in state.Networks.Values) - { - if (network.LocalDemandMet == 0) - continue; - - foreach (var loadId in network.Loads) - { - var load = state.Loads[loadId]; - if (!load.Enabled) - continue; - - var ratio = load.DesiredPower / network.LocalDemandTotal; - load.ReceivingPower = ratio * network.LocalDemandMet; - } - } - - PowerSolverShared.UpdateRampPositions(frameTime, state); - } - - private int TotalSubLoadCount(PowerState state, Network network) - { - // TODO: Cycle detection. - var height = network.Loads.Count; - - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - if (battery.LinkedNetworkDischarging != default) - { - height += TotalSubLoadCount(state, state.Networks[battery.LinkedNetworkDischarging]); - } - } - - return height; - } - - private void GetLoadingNetworksRecursively( - PowerState state, - Network network, - List networks, - ref float totalDemand) - { - networks.Add(network); - totalDemand += network.LocalDemandTotal - network.LocalDemandMet; - - foreach (var batteryId in network.BatteriesCharging) - { - var battery = state.Batteries[batteryId]; - if (battery.LinkedNetworkDischarging != default) - { - GetLoadingNetworksRecursively( - state, - state.Networks[battery.LinkedNetworkDischarging], - networks, - ref totalDemand); - } - } - } - } -} diff --git a/Content.Server/Power/Pow3r/PowerState.cs b/Content.Server/Power/Pow3r/PowerState.cs index 276fc785a9..0d220b9259 100644 --- a/Content.Server/Power/Pow3r/PowerState.cs +++ b/Content.Server/Power/Pow3r/PowerState.cs @@ -1,40 +1,51 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Numerics; +using System.Linq; +using System.Runtime.CompilerServices; using System.Text.Json; using System.Text.Json.Serialization; +using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Content.Server.Power.Pow3r { public sealed class PowerState { - public const int MaxTickData = 180; - public static readonly JsonSerializerOptions SerializerOptions = new() { IncludeFields = true, Converters = {new NodeIdJsonConverter()} }; - public Dictionary Supplies = new(); - public Dictionary Networks = new(); - public Dictionary Loads = new(); - public Dictionary Batteries = new(); + public GenIdStorage Supplies = new(); + public GenIdStorage Networks = new(); + public GenIdStorage Loads = new(); + public GenIdStorage Batteries = new(); public readonly struct NodeId : IEquatable { - public readonly int Id; + public readonly int Index; + public readonly int Generation; - public NodeId(int id) + public long Combined => (uint) Index | ((long) Generation << 32); + + public NodeId(int index, int generation) { - Id = id; + Index = index; + Generation = generation; + } + + public NodeId(long combined) + { + Index = (int) combined; + Generation = (int) (combined >> 32); } public bool Equals(NodeId other) { - return Id == other.Id; + return Index == other.Index && Generation == other.Generation; } public override bool Equals(object? obj) @@ -44,7 +55,7 @@ namespace Content.Server.Power.Pow3r public override int GetHashCode() { - return Id; + return HashCode.Combine(Index, Generation); } public static bool operator ==(NodeId left, NodeId right) @@ -59,7 +70,261 @@ namespace Content.Server.Power.Pow3r public override string ToString() { - return Id.ToString(); + return $"{Index} (G{Generation})"; + } + } + + public static class GenIdStorage + { + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + return GenIdStorage.FromEnumerable(enumerable); + } + } + + public sealed class GenIdStorage + { + // This is an implementation of "generational index" storage. + // + // The advantage of this storage method is extremely fast, O(1) lookup (way faster than Dictionary). + // Resolving a value in the storage is a single array load and generation compare. Extremely fast. + // Indices can also be cached into temporary + // Disadvantages are that storage cannot be shrunk, and sparse storage is inefficient space wise. + // Also this implementation does not have optimizations necessary to make sparse iteration efficient. + // + // The idea here is that the index type (NodeId in this case) has both an index and a generation. + // The index is an integer index into the storage array, the generation is used to avoid use-after-free. + // + // Empty slots in the array form a linked list of free slots. + // When we allocate a new slot, we pop one link off this linked list and hand out its index + generation. + // + // When we free a node, we bump the generation of the slot and make it the head of the linked list. + // The generation being bumped means that any IDs to this slot will fail to resolve (generation mismatch). + // + + // Index of the next free slot to use when allocating a new one. + // If this is int.MaxValue, + // it basically means "no slot available" and the next allocation call should resize the array storage. + private int _nextFree = int.MaxValue; + private Slot[] _storage; + + public int Count { get; private set; } + + public ref T this[NodeId id] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get + { + ref var slot = ref _storage[id.Index]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + return ref slot.Value; + } + } + + public GenIdStorage() + { + _storage = Array.Empty(); + } + + public static GenIdStorage FromEnumerable(IEnumerable<(NodeId, T)> enumerable) + { + var storage = new GenIdStorage(); + + // Cache enumerable to array to do double enumeration. + var cache = enumerable.ToArray(); + + if (cache.Length == 0) + return storage; + + // Figure out max size necessary and set storage size to that. + var maxSize = cache.Max(tup => tup.Item1.Index) + 1; + storage._storage = new Slot[maxSize]; + + // Fill in slots. + foreach (var (id, value) in cache) + { + DebugTools.Assert(id.Generation != 0, "Generation cannot be 0"); + + ref var slot = ref storage._storage[id.Index]; + DebugTools.Assert(slot.Generation == 0, "Duplicate key index!"); + + slot.Generation = id.Generation; + slot.Value = value; + } + + // Go through empty slots and build the free chain. + var nextFree = int.MaxValue; + for (var i = 0; i < storage._storage.Length; i++) + { + ref var slot = ref storage._storage[i]; + + if (slot.Generation != 0) + // Slot in use. + continue; + + slot.NextSlot = nextFree; + nextFree = i; + } + + storage.Count = cache.Length; + storage._nextFree = nextFree; + + return storage; + } + + public ref T Allocate(out NodeId id) + { + if (_nextFree == int.MaxValue) + ReAllocate(); + + var idx = _nextFree; + ref var slot = ref _storage[idx]; + + Count += 1; + _nextFree = slot.NextSlot; + // NextSlot = -1 indicates filled. + slot.NextSlot = -1; + + id = new NodeId(idx, slot.Generation); + return ref slot.Value; + } + + public void Free(NodeId id) + { + var idx = id.Index; + ref var slot = ref _storage[idx]; + if (slot.Generation != id.Generation) + ThrowKeyNotFound(); + + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + slot.Value = default!; + + Count -= 1; + slot.Generation += 1; + slot.NextSlot = _nextFree; + _nextFree = idx; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private void ReAllocate() + { + var oldLength = _storage.Length; + var newLength = Math.Max(oldLength, 2) * 2; + + ReAllocateTo(newLength); + } + + private void ReAllocateTo(int newSize) + { + var oldLength = _storage.Length; + DebugTools.Assert(newSize >= oldLength, "Cannot shrink GenIdStorage"); + + Array.Resize(ref _storage, newSize); + + for (var i = oldLength; i < newSize - 1; i++) + { + // Build linked list chain for newly allocated segment. + ref var slot = ref _storage[i]; + slot.NextSlot = i + 1; + // Every slot starts at generation 1. + slot.Generation = 1; + } + + _storage[^1].NextSlot = _nextFree; + + _nextFree = oldLength; + } + + public ValuesCollection Values => new(this); + + private struct Slot + { + // Next link on the free list. if int.MaxValue then this is the tail. + // If negative, this slot is occupied. + public int NextSlot; + // Generation of this slot. + public int Generation; + public T Value; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void ThrowKeyNotFound() + { + throw new KeyNotFoundException(); + } + + public readonly struct ValuesCollection : IReadOnlyCollection + { + private readonly GenIdStorage _owner; + + public ValuesCollection(GenIdStorage owner) + { + _owner = owner; + } + + public Enumerator GetEnumerator() + { + return new Enumerator(_owner); + } + + public int Count => _owner.Count; + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public struct Enumerator : IEnumerator + { + // Save the array in the enumerator here to avoid a few pointer dereferences. + private readonly Slot[] _owner; + private int _index; + + public Enumerator(GenIdStorage owner) + { + _owner = owner._storage; + Current = default!; + _index = -1; + } + + public bool MoveNext() + { + while (true) + { + _index += 1; + if (_index >= _owner.Length) + return false; + + ref var slot = ref _owner[_index]; + + if (slot.NextSlot < 0) + { + Current = slot.Value; + return true; + } + } + } + + public void Reset() + { + _index = -1; + } + + object IEnumerator.Current => Current!; + + public T Current { get; private set; } + + public void Dispose() + { + } + } } } @@ -67,12 +332,12 @@ namespace Content.Server.Power.Pow3r { public override NodeId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { - return new(reader.GetInt32()); + return new NodeId(reader.GetInt64()); } public override void Write(Utf8JsonWriter writer, NodeId value, JsonSerializerOptions options) { - writer.WriteNumberValue(value.Id); + writer.WriteNumberValue(value.Combined); } } @@ -186,22 +451,8 @@ namespace Content.Server.Power.Pow3r // "Supplying" means the network is connected to the OUTPUT port of the battery. [ViewVariables] public List BatteriesDischarging = new(); - // Calculation parameters used by GraphWalkSolver. - // Unused by BatteryRampPegSolver. - [JsonIgnore] public float LocalDemandTotal; - [JsonIgnore] public float LocalDemandMet; - [JsonIgnore] public float GroupDemandTotal; - [JsonIgnore] public float GroupDemandMet; - [ViewVariables] [JsonIgnore] public int Height; [JsonIgnore] public bool HeightTouched; - - // Supply available this tick. - [JsonIgnore] public float AvailableSupplyTotal; - - // Max theoretical supply assuming max ramp. - [JsonIgnore] public float TheoreticalSupplyTotal; - public float RemainingDemand => LocalDemandTotal - LocalDemandMet; } } } diff --git a/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs b/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs deleted file mode 100644 index 4230e39d8e..0000000000 --- a/Content.Server/Sound/Components/LoopingLoopingSoundComponent.cs +++ /dev/null @@ -1,67 +0,0 @@ -using Content.Shared.Sound; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.Network; - -namespace Content.Server.Sound.Components -{ - [RegisterComponent] - public class LoopingLoopingSoundComponent : SharedLoopingSoundComponent - { - /// - /// Stops all sounds. - /// - /// User that will be affected. - public void StopAllSounds(INetChannel? channel) - { - SendNetworkMessage(new StopAllSoundsMessage(), channel); - } - - /// - /// Stops a certain scheduled sound from playing. - /// - /// User that will be affected. - public void StopScheduledSound(string filename, INetChannel? channel) - { - SendNetworkMessage(new StopSoundScheduleMessage() {Filename = filename}, channel); - } - - /// - /// Adds an scheduled sound to be played. - /// - /// User that will be affected. - public void AddScheduledSound(ScheduledSound schedule, INetChannel? channel) - { - SendNetworkMessage(new ScheduledSoundMessage() {Schedule = schedule}, channel); - } - - public override void StopAllSounds() - { - StopAllSounds(null); - } - - public override void StopScheduledSound(string filename) - { - StopScheduledSound(filename, null); - } - - public override void AddScheduledSound(ScheduledSound schedule) - { - AddScheduledSound(schedule, null); - } - - /// - /// Play an audio file following the entity. - /// - /// The resource path to the OGG Vorbis file to play. - /// User that will be affected. - public void Play(string filename, AudioParams? audioParams = null, INetChannel? channel = null) - { - AddScheduledSound(new ScheduledSound() - { - Filename = filename, - AudioParams = audioParams, - }, channel); - } - } -} diff --git a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs index cbd10016a5..939acdd21a 100644 --- a/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/DefaultMetabolizable.cs @@ -1,11 +1,13 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; namespace Content.Shared.Chemistry.Metabolizable { /// - /// Default metabolism for reagents. Metabolizes the reagent with no effects + /// Default metabolization for reagents. Returns the amount of reagents metabolized without applying effects. + /// Metabolizes reagents at a constant rate, limited by how much is available. Other classes are derived from + /// this class, so that they do not need their own metabolization quantity calculation. /// [DataDefinition] public class DefaultMetabolizable : IMetabolizable @@ -13,12 +15,22 @@ namespace Content.Shared.Chemistry.Metabolizable /// /// Rate of metabolism in units / second /// - [DataField("rate")] - public double MetabolismRate { get; set; } = 1; + [DataField("rate")] public ReagentUnit MetabolismRate { get; set; } = ReagentUnit.New(1); - ReagentUnit IMetabolizable.Metabolize(IEntity solutionEntity, string reagentId, float tickTime) + public virtual ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent) { - return ReagentUnit.New(MetabolismRate * tickTime); + + // How much reagent should we metabolize + // The default behaviour is to metabolize at a constant rate, independent of the quantity of reagents. + var amountMetabolized = MetabolismRate * tickTime; + + // is that much reagent actually available? + if (availableReagent < amountMetabolized) + { + return availableReagent; + } + + return amountMetabolized; } } } diff --git a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs index e08e209a0c..f8b2a888cc 100644 --- a/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs +++ b/Content.Shared/Chemistry/Metabolizable/IMetabolizable.cs @@ -1,4 +1,4 @@ -using Content.Shared.Chemistry.Reagent; +using Content.Shared.Chemistry.Reagent; using Robust.Shared.GameObjects; namespace Content.Shared.Chemistry.Metabolizable @@ -16,7 +16,8 @@ namespace Content.Shared.Chemistry.Metabolizable /// The entity containing the solution. /// The reagent id /// The time since the last metabolism tick in seconds. + /// Reagent available to be metabolized. /// The amount of reagent to be removed. The metabolizing organ should handle removing the reagent. - ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime); + ReagentUnit Metabolize(IEntity solutionEntity, string reagentId, float tickTime, ReagentUnit availableReagent); } } diff --git a/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs b/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs index 59f931e871..fe8e081e25 100644 --- a/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs +++ b/Content.Shared/Chemistry/Reagent/ReagentEntityReaction.cs @@ -12,7 +12,7 @@ namespace Content.Shared.Chemistry.Reagent Ingestion, } - [DataDefinition] + [ImplicitDataDefinitionForInheritors] public abstract class ReagentEntityReaction { [ViewVariables] diff --git a/Content.Shared/Hands/Components/HandVirtualPullComponent.cs b/Content.Shared/Hands/Components/HandVirtualPullComponent.cs new file mode 100644 index 0000000000..42f6bd85f3 --- /dev/null +++ b/Content.Shared/Hands/Components/HandVirtualPullComponent.cs @@ -0,0 +1,50 @@ +using System; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Players; +using Robust.Shared.Serialization; + +namespace Content.Shared.Hands.Components +{ + [RegisterComponent] + [NetworkedComponent] + public sealed class HandVirtualPullComponent : Component + { + private EntityUid _pulledEntity; + public override string Name => "HandVirtualPull"; + + public EntityUid PulledEntity + { + get => _pulledEntity; + set + { + _pulledEntity = value; + Dirty(); + } + } + + public override ComponentState GetComponentState(ICommonSession player) + { + return new VirtualPullComponentState(_pulledEntity); + } + + public override void HandleComponentState(ComponentState? curState, ComponentState? nextState) + { + if (curState is not VirtualPullComponentState pullState) + return; + + _pulledEntity = pullState.PulledEntity; + } + + [Serializable, NetSerializable] + public sealed class VirtualPullComponentState : ComponentState + { + public readonly EntityUid PulledEntity; + + public VirtualPullComponentState(EntityUid pulledEntity) + { + PulledEntity = pulledEntity; + } + } + } +} diff --git a/Content.Shared/Hands/Components/SharedHandsComponent.cs b/Content.Shared/Hands/Components/SharedHandsComponent.cs index 15a2ca3d69..9e1978b06f 100644 --- a/Content.Shared/Hands/Components/SharedHandsComponent.cs +++ b/Content.Shared/Hands/Components/SharedHandsComponent.cs @@ -55,11 +55,11 @@ namespace Content.Shared.Hands.Components } } } + private string? _activeHand; [ViewVariables] - public IReadOnlyList ReadOnlyHands => Hands; - protected readonly List Hands = new(); + public readonly List Hands = new(); /// /// The amount of throw impulse per distance the player is from the throw target. @@ -89,7 +89,10 @@ namespace Content.Shared.Hands.Components public virtual void HandsModified() { + // todo axe all this for ECS. Dirty(); + + Owner.EntityManager.EventBus.RaiseEvent(EventSource.Local, new HandsModifiedMessage { Hands = this }); } public void AddHand(string handName, HandLocation handLocation) @@ -100,7 +103,7 @@ namespace Content.Shared.Hands.Components var container = ContainerHelpers.CreateContainer(Owner, handName); container.OccludesLight = false; - Hands.Add(new Hand(handName, true, handLocation, container)); + Hands.Add(new Hand(handName, handLocation, container)); if (ActiveHand == null) ActiveHand = handName; @@ -125,48 +128,55 @@ namespace Content.Shared.Hands.Components Hands.Remove(hand); if (ActiveHand == hand.Name) - ActiveHand = ReadOnlyHands.FirstOrDefault()?.Name; + ActiveHand = Hands.FirstOrDefault()?.Name; HandCountChanged(); HandsModified(); } - public bool HasHand(string handName) - { - foreach (var hand in Hands) - { - if (hand.Name == handName) - return true; - } - return false; - } - - private Hand? GetHand(string handName) - { - foreach (var hand in Hands) - { - if (hand.Name == handName) - return hand; - } - return null; - } - private Hand? GetActiveHand() { if (ActiveHand == null) return null; - return GetHand(ActiveHand); + return GetHandOrNull(ActiveHand); } - protected bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) + public bool HasHand(string handName) { - foundHand = GetHand(handName); - return foundHand != null; + return TryGetHand(handName, out _); } - protected bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) + public Hand? GetHandOrNull(string handName) + { + return TryGetHand(handName, out var hand) ? hand : null; + } + + public Hand GetHand(string handName) + { + if (!TryGetHand(handName, out var hand)) + throw new KeyNotFoundException($"Unable to find hand with name {handName}"); + + return hand; + } + + public bool TryGetHand(string handName, [NotNullWhen(true)] out Hand? foundHand) + { + foreach (var hand in Hands) + { + if (hand.Name == handName) + { + foundHand = hand; + return true; + }; + } + + foundHand = null; + return false; + } + + public bool TryGetActiveHand([NotNullWhen(true)] out Hand? activeHand) { activeHand = GetActiveHand(); return activeHand != null; @@ -211,7 +221,7 @@ namespace Content.Shared.Hands.Components public IEnumerable GetAllHeldEntities() { - foreach (var hand in ReadOnlyHands) + foreach (var hand in Hands) { if (hand.HeldEntity != null) yield return hand.HeldEntity; @@ -416,7 +426,7 @@ namespace Content.Shared.Hands.Components /// /// Drops a hands contents to the target location. /// - private void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation, bool intentionalDrop = true) + public void DropHeldEntity(Hand hand, EntityCoordinates targetDropLocation, bool intentionalDrop = true) { var heldEntity = hand.HeldEntity; @@ -538,16 +548,7 @@ namespace Content.Shared.Hands.Components public bool CanPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) { - if (!TryGetActiveHand(out var hand)) - return false; - - if (checkActionBlocker && !PlayerCanPickup()) - return false; - - if (!CanInsertEntityIntoHand(hand, entity)) - return false; - - return true; + return ActiveHand != null && CanPickupEntity(ActiveHand, entity, checkActionBlocker); } /// @@ -563,10 +564,7 @@ namespace Content.Shared.Hands.Components public bool TryPickupEntityToActiveHand(IEntity entity, bool checkActionBlocker = true) { - if (!TryGetActiveHand(out var hand)) - return false; - - return TryPickupEntity(hand, entity, checkActionBlocker); + return ActiveHand != null && TryPickupEntity(ActiveHand, entity, checkActionBlocker); } /// @@ -574,9 +572,6 @@ namespace Content.Shared.Hands.Components /// protected bool CanInsertEntityIntoHand(Hand hand, IEntity entity) { - if (!hand.Enabled) - return false; - var handContainer = hand.Container; if (handContainer == null) return false; @@ -602,7 +597,7 @@ namespace Content.Shared.Hands.Components /// /// Puts an entity into the player's hand, assumes that the insertion is allowed. /// - private void PutEntityIntoHand(Hand hand, IEntity entity) + public void PutEntityIntoHand(Hand hand, IEntity entity) { var handContainer = hand.Container; if (handContainer == null) @@ -658,7 +653,7 @@ namespace Content.Shared.Hands.Components if (newActiveIndex > finalHandIndex) newActiveIndex = 0; - nextHand = ReadOnlyHands[newActiveIndex].Name; + nextHand = Hands[newActiveIndex].Name; return true; } @@ -752,7 +747,7 @@ namespace Content.Shared.Hands.Components Hand? priorityHand = null; if (priorityHandName != null) - priorityHand = GetHand(priorityHandName); + priorityHand = GetHandOrNull(priorityHandName); return TryPutInAnyHand(entity, priorityHand, checkActionBlocker); } @@ -793,43 +788,16 @@ namespace Content.Shared.Hands.Components protected virtual void DoActivate(IEntity heldEntity) { } protected virtual void HandlePickupAnimation(IEntity entity) { } - - protected void EnableHand(Hand hand) - { - hand.Enabled = true; - Dirty(); - } - - protected void DisableHand(Hand hand) - { - hand.Enabled = false; - DropHeldEntityToFloor(hand, intentionalDrop: false); - Dirty(); - } } - public interface IReadOnlyHand + public class Hand { + [ViewVariables] public string Name { get; } - public bool Enabled { get; } - + [ViewVariables] public HandLocation Location { get; } - public abstract IEntity? HeldEntity { get; } - } - - public class Hand : IReadOnlyHand - { - [ViewVariables] - public string Name { get; set; } - - [ViewVariables] - public bool Enabled { get; set; } - - [ViewVariables] - public HandLocation Location { get; set; } - /// /// The container used to hold the contents of this hand. Nullable because the client must get the containers via , /// which may not be synced with the server when the client hands are created. @@ -840,37 +808,36 @@ namespace Content.Shared.Hands.Components [ViewVariables] public IEntity? HeldEntity => Container?.ContainedEntities?.FirstOrDefault(); - public Hand(string name, bool enabled, HandLocation location, IContainer? container = null) + public bool IsEmpty => HeldEntity == null; + + public Hand(string name, HandLocation location, IContainer? container = null) { Name = name; - Enabled = enabled; Location = location; Container = container; } public HandState ToHandState() { - return new(Name, Location, Enabled); + return new(Name, Location); } } [Serializable, NetSerializable] - public sealed class HandState + public struct HandState { public string Name { get; } public HandLocation Location { get; } - public bool Enabled { get; } - public HandState(string name, HandLocation location, bool enabled) + public HandState(string name, HandLocation location) { Name = name; Location = location; - Enabled = enabled; } } [Serializable, NetSerializable] - public class HandsComponentState : ComponentState + public sealed class HandsComponentState : ComponentState { public HandState[] Hands { get; } public string? ActiveHand { get; } @@ -886,25 +853,20 @@ namespace Content.Shared.Hands.Components /// A message that calls the use interaction on an item in hand, presumed for now the interaction will occur only on the active hand. /// [Serializable, NetSerializable] - public class UseInHandMsg : ComponentMessage + public sealed class UseInHandMsg : EntityEventArgs { - public UseInHandMsg() - { - Directed = true; - } } /// /// A message that calls the activate interaction on the item in the specified hand. /// [Serializable, NetSerializable] - public class ActivateInHandMsg : ComponentMessage + public class ActivateInHandMsg : EntityEventArgs { public string HandName { get; } public ActivateInHandMsg(string handName) { - Directed = true; HandName = handName; } } @@ -913,13 +875,12 @@ namespace Content.Shared.Hands.Components /// Uses the item in the active hand on the item in the specified hand. /// [Serializable, NetSerializable] - public class ClientAttackByInHandMsg : ComponentMessage + public class ClientInteractUsingInHandMsg : EntityEventArgs { public string HandName { get; } - public ClientAttackByInHandMsg(string handName) + public ClientInteractUsingInHandMsg(string handName) { - Directed = true; HandName = handName; } } @@ -928,28 +889,12 @@ namespace Content.Shared.Hands.Components /// Moves an item from one hand to the active hand. /// [Serializable, NetSerializable] - public class MoveItemFromHandMsg : ComponentMessage + public class MoveItemFromHandMsg : EntityEventArgs { public string HandName { get; } public MoveItemFromHandMsg(string handName) { - Directed = true; - HandName = handName; - } - } - - /// - /// Sets the player's active hand to a specified hand. - /// - [Serializable, NetSerializable] - public class ClientChangedHandMsg : ComponentMessage - { - public string HandName { get; } - - public ClientChangedHandMsg(string handName) - { - Directed = true; HandName = handName; } } @@ -975,7 +920,7 @@ namespace Content.Shared.Hands.Components } [Serializable, NetSerializable] - public class PickupAnimationMessage : ComponentMessage + public class PickupAnimationMessage : EntityEventArgs { public EntityUid EntityUid { get; } public EntityCoordinates InitialPosition { get; } @@ -983,10 +928,15 @@ namespace Content.Shared.Hands.Components public PickupAnimationMessage(EntityUid entityUid, Vector2 pickupDirection, EntityCoordinates initialPosition) { - Directed = true; EntityUid = entityUid; PickupDirection = pickupDirection; InitialPosition = initialPosition; } } + + [Serializable, NetSerializable] + public struct HandsModifiedMessage + { + public SharedHandsComponent Hands; + } } diff --git a/Content.Shared/Hands/SharedHandsSystem.cs b/Content.Shared/Hands/SharedHandsSystem.cs index 9a9bdec3d8..96bd96d793 100644 --- a/Content.Shared/Hands/SharedHandsSystem.cs +++ b/Content.Shared/Hands/SharedHandsSystem.cs @@ -1,7 +1,6 @@ using Content.Shared.Hands.Components; using Robust.Shared.Containers; using Robust.Shared.GameObjects; -using Robust.Shared.Map; using Robust.Shared.Serialization; using System; @@ -16,11 +15,7 @@ namespace Content.Shared.Hands SubscribeLocalEvent(HandleContainerModified); SubscribeLocalEvent(HandleContainerModified); - SubscribeLocalEvent(HandleSetHand); - SubscribeNetworkEvent(HandleSetHand); - - SubscribeLocalEvent(HandleDrop); - SubscribeNetworkEvent(HandleDrop); + SubscribeAllEvent(HandleSetHand); } public void DropHandItems(IEntity entity, bool doMobChecks = true) @@ -38,14 +33,16 @@ namespace Content.Shared.Hands eventBus.RaiseLocalEvent(uid, msg); - if (msg.Cancelled) return; + if (msg.Cancelled) + return; if (entity.TryGetContainerMan(out var containerManager)) { var parentMsg = new ContainedEntityDropHandItemsAttemptEvent(uid); eventBus.RaiseLocalEvent(containerManager.Owner.Uid, parentMsg); - if (parentMsg.Cancelled) return; + if (parentMsg.Cancelled) + return; } DropAllItemsInHands(entity, doMobChecks); @@ -55,9 +52,9 @@ namespace Content.Shared.Hands { } - private void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) + private static void HandleSetHand(RequestSetHandEvent msg, EntitySessionEventArgs eventArgs) { - var entity = eventArgs.SenderSession?.AttachedEntity; + var entity = eventArgs.SenderSession.AttachedEntity; if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) return; @@ -65,17 +62,13 @@ namespace Content.Shared.Hands hands.ActiveHand = msg.HandName; } - private void HandleDrop(RequestDropHeldEntityEvent msg, EntitySessionEventArgs eventArgs) + protected virtual void HandleContainerModified( + EntityUid uid, + SharedHandsComponent component, + ContainerModifiedMessage args) { - var entity = eventArgs.SenderSession?.AttachedEntity; - - if (entity == null || !entity.TryGetComponent(out SharedHandsComponent? hands)) - return; - - hands.TryDropHand(msg.HandName, msg.DropTarget); + component.Dirty(); } - - protected abstract void HandleContainerModified(EntityUid uid, SharedHandsComponent component, ContainerModifiedMessage args); } public sealed class ContainedEntityDropHandItemsAttemptEvent : CancellableEntityEventArgs @@ -103,21 +96,4 @@ namespace Content.Shared.Hands HandName = handName; } } - - [Serializable, NetSerializable] - public class RequestDropHeldEntityEvent : EntityEventArgs - { - /// - /// The hand to drop from. - /// - public string HandName { get; } - - public EntityCoordinates DropTarget { get; } - - public RequestDropHeldEntityEvent(string handName, EntityCoordinates dropTarget) - { - HandName = handName; - DropTarget = dropTarget; - } - } } diff --git a/Content.Shared/Interaction/BeforeInteract.cs b/Content.Shared/Interaction/BeforeInteract.cs new file mode 100644 index 0000000000..2c8bb5ca83 --- /dev/null +++ b/Content.Shared/Interaction/BeforeInteract.cs @@ -0,0 +1,53 @@ +using JetBrains.Annotations; +using Robust.Shared.GameObjects; +using Robust.Shared.Map; + +namespace Content.Shared.Interaction +{ + /// + /// Raised directed on the used object when clicking on another object before an interaction is handled. + /// + [PublicAPI] + public class BeforeInteractEvent : HandledEntityEventArgs + { + /// + /// Entity that triggered the interaction. + /// + public IEntity User { get; } + + /// + /// Entity that the user used to interact. + /// + public IEntity Used { get; } + + /// + /// Entity that was interacted on. This can be null if the attack did not click on an entity. + /// + public IEntity? Target { get; } + + /// + /// Location that the user clicked outside of their interaction range. + /// + public EntityCoordinates ClickLocation { get; } + + /// + /// Is the click location close enough to reach by the player? This does not check for obstructions, just that the target is within + /// reach radius around the user. + /// + public bool CanReach { get; } + + public BeforeInteractEvent( + IEntity user, + IEntity used, + IEntity? target, + EntityCoordinates clickLocation, + bool canReach) + { + User = user; + Used = used; + Target = target; + ClickLocation = clickLocation; + CanReach = canReach; + } + } +} diff --git a/Content.Shared/DragDrop/IDropped.cs b/Content.Shared/Interaction/IDropped.cs similarity index 97% rename from Content.Shared/DragDrop/IDropped.cs rename to Content.Shared/Interaction/IDropped.cs index c09f7fc7df..b1daed20fc 100644 --- a/Content.Shared/DragDrop/IDropped.cs +++ b/Content.Shared/Interaction/IDropped.cs @@ -3,7 +3,7 @@ using JetBrains.Annotations; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; -namespace Content.Shared.DragDrop +namespace Content.Shared.Interaction { /// /// This interface gives components behavior when they're dropped by a mob. diff --git a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs index 1cd4c47b98..1ee1cd35c9 100644 --- a/Content.Shared/Light/Component/SharedExpendableLightComponent.cs +++ b/Content.Shared/Light/Component/SharedExpendableLightComponent.cs @@ -1,5 +1,9 @@ using System; using Content.Shared.Sound; +using Robust.Shared.Audio; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; +using Robust.Shared.Players; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; @@ -9,7 +13,8 @@ namespace Content.Shared.Light.Component [Serializable, NetSerializable] public enum ExpendableLightVisuals { - State + State, + Behavior } [Serializable, NetSerializable] @@ -21,8 +26,11 @@ namespace Content.Shared.Light.Component Dead } + [NetworkedComponent] public abstract class SharedExpendableLightComponent: Robust.Shared.GameObjects.Component { + public static readonly AudioParams LoopedSoundParams = new(0, 1, "Master", 62.5f, 1, true, 0.3f); + public sealed override string Name => "ExpendableLight"; [ViewVariables(VVAccess.ReadOnly)] @@ -66,7 +74,7 @@ namespace Content.Shared.Light.Component [ViewVariables] [DataField("loopedSound")] - protected string LoopedSound { get; set; } = default!; + public string LoopedSound { get; set; } = string.Empty; [ViewVariables] [DataField("dieSound")] diff --git a/Content.Shared/Pulling/Components/SharedPullableComponent.cs b/Content.Shared/Pulling/Components/SharedPullableComponent.cs index 34890165eb..a25f3915ff 100644 --- a/Content.Shared/Pulling/Components/SharedPullableComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullableComponent.cs @@ -50,6 +50,8 @@ namespace Content.Shared.Pulling.Components return; } + var eventBus = Owner.EntityManager.EventBus; + // New value. Abandon being pulled by any existing object. if (_puller != null) { @@ -64,10 +66,9 @@ namespace Content.Shared.Pulling.Components { var message = new PullStoppedMessage(oldPullerPhysics, _physics); - oldPuller.SendMessage(null, message); - Owner.SendMessage(null, message); + eventBus.RaiseLocalEvent(oldPuller.Uid, message, broadcast: false); + eventBus.RaiseLocalEvent(Owner.Uid, message); - oldPuller.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); _physics.WakeBody(); } // else-branch warning is handled below @@ -125,14 +126,14 @@ namespace Content.Shared.Pulling.Components var pullAttempt = new PullAttemptMessage(pullerPhysics, _physics); - value.SendMessage(null, pullAttempt); + eventBus.RaiseLocalEvent(value.Uid, pullAttempt, broadcast: false); if (pullAttempt.Cancelled) { return; } - Owner.SendMessage(null, pullAttempt); + eventBus.RaiseLocalEvent(Owner.Uid, pullAttempt); if (pullAttempt.Cancelled) { @@ -147,10 +148,8 @@ namespace Content.Shared.Pulling.Components var message = new PullStartedMessage(PullerPhysics, _physics); - _puller.SendMessage(null, message); - Owner.SendMessage(null, message); - - _puller.EntityManager.EventBus.RaiseEvent(EventSource.Local, message); + eventBus.RaiseLocalEvent(_puller.Uid, message, broadcast: false); + eventBus.RaiseLocalEvent(Owner.Uid, message); var union = PullerPhysics.GetWorldAABB().Union(_physics.GetWorldAABB()); var length = Math.Max(union.Size.X, union.Size.Y) * 0.75f; @@ -335,29 +334,6 @@ namespace Content.Shared.Pulling.Components Puller = entity; } - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - if (message is not PullMessage pullMessage || - pullMessage.Pulled.Owner != Owner) - { - return; - } - - var pulledStatus = Owner.GetComponentOrNull(); - - switch (message) - { - case PullStartedMessage: - pulledStatus?.ShowAlert(AlertType.Pulled); - break; - case PullStoppedMessage: - pulledStatus?.ClearAlert(AlertType.Pulled); - break; - } - } - protected override void OnRemove() { TryStopPull(); diff --git a/Content.Shared/Pulling/Components/SharedPullerComponent.cs b/Content.Shared/Pulling/Components/SharedPullerComponent.cs index 12a539ab12..1579339958 100644 --- a/Content.Shared/Pulling/Components/SharedPullerComponent.cs +++ b/Content.Shared/Pulling/Components/SharedPullerComponent.cs @@ -1,6 +1,4 @@ -using Content.Shared.Alert; -using Content.Shared.Movement.Components; -using Content.Shared.Physics.Pull; +using Content.Shared.Movement.Components; using Robust.Shared.GameObjects; using Component = Robust.Shared.GameObjects.Component; @@ -46,30 +44,5 @@ namespace Content.Shared.Pulling.Components base.OnRemove(); } - - public override void HandleMessage(ComponentMessage message, IComponent? component) - { - base.HandleMessage(message, component); - - if (message is not PullMessage pullMessage || - pullMessage.Puller.Owner != Owner) - { - return; - } - - SharedAlertsComponent? ownerStatus = Owner.GetComponentOrNull(); - - switch (message) - { - case PullStartedMessage msg: - Pulling = msg.Pulled.Owner; - ownerStatus?.ShowAlert(AlertType.Pulling); - break; - case PullStoppedMessage _: - Pulling = null; - ownerStatus?.ClearAlert(AlertType.Pulling); - break; - } - } } } diff --git a/Content.Shared/Physics/Pull/PullAttemptMessage.cs b/Content.Shared/Pulling/Events/PullAttemptMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullAttemptMessage.cs rename to Content.Shared/Pulling/Events/PullAttemptMessage.cs diff --git a/Content.Shared/Physics/Pull/PullMessage.cs b/Content.Shared/Pulling/Events/PullMessage.cs similarity index 87% rename from Content.Shared/Physics/Pull/PullMessage.cs rename to Content.Shared/Pulling/Events/PullMessage.cs index cbdb5af3a1..4376720a03 100644 --- a/Content.Shared/Physics/Pull/PullMessage.cs +++ b/Content.Shared/Pulling/Events/PullMessage.cs @@ -3,7 +3,7 @@ using Robust.Shared.Physics; namespace Content.Shared.Physics.Pull { - public class PullMessage : ComponentMessage + public class PullMessage : EntityEventArgs { public readonly IPhysBody Puller; public readonly IPhysBody Pulled; diff --git a/Content.Shared/Physics/Pull/PullStartedMessage.cs b/Content.Shared/Pulling/Events/PullStartedMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullStartedMessage.cs rename to Content.Shared/Pulling/Events/PullStartedMessage.cs diff --git a/Content.Shared/Physics/Pull/PullStoppedMessage.cs b/Content.Shared/Pulling/Events/PullStoppedMessage.cs similarity index 100% rename from Content.Shared/Physics/Pull/PullStoppedMessage.cs rename to Content.Shared/Pulling/Events/PullStoppedMessage.cs diff --git a/Content.Shared/Pulling/Systems/SharedPullerSystem.cs b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs new file mode 100644 index 0000000000..d05754c851 --- /dev/null +++ b/Content.Shared/Pulling/Systems/SharedPullerSystem.cs @@ -0,0 +1,44 @@ +using Content.Shared.Alert; +using Content.Shared.Physics.Pull; +using Content.Shared.Pulling.Components; +using JetBrains.Annotations; +using Robust.Shared.GameObjects; + +namespace Content.Shared.Pulling +{ + [UsedImplicitly] + public sealed class SharedPullerSystem : EntitySystem + { + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(PullerHandlePullStarted); + SubscribeLocalEvent(PullerHandlePullStopped); + } + + private static void PullerHandlePullStarted( + EntityUid uid, + SharedPullerComponent component, + PullStartedMessage args) + { + if (args.Puller.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ShowAlert(AlertType.Pulling); + } + + private static void PullerHandlePullStopped( + EntityUid uid, + SharedPullerComponent component, + PullStoppedMessage args) + { + if (args.Puller.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ClearAlert(AlertType.Pulling); + } + } +} diff --git a/Content.Shared/Pulling/SharedPullingSystem.cs b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs similarity index 88% rename from Content.Shared/Pulling/SharedPullingSystem.cs rename to Content.Shared/Pulling/Systems/SharedPullingSystem.cs index 0bb04d19b4..5783ee332d 100644 --- a/Content.Shared/Pulling/SharedPullingSystem.cs +++ b/Content.Shared/Pulling/Systems/SharedPullingSystem.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using Content.Shared.Alert; using Content.Shared.GameTicking; using Content.Shared.Input; using Content.Shared.Physics.Pull; @@ -56,12 +57,34 @@ namespace Content.Shared.Pulling SubscribeLocalEvent(PullerMoved); SubscribeLocalEvent(HandleContainerInsert); + SubscribeLocalEvent(PullableHandlePullStarted); + SubscribeLocalEvent(PullableHandlePullStopped); + CommandBinds.Builder .Bind(ContentKeyFunctions.MovePulledObject, new PointerInputCmdHandler(HandleMovePulledObject)) .Bind(ContentKeyFunctions.ReleasePulledObject, InputCmdHandler.FromDelegate(HandleReleasePulledObject)) .Register(); } + // Raise a "you are being pulled" alert if the pulled entity has alerts. + private static void PullableHandlePullStarted(EntityUid uid, SharedPullableComponent component, PullStartedMessage args) + { + if (args.Pulled.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ShowAlert(AlertType.Pulled); + } + + private static void PullableHandlePullStopped(EntityUid uid, SharedPullableComponent component, PullStoppedMessage args) + { + if (args.Pulled.Owner.Uid != uid) + return; + + if (component.Owner.TryGetComponent(out SharedAlertsComponent? alerts)) + alerts.ClearAlert(AlertType.Pulled); + } + public override void Update(float frameTime) { base.Update(frameTime); diff --git a/Content.Shared/Sound/SharedLoopingSoundComponent.cs b/Content.Shared/Sound/SharedLoopingSoundComponent.cs deleted file mode 100644 index b31234d03e..0000000000 --- a/Content.Shared/Sound/SharedLoopingSoundComponent.cs +++ /dev/null @@ -1,116 +0,0 @@ -using System; -using Robust.Shared.Audio; -using Robust.Shared.GameObjects; -using Robust.Shared.GameStates; -using Robust.Shared.Serialization; -using Robust.Shared.Serialization.Manager.Attributes; - -namespace Content.Shared.Sound -{ - [NetworkedComponent()] - public class SharedLoopingSoundComponent : Component - { - public override string Name => "LoopingSound"; - - /// - /// Stops all sounds. - /// - public virtual void StopAllSounds() - {} - - /// - /// Stops a certain scheduled sound from playing. - /// - public virtual void StopScheduledSound(string filename) - {} - - /// - /// Adds an scheduled sound to be played. - /// - public virtual void AddScheduledSound(ScheduledSound scheduledSound) - {} - - /// - /// Play an audio file following the entity. - /// - /// The resource path to the OGG Vorbis file to play. - public void Play(string filename, AudioParams? audioParams = null) - { - AddScheduledSound(new ScheduledSound() - { - Filename = filename, - AudioParams = audioParams, - }); - } - } - - [NetSerializable, Serializable] - public class ScheduledSoundMessage : ComponentMessage - { - public ScheduledSound Schedule = new(); - public ScheduledSoundMessage() - { - Directed = true; - } - } - - [NetSerializable, Serializable] - public class StopSoundScheduleMessage : ComponentMessage - { - public string Filename = string.Empty; - public StopSoundScheduleMessage() - { - Directed = true; - } - } - - [NetSerializable, Serializable] - public class StopAllSoundsMessage : ComponentMessage - { - public StopAllSoundsMessage() - { - Directed = true; - } - } - - [Serializable, NetSerializable] - [DataDefinition] - public class ScheduledSound - { - [DataField("fileName")] - public string Filename = string.Empty; - - /// - /// The parameters to play the sound with. - /// - [DataField("audioparams")] - public AudioParams? AudioParams; - - /// - /// Delay in milliseconds before playing the sound, - /// and delay between repetitions if Times is not 0. - /// - [DataField("delay")] - public uint Delay; - - /// - /// Maximum number of milliseconds to add to the delay randomly. - /// Useful for random ambience noises. Generated value differs from client to client. - /// - [DataField("randomdelay")] - public uint RandomDelay; - - /// - /// How many times to repeat the sound. If it's 0, it will play the sound once. - /// If it's less than 0, it will repeat the sound indefinitely. - /// If it's greater than 0, it will play the sound n+1 times. - /// - [DataField("times")] - public int Times; - - /// - /// Whether the sound will play or not. - /// - public bool Play = true; - } -} diff --git a/Content.Shared/Sound/SoundSpecifier.cs b/Content.Shared/Sound/SoundSpecifier.cs index dff9987880..a911c5db81 100644 --- a/Content.Shared/Sound/SoundSpecifier.cs +++ b/Content.Shared/Sound/SoundSpecifier.cs @@ -7,7 +7,7 @@ using System.Diagnostics.CodeAnalysis; namespace Content.Shared.Sound { - [DataDefinition] + [ImplicitDataDefinitionForInheritors] public abstract class SoundSpecifier { public abstract string GetSound(); @@ -15,7 +15,6 @@ namespace Content.Shared.Sound public abstract bool TryGetSound([NotNullWhen(true)] out string? sound); } - [DataDefinition] public sealed class SoundPathSpecifier : SoundSpecifier { public const string Node = "path"; @@ -49,7 +48,6 @@ namespace Content.Shared.Sound } } - [DataDefinition] public sealed class SoundCollectionSpecifier : SoundSpecifier { public const string Node = "collection"; diff --git a/Pow3r/Program.SaveLoad.cs b/Pow3r/Program.SaveLoad.cs index 2ffdc8eb4e..5fff64f662 100644 --- a/Pow3r/Program.SaveLoad.cs +++ b/Pow3r/Program.SaveLoad.cs @@ -20,15 +20,14 @@ namespace Pow3r return; _paused = dat.Paused; - _nextId = dat.NextId; _currentSolver = dat.Solver; _state = new PowerState { - Networks = dat.Networks.ToDictionary(n => n.Id, n => n), - Supplies = dat.Supplies.ToDictionary(s => s.Id, s => s), - Loads = dat.Loads.ToDictionary(l => l.Id, l => l), - Batteries = dat.Batteries.ToDictionary(b => b.Id, b => b) + Networks = GenIdStorage.FromEnumerable(dat.Networks.Select(n => (n.Id, n))), + Supplies = GenIdStorage.FromEnumerable(dat.Supplies.Select(s => (s.Id, s))), + Loads = GenIdStorage.FromEnumerable(dat.Loads.Select(l => (l.Id, l))), + Batteries = GenIdStorage.FromEnumerable(dat.Batteries.Select(b => (b.Id, b))) }; _displayLoads = dat.Loads.ToDictionary(n => n.Id, _ => new DisplayLoad()); @@ -44,7 +43,6 @@ namespace Pow3r var data = new DiskDat { Paused = _paused, - NextId = _nextId, Solver = _currentSolver, Loads = _state.Loads.Values.ToList(), @@ -59,7 +57,6 @@ namespace Pow3r private sealed class DiskDat { public bool Paused; - public int NextId; public int Solver; public List Loads; diff --git a/Pow3r/Program.Simulation.cs b/Pow3r/Program.Simulation.cs index 440e262f83..fac2dbc415 100644 --- a/Pow3r/Program.Simulation.cs +++ b/Pow3r/Program.Simulation.cs @@ -10,7 +10,6 @@ namespace Pow3r { private const int MaxTickData = 180; - private int _nextId = 1; private PowerState _state = new(); private Network _linking; private int _tickDataIdx; @@ -18,13 +17,11 @@ namespace Pow3r private readonly string[] _solverNames = { - nameof(GraphWalkSolver), nameof(BatteryRampPegSolver), nameof(NoOpSolver) }; private readonly IPowerSolver[] _solvers = { - new GraphWalkSolver(), new BatteryRampPegSolver(), new NoOpSolver() }; @@ -35,11 +32,6 @@ namespace Pow3r private readonly Queue _remQueue = new(); private readonly Stopwatch _simStopwatch = new Stopwatch(); - private NodeId AllocId() - { - return new(_nextId++); - } - private void Tick(float frameTime) { if (_paused) diff --git a/Pow3r/Program.UI.cs b/Pow3r/Program.UI.cs index 96f7b90cf8..9a99a52699 100644 --- a/Pow3r/Program.UI.cs +++ b/Pow3r/Program.UI.cs @@ -36,30 +36,30 @@ namespace Pow3r if (Button("Generator")) { - var id = AllocId(); - _state.Supplies.Add(id, new Supply { Id = id }); - _displaySupplies.Add(id, new DisplaySupply()); + var supply = new Supply(); + _state.Supplies.Allocate(out supply.Id) = supply; + _displaySupplies.Add(supply.Id, new DisplaySupply()); } if (Button("Load")) { - var id = AllocId(); - _state.Loads.Add(id, new Load { Id = id }); - _displayLoads.Add(id, new DisplayLoad()); + var load = new Load(); + _state.Loads.Allocate(out load.Id) = load; + _displayLoads.Add(load.Id, new DisplayLoad()); } if (Button("Network")) { - var id = AllocId(); - _state.Networks.Add(id, new Network { Id = id }); - _displayNetworks.Add(id, new DisplayNetwork()); + var network = new Network(); + _state.Networks.Allocate(out network.Id) = network; + _displayNetworks.Add(network.Id, new DisplayNetwork()); } if (Button("Battery")) { - var id = AllocId(); - _state.Batteries.Add(id, new Battery { Id = id }); - _displayBatteries.Add(id, new DisplayBattery()); + var battery = new Battery(); + _state.Batteries.Allocate(out battery.Id) = battery; + _displayBatteries.Add(battery.Id, new DisplayBattery()); } Checkbox("Paused", ref _paused); @@ -355,25 +355,25 @@ namespace Pow3r switch (item) { case Network n: - _state.Networks.Remove(n.Id); + _state.Networks.Free(n.Id); _displayNetworks.Remove(n.Id); reLink = true; break; case Supply s: - _state.Supplies.Remove(s.Id); + _state.Supplies.Free(s.Id); _state.Networks.Values.ForEach(n => n.Supplies.Remove(s.Id)); _displaySupplies.Remove(s.Id); break; case Load l: - _state.Loads.Remove(l.Id); + _state.Loads.Free(l.Id); _state.Networks.Values.ForEach(n => n.Loads.Remove(l.Id)); _displayLoads.Remove(l.Id); break; case Battery b: - _state.Batteries.Remove(b.Id); + _state.Batteries.Free(b.Id); _state.Networks.Values.ForEach(n => n.BatteriesCharging.Remove(b.Id)); _state.Networks.Values.ForEach(n => n.BatteriesDischarging.Remove(b.Id)); _displayBatteries.Remove(b.Id); diff --git a/Resources/Changelog/Changelog.yml b/Resources/Changelog/Changelog.yml index 62bb69b3ae..085c58872c 100644 --- a/Resources/Changelog/Changelog.yml +++ b/Resources/Changelog/Changelog.yml @@ -1613,3 +1613,10 @@ Entries: - {message: Added bio suits into bio lockers, type: Add} id: 287 time: '2021-07-28T17:36:06.0000000+00:00' +- author: PJB3005 + changes: + - {message: Pulling is better integrated with hands now. It properly picks an empty + hand and dropping on the pulling hand stops the pull. You also see what you're + pulling in your hand!, type: Tweak} + id: 288 + time: '2021-07-31T01:14:00.0000000+00:00' diff --git a/Resources/Prototypes/Entities/Clothing/Head/base.yml b/Resources/Prototypes/Entities/Clothing/Head/base.yml index c81acedf5c..ea2aa62164 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/base.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/base.yml @@ -47,7 +47,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml index e8e403ae9b..ab1d35e31f 100644 --- a/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml +++ b/Resources/Prototypes/Entities/Clothing/Head/hardhats.yml @@ -15,7 +15,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml index d9481657b5..8863546e1c 100644 --- a/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml +++ b/Resources/Prototypes/Entities/Clothing/Shoes/specific.yml @@ -19,7 +19,6 @@ sprite: Clothing/Shoes/Specific/clown.rsi - type: Clothing sprite: Clothing/Shoes/Specific/clown.rsi - - type: LoopingSound - type: FootstepModifier footstepSoundCollection: footstep_clown diff --git a/Resources/Prototypes/Entities/Effects/puddle.yml b/Resources/Prototypes/Entities/Effects/puddle.yml index 300ccbcf6e..6e018f8f0b 100644 --- a/Resources/Prototypes/Entities/Effects/puddle.yml +++ b/Resources/Prototypes/Entities/Effects/puddle.yml @@ -10,7 +10,6 @@ - type: Puddle spill_sound: /Audio/Effects/Fluids/splat.ogg recolor: true - - type: LoopingSound - type: Clickable - type: Slippery - type: Physics diff --git a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml index 4ef1d7ba87..45b1b8e193 100644 --- a/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml +++ b/Resources/Prototypes/Entities/Mobs/NPCs/animals.yml @@ -387,7 +387,6 @@ # Eek! You can eat them alive for now until someone makes something that detects when # a mob is dead or something idk - type: Food - - type: LoopingSound - type: SolutionContainer contents: reagents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml index 5154e389c5..5e08078666 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/bread.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/bread.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml index 5f792c0c8e..0ecb6b5fa6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/cake.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/cake.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml index 25b22164db..5f71cd9431 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/donut.yml @@ -13,7 +13,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/donut.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 5 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml index d697dd849f..416fcd82ff 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/misc.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/misc.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 5 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml index c5d94a8ded..2a5c2fa711 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Baked/pizza.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/Baked/pizza.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 15 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml index da91d4b718..b80ef6f762 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/box.yml @@ -92,7 +92,6 @@ - box11 - box12 # Someday... - # - type: LoopingSound # - type: DamageOnLand # amount: 5 # - type: DamageOtherOnHit diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml index f37b3c2670..fa7bc163fa 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/Containers/tin.yml @@ -78,7 +78,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinPeachesTrash @@ -127,7 +126,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinPeachesMaintTrash @@ -176,7 +174,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinBeansTrash @@ -227,7 +224,6 @@ reagents: - ReagentId: Nutriment Quantity: 15 - - type: LoopingSound - type: Food trash: FoodTinMRETrash diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml index 956cbabd68..7f4dfafec8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/burger.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/burger.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 15 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml index 886827dcd8..06c818d1b9 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/egg.yml @@ -23,7 +23,6 @@ reagents: - ReagentId: Egg Quantity: 5 - - type: LoopingSound - type: DamageOnLand amount: 1 - type: DamageOtherOnHit diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml index 86d65c8845..745d0ec100 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/frozen.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/frozen.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 # For sprinkles or something? Idk. contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml index c04c0e016d..fef5800303 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/meat.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/meat.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml index 5c0f7ebce4..02ee16cf0c 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/noodles.yml @@ -11,7 +11,6 @@ - type: Sprite sprite: Objects/Consumable/Food/noodles.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml index 704e8a7376..3de439686d 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/skewer.yml @@ -10,7 +10,6 @@ - type: Sprite sprite: Objects/Consumable/Food/skewer.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml index b3fe15a37c..2fcb644269 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/snacks.yml @@ -9,7 +9,6 @@ - type: Sprite sprite: Objects/Consumable/Food/snacks.rsi netsync: false - - type: LoopingSound - type: SolutionContainer maxVol: 30 # Room for extra condiments contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml index 95d8810b6b..d7a7ab1de6 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/Food/soup.yml @@ -5,7 +5,6 @@ components: - type: Food trash: FoodBowlBig - - type: LoopingSound - type: SolutionContainer maxVol: 20 contents: diff --git a/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml b/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml index 25c6f54d7c..eb10d855f8 100644 --- a/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml +++ b/Resources/Prototypes/Entities/Objects/Consumable/trash_drinks.yml @@ -6,7 +6,6 @@ id: DrinkBottleBaseEmpty description: That's an empty bottle. components: - - type: LoopingSound - type: Sprite state: icon - type: SolutionContainer diff --git a/Resources/Prototypes/Entities/Objects/Devices/pda.yml b/Resources/Prototypes/Entities/Objects/Devices/pda.yml index f52578dce9..cf36a7dfa6 100644 --- a/Resources/Prototypes/Entities/Objects/Devices/pda.yml +++ b/Resources/Prototypes/Entities/Objects/Devices/pda.yml @@ -28,7 +28,6 @@ interfaces: - key: enum.PDAUiKey.Key type: PDABoundUserInterface - - type: LoopingSound - type: entity parent: BasePDA diff --git a/Resources/Prototypes/Entities/Objects/Fun/skub.yml b/Resources/Prototypes/Entities/Objects/Fun/skub.yml index e4695f5062..6f1e2e6842 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/skub.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/skub.yml @@ -10,7 +10,6 @@ - type: Item sprite: Objects/Misc/skub.rsi - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/skub.ogg diff --git a/Resources/Prototypes/Entities/Objects/Fun/toys.yml b/Resources/Prototypes/Entities/Objects/Fun/toys.yml index 0d8babe815..3ab1f035c5 100644 --- a/Resources/Prototypes/Entities/Objects/Fun/toys.yml +++ b/Resources/Prototypes/Entities/Objects/Fun/toys.yml @@ -5,7 +5,7 @@ id: BasePlushie components: - type: EmitSoundOnUse - sound: + sound: collection: ToySqueak - type: EmitSoundOnLand sound: @@ -13,7 +13,6 @@ - type: EmitSoundOnActivate sound: collection: ToySqueak - - type: LoopingSound - type: ItemCooldown - type: UseDelay delay: 1.0 @@ -91,7 +90,6 @@ sprite: Objects/Fun/toys.rsi state: plushie_snake - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/rattle.ogg @@ -108,7 +106,6 @@ sprite: Objects/Fun/toys.rsi state: toy_mouse - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/mousesqueek.ogg @@ -125,7 +122,6 @@ sprite: Objects/Fun/toys.rsi state: plushie_vox - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Voice/Vox/shriek1.ogg @@ -151,7 +147,6 @@ sound: path: /Audio/Items/Toys/helpme.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/helpme.ogg @@ -170,12 +165,11 @@ - type: Item sprite: Objects/Misc/carvings.rsi - type: EmitSoundOnThrow - sound: + sound: path: /Audio/Items/Toys/hellothere.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/hellothere.ogg - type: UseDelay delay: 1.0 @@ -195,9 +189,8 @@ sound: path: /Audio/Items/Toys/thankyou.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/thankyou.ogg - type: UseDelay delay: 1.0 @@ -214,12 +207,11 @@ - type: Item sprite: Objects/Misc/carvings.rsi - type: EmitSoundOnThrow - sound: + sound: path: /Audio/Items/Toys/verygood.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse - sound: + sound: path: /Audio/Items/Toys/verygood.ogg - type: UseDelay delay: 1.0 @@ -239,7 +231,6 @@ sound: path: /Audio/Items/Toys/imsorry.ogg - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/imsorry.ogg @@ -311,7 +302,6 @@ sprite: Objects/Fun/toys.rsi state: ian - type: ItemCooldown - - type: LoopingSound - type: EmitSoundOnUse sound: path: /Audio/Items/Toys/ian.ogg diff --git a/Resources/Prototypes/Entities/Objects/Misc/torch.yml b/Resources/Prototypes/Entities/Objects/Misc/torch.yml index e2ca2f54ec..605ac31e3b 100644 --- a/Resources/Prototypes/Entities/Objects/Misc/torch.yml +++ b/Resources/Prototypes/Entities/Objects/Misc/torch.yml @@ -29,7 +29,6 @@ - type: Item sprite: Objects/Misc/torch.rsi HeldPrefix: unlit - - type: LoopingSound - type: Construction graph: lightTorch node: torch diff --git a/Resources/Prototypes/Entities/Objects/Tools/flare.yml b/Resources/Prototypes/Entities/Objects/Tools/flare.yml index 88f24071e3..cf2d18ee78 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flare.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flare.yml @@ -32,7 +32,6 @@ sprite: Objects/Misc/flare.rsi color: "#FF0000" HeldPrefix: unlit - - type: LoopingSound - type: Appearance visuals: - type: ExpendableLightVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml b/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml index cf5bff920b..80ffa05614 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/flashlight.yml @@ -23,7 +23,6 @@ - type: PointLight enabled: false radius: 3 - - type: LoopingSound - type: Appearance visuals: - type: FlashLightVisualizer diff --git a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml index e50fc08d74..11fc7a70db 100644 --- a/Resources/Prototypes/Entities/Objects/Tools/lantern.yml +++ b/Resources/Prototypes/Entities/Objects/Tools/lantern.yml @@ -23,7 +23,6 @@ radius: 3 energy: 2.5 color: "#FFC458" - - type: LoopingSound - type: Appearance visuals: - type: LanternVisualizer @@ -39,4 +38,3 @@ radius: 5 energy: 10 color: "#FFC458" - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml index ec7cf4e96a..39f1b8bf96 100644 --- a/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml +++ b/Resources/Prototypes/Entities/Objects/Weapons/Melee/pickaxe.yml @@ -17,4 +17,3 @@ size: 24 sprite: Objects/Weapons/Melee/pickaxe.rsi prefix: inhand - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Dispensers/base.yml b/Resources/Prototypes/Entities/Structures/Dispensers/base.yml index 412c455d39..2f33762b74 100644 --- a/Resources/Prototypes/Entities/Structures/Dispensers/base.yml +++ b/Resources/Prototypes/Entities/Structures/Dispensers/base.yml @@ -26,7 +26,6 @@ interfaces: - key: enum.ReagentDispenserUiKey.Key type: ReagentDispenserBoundUserInterface - - type: LoopingSound - type: Anchorable - type: Pullable - type: Damageable diff --git a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml index aa9093711a..4210bdcac9 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/microwave.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/microwave.yml @@ -11,7 +11,6 @@ - type: Appearance visuals: - type: MicrowaveVisualizer - - type: LoopingSound - type: UserInterface interfaces: - key: enum.MicrowaveUiKey.Key diff --git a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml index 4a9a89d5a2..1ab00ae0d5 100644 --- a/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml +++ b/Resources/Prototypes/Entities/Structures/Machines/reagent_grinder.yml @@ -14,7 +14,6 @@ - type: Appearance visuals: - type: ReagentGrinderVisualizer - - type: LoopingSound - type: Physics fixtures: - shape: diff --git a/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml index 7c387a93d4..b64910f093 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Closets/base.yml @@ -57,4 +57,3 @@ - type: StorageVisualizer state_open: generic_open state_closed: generic_door - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml index eec102958c..2177b328c2 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/Crates/base.yml @@ -49,4 +49,3 @@ - type: StorageVisualizer state_open: crate_open state_closed: crate_door - - type: LoopingSound diff --git a/Resources/Prototypes/Entities/Structures/Storage/morgue.yml b/Resources/Prototypes/Entities/Structures/Storage/morgue.yml index a4dab64e39..92e4e35a4e 100644 --- a/Resources/Prototypes/Entities/Structures/Storage/morgue.yml +++ b/Resources/Prototypes/Entities/Structures/Storage/morgue.yml @@ -105,7 +105,6 @@ openSound: /Audio/Items/deconstruct.ogg trayPrototype: CrematoriumTray doSoulBeep: false - - type: LoopingSound - type: Appearance visuals: - type: CrematoriumVisualizer diff --git a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml index 620f13a43d..de20894f6d 100644 --- a/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml +++ b/Resources/Prototypes/Entities/Structures/Wallmounts/lighting.yml @@ -16,7 +16,6 @@ !type:PhysShapeAabb bounds: "-0.45, -0.15, 0.45, 0.35" layer: [ Passable ] - - type: LoopingSound - type: Sprite sprite: Structures/Wallmounts/Lighting/light_tube.rsi layers: diff --git a/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml b/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml new file mode 100644 index 0000000000..aa5b7bdb22 --- /dev/null +++ b/Resources/Prototypes/Entities/Virtual/virtual_pull_item.yml @@ -0,0 +1,8 @@ +# This item is stored in the hand slot while you are pulling something, to represent that and what you are pulling. +- type: entity + id: HandVirtualPull + name: VIRTUAL PULL YOU SHOULD NOT SEE THIS + abstract: true + components: + - type: Item + - type: HandVirtualPull diff --git a/RobustToolbox b/RobustToolbox index e93c0f76a9..8fea42ff9a 160000 --- a/RobustToolbox +++ b/RobustToolbox @@ -1 +1 @@ -Subproject commit e93c0f76a9ea5021754b6c9b5f1819e0a46b8f4b +Subproject commit 8fea42ff9ac05dfef40f9fec0dfd051ba054dbfa