diff --git a/Content.Client/Access/IdCardConsoleSystem.cs b/Content.Client/Access/IdCardConsoleSystem.cs index 27577d66e2..f875204bfd 100644 --- a/Content.Client/Access/IdCardConsoleSystem.cs +++ b/Content.Client/Access/IdCardConsoleSystem.cs @@ -1,4 +1,4 @@ -using Content.Shared.Access; +using Content.Shared.Access.Systems; using JetBrains.Annotations; namespace Content.Client.Access diff --git a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs index 819ac7f442..e492d7e89b 100644 --- a/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs +++ b/Content.Client/Access/UI/IdCardConsoleBoundUserInterface.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; -using Content.Shared.Access; using Robust.Client.GameObjects; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Prototypes; -using static Content.Shared.Access.SharedIdCardConsoleComponent; +using static Content.Shared.Access.Components.SharedIdCardConsoleComponent; namespace Content.Client.Access.UI { diff --git a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs index dfb35f36ac..e835c7e6f7 100644 --- a/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs +++ b/Content.Client/Access/UI/IdCardConsoleWindow.xaml.cs @@ -8,7 +8,7 @@ using Robust.Client.UserInterface.XAML; using Robust.Shared.Localization; using Robust.Shared.Maths; using Robust.Shared.Prototypes; -using static Content.Shared.Access.SharedIdCardConsoleComponent; +using static Content.Shared.Access.Components.SharedIdCardConsoleComponent; namespace Content.Client.Access.UI { diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs index e5ba6ea452..81c845b3cd 100644 --- a/Content.Client/Entry/IgnoredComponents.cs +++ b/Content.Client/Entry/IgnoredComponents.cs @@ -46,7 +46,6 @@ namespace Content.Client.Entry "ResearchServer", "ResearchPointSource", "ResearchClient", - "IdCard", "Access", "AccessReader", "IdCardConsole", @@ -155,7 +154,6 @@ namespace Content.Client.Entry "RefillableSolution", "DrainableSolution", "ExaminableSolution", - "FitsInDispenser", "DrawableSolution", "InjectableSolution", "Barotrauma", @@ -295,7 +293,6 @@ namespace Content.Client.Entry "BatteryCharger", "UnpoweredFlashlight", "Uplink", - "PDA", "SpawnItemsOnUse", "AmbientOnPowered", "Wieldable", diff --git a/Content.Client/Items/Managers/ItemSlotManager.cs b/Content.Client/Items/Managers/ItemSlotManager.cs index c8a6171225..83d250e792 100644 --- a/Content.Client/Items/Managers/ItemSlotManager.cs +++ b/Content.Client/Items/Managers/ItemSlotManager.cs @@ -80,7 +80,7 @@ namespace Content.Client.Items.Managers } else if (args.Function == ContentKeyFunctions.AltActivateItemInWorld) { - _entityManager.EntityNetManager?.SendSystemNetworkMessage(new InteractInventorySlotEvent(item, altInteract: true)); + _entityManager.RaisePredictiveEvent(new InteractInventorySlotEvent(item, altInteract: true)); } else { diff --git a/Content.Client/PDA/PDASystem.cs b/Content.Client/PDA/PDASystem.cs new file mode 100644 index 0000000000..cec18d90b3 --- /dev/null +++ b/Content.Client/PDA/PDASystem.cs @@ -0,0 +1,10 @@ +using Content.Shared.PDA; +using Robust.Shared.GameObjects; + +namespace Content.Client.PDA +{ + public sealed class PDASystem : SharedPDASystem + { + // Nothing here. Have a lovely day. + } +} diff --git a/Content.Client/Verbs/VerbSystem.cs b/Content.Client/Verbs/VerbSystem.cs index 51f1a03d75..5ba5a07571 100644 --- a/Content.Client/Verbs/VerbSystem.cs +++ b/Content.Client/Verbs/VerbSystem.cs @@ -192,27 +192,43 @@ namespace Content.Client.Verbs /// Execute actions associated with the given verb. /// /// - /// Unless this is a client-exclusive verb, this will also tell the server to run the same verb. However, if the verb - /// is disabled and has a tooltip, this function will only generate a pop-up-message instead of executing anything. + /// Unless this is a client-exclusive verb, this will also tell the server to run the same verb. /// public void ExecuteVerb(EntityUid target, Verb verb, VerbType verbType) { - if (verb.Disabled) - { - if (verb.Message != null) - _popupSystem.PopupCursor(verb.Message); - return; - } - var user = _playerManager.LocalPlayer?.ControlledEntity; if (user == null) return; - ExecuteVerb(verb, user.Value, target); - - if (!verb.ClientExclusive) + // is this verb actually valid? + if (verb.Disabled) { - RaiseNetworkEvent(new ExecuteVerbEvent(target, verb, verbType)); + // maybe send an informative pop-up message. + if (!string.IsNullOrWhiteSpace(verb.Message)) + _popupSystem.PopupEntity(verb.Message, user.Value); + + return; + } + + if (verb.ClientExclusive) + // is this a client exclusive (gui) verb? + ExecuteVerb(verb, user.Value, target); + else + EntityManager.RaisePredictiveEvent(new ExecuteVerbEvent(target, verb, verbType)); + } + + public override void ExecuteVerb(Verb verb, EntityUid user, EntityUid target, bool forced = false) + { + // invoke any relevant actions + verb.Act?.Invoke(); + + // Maybe raise a local event + if (verb.ExecutionEventArgs != null) + { + if (verb.EventTarget.IsValid()) + RaiseLocalEvent(verb.EventTarget, verb.ExecutionEventArgs); + else + RaiseLocalEvent(verb.ExecutionEventArgs); } } diff --git a/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs b/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs index fa3c862397..0ac5212b61 100644 --- a/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs +++ b/Content.IntegrationTests/Tests/PDA/PDAExtensionsTests.cs @@ -1,11 +1,12 @@ using System.Linq; using System.Threading.Tasks; -using Content.Server.Access.Components; using Content.Server.Hands.Components; using Content.Server.Inventory.Components; using Content.Server.Items; using Content.Server.PDA; +using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; +using Content.Shared.PDA; using NUnit.Framework; using Robust.Server.Player; using Robust.Shared.GameObjects; @@ -78,7 +79,7 @@ namespace Content.IntegrationTests.Tests.PDA var itemSlots = sEntityManager.GetComponent(dummyPda); sEntityManager.EntitySysManager.GetEntitySystem() - .TryInsert(dummyPda, pdaComponent.IdSlot, pdaIdCard); + .TryInsert(dummyPda, pdaComponent.IdSlot, pdaIdCard, null); var pdaContainedId = pdaComponent.ContainedID; // The PDA in the hand should be found first diff --git a/Content.Server/Access/Components/IdCardConsoleComponent.cs b/Content.Server/Access/Components/IdCardConsoleComponent.cs index e31c97fedb..e505a40b16 100644 --- a/Content.Server/Access/Components/IdCardConsoleComponent.cs +++ b/Content.Server/Access/Components/IdCardConsoleComponent.cs @@ -4,6 +4,7 @@ using Content.Server.Access.Systems; using Content.Server.Power.Components; using Content.Server.UserInterface; using Content.Shared.Access; +using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; @@ -115,6 +116,9 @@ namespace Content.Server.Access.Components public void UpdateUserInterface() { + if (!Initialized) + return; + IdCardConsoleBoundUserInterfaceState newState; // this could be prettier if (TargetIdSlot.Item is not {Valid: true} targetIdEntity) diff --git a/Content.Server/Access/Systems/AccessReaderSystem.cs b/Content.Server/Access/Systems/AccessReaderSystem.cs index 25c81350e5..8157646f8b 100644 --- a/Content.Server/Access/Systems/AccessReaderSystem.cs +++ b/Content.Server/Access/Systems/AccessReaderSystem.cs @@ -5,10 +5,10 @@ using System.Linq; using Content.Server.Access.Components; using Content.Server.Inventory.Components; using Content.Server.Items; -using Content.Server.PDA; using Content.Shared.Access; using Content.Shared.Hands.Components; using Content.Shared.Inventory; +using Content.Shared.PDA; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; diff --git a/Content.Server/Access/Systems/IdCardConsoleSystem.cs b/Content.Server/Access/Systems/IdCardConsoleSystem.cs index 3e3740510b..235a989cea 100644 --- a/Content.Server/Access/Systems/IdCardConsoleSystem.cs +++ b/Content.Server/Access/Systems/IdCardConsoleSystem.cs @@ -1,5 +1,5 @@ using Content.Server.Access.Components; -using Content.Shared.Access; +using Content.Shared.Access.Systems; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; diff --git a/Content.Server/Access/Systems/IdCardSystem.cs b/Content.Server/Access/Systems/IdCardSystem.cs index 3c363206cc..c1bcaff58e 100644 --- a/Content.Server/Access/Systems/IdCardSystem.cs +++ b/Content.Server/Access/Systems/IdCardSystem.cs @@ -1,18 +1,17 @@ -using Content.Server.Access.Components; using Content.Server.Inventory.Components; using Content.Server.Items; -using Content.Server.PDA; -using Content.Shared.Access; using Content.Shared.Hands.Components; using Content.Shared.Inventory; +using Content.Shared.PDA; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using System.Diagnostics.CodeAnalysis; -using Robust.Shared.IoC; +using Content.Shared.Access.Components; +using Content.Shared.Access.Systems; namespace Content.Server.Access.Systems { - public class IdCardSystem : EntitySystem + public class IdCardSystem : SharedIdCardSystem { public override void Initialize() { diff --git a/Content.Server/Administration/Commands/SetOutfitCommand.cs b/Content.Server/Administration/Commands/SetOutfitCommand.cs index 6bf82f8a95..31c3891a4b 100644 --- a/Content.Server/Administration/Commands/SetOutfitCommand.cs +++ b/Content.Server/Administration/Commands/SetOutfitCommand.cs @@ -2,10 +2,10 @@ using Content.Server.Administration.UI; using Content.Server.EUI; using Content.Server.Inventory.Components; using Content.Server.Items; -using Content.Server.PDA; using Content.Server.Preferences.Managers; using Content.Shared.Administration; using Content.Shared.Inventory; +using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Roles; using Robust.Server.GameObjects; diff --git a/Content.Server/Cabinet/ItemCabinetSystem.cs b/Content.Server/Cabinet/ItemCabinetSystem.cs index 628cf32005..b678b523fb 100644 --- a/Content.Server/Cabinet/ItemCabinetSystem.cs +++ b/Content.Server/Cabinet/ItemCabinetSystem.cs @@ -59,6 +59,8 @@ namespace Content.Server.Cabinet private void OnContainerModified(EntityUid uid, ItemCabinetComponent cabinet, ContainerModifiedMessage args) { + if (!cabinet.Initialized) return; + if (args.Container.ID == cabinet.CabinetSlot.ID) UpdateAppearance(uid, cabinet); } diff --git a/Content.Server/Cargo/Components/CargoTelepadComponent.cs b/Content.Server/Cargo/Components/CargoTelepadComponent.cs index b43937af93..8a8b1502cf 100644 --- a/Content.Server/Cargo/Components/CargoTelepadComponent.cs +++ b/Content.Server/Cargo/Components/CargoTelepadComponent.cs @@ -142,7 +142,7 @@ namespace Content.Server.Cargo.Components // attempt to attach the label if (_entMan.TryGetComponent(product, out PaperLabelComponent label)) { - EntitySystem.Get().TryInsert(Owner, label.LabelSlot, printed); + EntitySystem.Get().TryInsert(Owner, label.LabelSlot, printed, null); } } diff --git a/Content.Server/Chemistry/Components/ChemMasterComponent.cs b/Content.Server/Chemistry/Components/ChemMasterComponent.cs index afba36220f..dee840b3e4 100644 --- a/Content.Server/Chemistry/Components/ChemMasterComponent.cs +++ b/Content.Server/Chemistry/Components/ChemMasterComponent.cs @@ -197,6 +197,8 @@ namespace Content.Server.Chemistry.Components public void UpdateUserInterface() { + if (!Initialized) return; + var state = GetUserInterfaceState(); UserInterface?.SetState(state); } diff --git a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs index 19c63bba8f..4de31b1bb9 100644 --- a/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs +++ b/Content.Server/Chemistry/Components/ReagentDispenserComponent.cs @@ -241,6 +241,8 @@ namespace Content.Server.Chemistry.Components public void UpdateUserInterface() { + if (!Initialized) return; + var state = GetUserInterfaceState(); UserInterface?.SetState(state); } diff --git a/Content.Server/Chemistry/Components/TransformableContainerComponent.cs b/Content.Server/Chemistry/Components/TransformableContainerComponent.cs index 69f4a527e2..1e3b821327 100644 --- a/Content.Server/Chemistry/Components/TransformableContainerComponent.cs +++ b/Content.Server/Chemistry/Components/TransformableContainerComponent.cs @@ -1,4 +1,5 @@ using Content.Server.Chemistry.Components.SolutionManager; +using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.Reagent; using Robust.Server.GameObjects; using Robust.Shared.GameObjects; diff --git a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs index 87e0a96928..c335ebfbaa 100644 --- a/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs +++ b/Content.Server/Chemistry/EntitySystems/HypospraySystem.cs @@ -1,4 +1,4 @@ -using Content.Server.Chemistry.Components; +using Content.Server.Chemistry.Components; using Content.Shared.Interaction; using Content.Shared.Weapons.Melee; using JetBrains.Annotations; diff --git a/Content.Server/GameTicking/GameTicker.Spawning.cs b/Content.Server/GameTicking/GameTicker.Spawning.cs index 83c45375e8..c93d803727 100644 --- a/Content.Server/GameTicking/GameTicker.Spawning.cs +++ b/Content.Server/GameTicking/GameTicker.Spawning.cs @@ -21,6 +21,7 @@ using Content.Shared.Database; using Content.Shared.GameTicking; using Content.Shared.Ghost; using Content.Shared.Inventory; +using Content.Shared.PDA; using Content.Shared.Preferences; using Content.Shared.Roles; using Content.Shared.Station; diff --git a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs index 8e240b9bc8..b30fbf506b 100644 --- a/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs +++ b/Content.Server/GameTicking/Presets/PresetTraitorDeathMatch.cs @@ -20,6 +20,7 @@ using Content.Shared.Damage; using Content.Shared.Damage.Prototypes; using Content.Shared.Inventory; using Content.Shared.MobState.Components; +using Content.Shared.PDA; using Content.Shared.Traitor.Uplink; using Robust.Server.Player; using Robust.Shared.Configuration; diff --git a/Content.Server/Interaction/InteractionSystem.cs b/Content.Server/Interaction/InteractionSystem.cs index c5542c14c3..f21ee116ef 100644 --- a/Content.Server/Interaction/InteractionSystem.cs +++ b/Content.Server/Interaction/InteractionSystem.cs @@ -41,19 +41,17 @@ namespace Content.Server.Interaction { [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly PullingSystem _pullSystem = default!; - [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; [Dependency] private readonly AdminLogSystem _adminLogSystem = default!; public override void Initialize() { + base.Initialize(); + SubscribeNetworkEvent(HandleDragDropRequestEvent); - SubscribeNetworkEvent(HandleInteractInventorySlotEvent); CommandBinds.Builder .Bind(EngineKeyFunctions.Use, new PointerInputCmdHandler(HandleUseInteraction)) - .Bind(ContentKeyFunctions.AltActivateItemInWorld, - new PointerInputCmdHandler(HandleAltUseInteraction)) .Bind(ContentKeyFunctions.WideAttack, new PointerInputCmdHandler(HandleWideAttack)) .Bind(ContentKeyFunctions.ActivateItemInWorld, @@ -69,36 +67,6 @@ namespace Content.Server.Interaction base.Shutdown(); } - #region Client Input Validation - private bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, EntityUid uid, [NotNullWhen(true)] out EntityUid? userEntity) - { - userEntity = null; - - if (!coords.IsValid(EntityManager)) - { - Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); - return false; - } - - if (uid.IsClientSide()) - { - Logger.WarningS("system.interaction", - $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); - return false; - } - - userEntity = ((IPlayerSession?) session)?.AttachedEntity; - - if (userEntity == null || !userEntity.Value.IsValid()) - { - Logger.WarningS("system.interaction", - $"Client sent interaction with no attached entity. Session={session}"); - return false; - } - - return true; - } - public override bool CanAccessViaStorage(EntityUid user, EntityUid target) { if (Deleted(target)) @@ -119,37 +87,6 @@ namespace Content.Server.Interaction // we don't check if the user can access the storage entity itself. This should be handed by the UI system. return storage.SubscribedSessions.Contains(actor.PlayerSession); } - #endregion - - /// - /// Handles the event were a client uses an item in their inventory or in their hands, either by - /// alt-clicking it or pressing 'E' while hovering over it. - /// - private void HandleInteractInventorySlotEvent(InteractInventorySlotEvent msg, EntitySessionEventArgs args) - { - if (Deleted(msg.ItemUid)) - { - Logger.WarningS("system.interaction", - $"Client sent inventory interaction with an invalid target item. Session={args.SenderSession}"); - return; - } - - var itemCoords = Transform(msg.ItemUid).Coordinates; - - // client sanitization - if (!ValidateClientInput(args.SenderSession, itemCoords, msg.ItemUid, out var userEntity)) - { - Logger.InfoS("system.interaction", $"Inventory interaction validation failed. Session={args.SenderSession}"); - return; - } - - if (msg.AltInteract) - // Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world. - UserInteraction(userEntity.Value, itemCoords, msg.ItemUid, msg.AltInteract); - else - // User used 'E'. We want to activate it, not simulate clicking on the item - InteractionActivate(userEntity.Value, msg.ItemUid); - } #region Drag drop private void HandleDragDropRequestEvent(DragDropRequestEvent msg, EntitySessionEventArgs args) @@ -258,20 +195,6 @@ namespace Content.Server.Interaction return true; } - public bool HandleAltUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) - { - // client sanitization - if (!ValidateClientInput(session, coords, uid, out var userEntity)) - { - Logger.InfoS("system.interaction", $"Alt-use input validation failed"); - return true; - } - - UserInteraction(userEntity.Value, coords, !Deleted(uid) ? uid : null, altInteract : true ); - - return true; - } - private bool HandleTryPullObject(ICommonSession? session, EntityCoordinates coords, EntityUid uid) { if (!ValidateClientInput(session, coords, uid, out var userEntity)) @@ -295,97 +218,14 @@ namespace Content.Server.Interaction return _pullSystem.TogglePull(userEntity.Value, pull); } - /// - /// Resolves user interactions with objects. - /// - /// - /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. - /// - /// Whether to use default or alternative interactions (usually as a result of - /// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat - /// interaction. Having an item in the active hand also disables alternative interactions. - public async void UserInteraction(EntityUid user, EntityCoordinates coordinates, EntityUid? target, bool altInteract = false) - { - // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? - if (!altInteract && TryComp(user, out CombatModeComponent? combatMode) && combatMode.IsInCombatMode) - { - DoAttack(user, coordinates, false, target); - return; - } - - if (!ValidateInteractAndFace(user, coordinates)) - return; - - if (!_actionBlockerSystem.CanInteract(user)) - return; - - // Check if interacted entity is in the same container, the direct child, or direct parent of the user. - // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) - if (target != null && !Deleted(target.Value) && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) - { - Logger.WarningS("system.interaction", - $"User entity {ToPrettyString(user):user} clicked on object {ToPrettyString(target.Value):target} that isn't the parent, child, or in the same container"); - return; - } - - // Verify user has a hand, and find what object they are currently holding in their active hand - if (!TryComp(user, out HandsComponent? hands)) - return; - - var item = hands.GetActiveHand?.Owner; - - // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. - var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); - if (target == null || Deleted(target.Value) || !inRangeUnobstructed) - { - if (item == null) - return; - - if (!await InteractUsingRanged(user, item.Value, target, coordinates, inRangeUnobstructed) && - !inRangeUnobstructed) - { - var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); - user.PopupMessage(message); - } - - return; - } - - // We are close to the nearby object. - if (altInteract) - // Perform alternative interactions, using context menu verbs. - AltInteract(user, target.Value); - else if (item != null && item != target) - // We are performing a standard interaction with an item, and the target isn't the same as the item - // currently in our hand. We will use the item in our hand on the nearby object via InteractUsing - await InteractUsing(user, item.Value, target.Value, coordinates); - else if (item == null) - // Since our hand is empty we will use InteractHand/Activate - InteractHand(user, target.Value); - } - - private bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates) - { - // Verify user is on the same map as the entity they clicked on - if (coordinates.GetMapId(EntityManager) != Transform(user).MapID) - { - Logger.WarningS("system.interaction", - $"User entity {ToPrettyString(user):user} clicked on a map they aren't located on"); - return false; - } - - _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager)); - - return true; - } - /// /// Uses an empty hand on an entity /// Finds components with the InteractHand interface and calls their function /// NOTE: Does not have an InRangeUnobstructed check /// - public void InteractHand(EntityUid user, EntityUid target) + public override void InteractHand(EntityUid user, EntityUid target) { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. if (!_actionBlockerSystem.CanInteract(user)) return; @@ -416,8 +256,9 @@ namespace Content.Server.Interaction /// Will have two behaviors, either "uses" the used entity at range on the target entity if it is capable of accepting that action /// Or it will use the used entity itself on the position clicked, regardless of what was there /// - public async Task InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) + public override async Task InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, EntityCoordinates clickLocation, bool inRangeUnobstructed) { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. if (InteractDoBefore(user, used, inRangeUnobstructed ? target : null, clickLocation, false)) return true; @@ -445,8 +286,9 @@ namespace Content.Server.Interaction return await InteractDoAfter(user, used, inRangeUnobstructed ? target : null, clickLocation, false); } - public void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? targetUid = null) + public override void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, EntityUid? target = null) { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. if (!ValidateInteractAndFace(user, coordinates)) return; @@ -456,10 +298,10 @@ namespace Content.Server.Interaction if (!wideAttack) { // Check if interacted entity is in the same container, the direct child, or direct parent of the user. - if (targetUid != null && !Deleted(targetUid.Value) && !user.IsInSameOrParentContainer(targetUid.Value) && !CanAccessViaStorage(user, targetUid.Value)) + if (target != null && !Deleted(target.Value) && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) { Logger.WarningS("system.interaction", - $"User entity {ToPrettyString(user):user} clicked on object {ToPrettyString(targetUid.Value):target} that isn't the parent, child, or in the same container"); + $"User entity {ToPrettyString(user):user} clicked on object {ToPrettyString(target.Value):target} that isn't the parent, child, or in the same container"); return; } @@ -488,15 +330,15 @@ namespace Content.Server.Interaction } else { - var ev = new ClickAttackEvent(item.Value, user, coordinates, targetUid); + var ev = new ClickAttackEvent(item.Value, user, coordinates, target); RaiseLocalEvent(item.Value, ev, false); if (ev.Handled) { - if (targetUid != null) + if (target != null) { _adminLogSystem.Add(LogType.AttackArmedClick, LogImpact.Medium, - $"{ToPrettyString(user):user} attacked {ToPrettyString(targetUid.Value):target} with {ToPrettyString(item.Value):used} at {coordinates}"); + $"{ToPrettyString(user):user} attacked {ToPrettyString(target.Value):target} with {ToPrettyString(item.Value):used} at {coordinates}"); } else { @@ -508,10 +350,10 @@ namespace Content.Server.Interaction } } } - else if (!wideAttack && targetUid != null && HasComp(targetUid.Value)) + else if (!wideAttack && target != null && HasComp(target.Value)) { // We pick up items if our hand is empty, even if we're in combat mode. - InteractHand(user, targetUid.Value); + InteractHand(user, target.Value); return; } } @@ -527,14 +369,14 @@ namespace Content.Server.Interaction } else { - var ev = new ClickAttackEvent(user, user, coordinates, targetUid); + var ev = new ClickAttackEvent(user, user, coordinates, target); RaiseLocalEvent(user, ev, false); if (ev.Handled) { - if (targetUid != null) + if (target != null) { _adminLogSystem.Add(LogType.AttackUnarmedClick, LogImpact.Medium, - $"{ToPrettyString(user):user} attacked {ToPrettyString(targetUid.Value):target} at {coordinates}"); + $"{ToPrettyString(user):user} attacked {ToPrettyString(target.Value):target} at {coordinates}"); } else { diff --git a/Content.Server/Labels/Label/LabelSystem.cs b/Content.Server/Labels/Label/LabelSystem.cs index 3fc98966ab..4f4621c5e0 100644 --- a/Content.Server/Labels/Label/LabelSystem.cs +++ b/Content.Server/Labels/Label/LabelSystem.cs @@ -85,9 +85,10 @@ namespace Content.Server.Labels args.PushMarkup(text.TrimEnd()); } - private void OnContainerModified(EntityUid uid, PaperLabelComponent label, ContainerModifiedMessage args) { + if (!label.Initialized) return; + if (args.Container.ID != label.LabelSlot.ID) return; diff --git a/Content.Server/Nuke/NukeSystem.cs b/Content.Server/Nuke/NukeSystem.cs index bceb21a134..5d2cee9ce0 100644 --- a/Content.Server/Nuke/NukeSystem.cs +++ b/Content.Server/Nuke/NukeSystem.cs @@ -62,6 +62,9 @@ namespace Content.Server.Nuke { component.RemainingTime = component.Timer; _itemSlots.AddItemSlot(uid, component.Name, component.DiskSlot); + + UpdateStatus(uid, component); + UpdateUserInterface(uid, component); } public override void Update(float frameTime) @@ -101,6 +104,8 @@ namespace Content.Server.Nuke private void OnItemSlotChanged(EntityUid uid, NukeComponent component, ContainerModifiedMessage args) { + if (!component.Initialized) return; + if (args.Container.ID != component.DiskSlot.ID) return; diff --git a/Content.Server/PDA/PDAExtensions.cs b/Content.Server/PDA/PDAExtensions.cs index 8787dc5165..23c5ffc0ce 100644 --- a/Content.Server/PDA/PDAExtensions.cs +++ b/Content.Server/PDA/PDAExtensions.cs @@ -1,7 +1,8 @@ -using System.Diagnostics.CodeAnalysis; -using Content.Server.Access.Components; +using System.Diagnostics.CodeAnalysis; using Content.Server.Hands.Components; using Content.Server.Inventory.Components; +using Content.Shared.Access.Components; +using Content.Shared.PDA; using Robust.Shared.GameObjects; using Robust.Shared.IoC; diff --git a/Content.Server/PDA/PDASystem.cs b/Content.Server/PDA/PDASystem.cs index 5c95c183d3..903f868ee8 100644 --- a/Content.Server/PDA/PDASystem.cs +++ b/Content.Server/PDA/PDASystem.cs @@ -1,4 +1,3 @@ -using Content.Server.Access.Components; using Content.Server.Light.Components; using Content.Server.Light.EntitySystems; using Content.Server.Light.Events; @@ -15,9 +14,8 @@ using Robust.Shared.IoC; namespace Content.Server.PDA { - public class PDASystem : EntitySystem + public sealed class PDASystem : SharedPDASystem { - [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly UplinkSystem _uplinkSystem = default!; [Dependency] private readonly UnpoweredFlashlightSystem _unpoweredFlashlight = default!; @@ -25,36 +23,21 @@ namespace Content.Server.PDA { base.Initialize(); - SubscribeLocalEvent(OnComponentInit); - SubscribeLocalEvent(OnComponentRemove); - SubscribeLocalEvent(OnActivateInWorld); SubscribeLocalEvent(OnUse); - SubscribeLocalEvent(OnItemInserted); - SubscribeLocalEvent(OnItemRemoved); SubscribeLocalEvent(OnLightToggle); - - SubscribeLocalEvent(OnUplinkInit); - SubscribeLocalEvent(OnUplinkRemoved); } - private void OnComponentInit(EntityUid uid, PDAComponent pda, ComponentInit args) + protected override void OnComponentInit(EntityUid uid, PDAComponent pda, ComponentInit args) { + base.OnComponentInit(uid, pda, args); + var ui = pda.Owner.GetUIOrNull(PDAUiKey.Key); if (ui != null) ui.OnReceiveMessage += (msg) => OnUIMessage(pda, msg); - - if (pda.IdCard != null) - pda.IdSlot.StartingItem = pda.IdCard; - _itemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-id", pda.IdSlot); - _itemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-pen", pda.PenSlot); } - private void OnComponentRemove(EntityUid uid, PDAComponent pda, ComponentRemove args) - { - _itemSlotsSystem.RemoveItemSlot(uid, pda.IdSlot); - _itemSlotsSystem.RemoveItemSlot(uid, pda.PenSlot); - } + private void OnUse(EntityUid uid, PDAComponent pda, UseInHandEvent args) { @@ -70,21 +53,15 @@ namespace Content.Server.PDA args.Handled = OpenUI(pda, args.User); } - private void OnItemInserted(EntityUid uid, PDAComponent pda, EntInsertedIntoContainerMessage args) + protected override void OnItemInserted(EntityUid uid, PDAComponent pda, EntInsertedIntoContainerMessage args) { - if (args.Container.ID == pda.IdSlot.ID) - pda.ContainedID = EntityManager.GetComponentOrNull(args.Entity); - - UpdatePDAAppearance(pda); + base.OnItemInserted(uid, pda, args); UpdatePDAUserInterface(pda); } - private void OnItemRemoved(EntityUid uid, PDAComponent pda, EntRemovedFromContainerMessage args) + protected override void OnItemRemoved(EntityUid uid, PDAComponent pda, EntRemovedFromContainerMessage args) { - if (args.Container.ID == pda.IdSlot.ID) - pda.ContainedID = null; - - UpdatePDAAppearance(pda); + base.OnItemRemoved(uid, pda, args); UpdatePDAUserInterface(pda); } @@ -162,12 +139,12 @@ namespace Content.Server.PDA case PDAEjectIDMessage _: { - _itemSlotsSystem.TryEjectToHands(pda.Owner, pda.IdSlot, playerUid); + ItemSlotsSystem.TryEjectToHands(pda.Owner, pda.IdSlot, playerUid); break; } case PDAEjectPenMessage _: { - _itemSlotsSystem.TryEjectToHands(pda.Owner, pda.PenSlot, playerUid); + ItemSlotsSystem.TryEjectToHands(pda.Owner, pda.PenSlot, playerUid); break; } case PDAShowUplinkMessage _: diff --git a/Content.Server/Sandbox/SandboxManager.cs b/Content.Server/Sandbox/SandboxManager.cs index 572dc5f4cb..4c78ce12d6 100644 --- a/Content.Server/Sandbox/SandboxManager.cs +++ b/Content.Server/Sandbox/SandboxManager.cs @@ -5,9 +5,10 @@ using Content.Server.GameTicking; using Content.Server.Hands.Components; using Content.Server.Inventory.Components; using Content.Server.Items; -using Content.Server.PDA; using Content.Shared.Access; +using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; +using Content.Shared.PDA; using Content.Shared.Sandbox; using Robust.Server.Console; using Robust.Server.GameObjects; @@ -139,7 +140,7 @@ namespace Content.Server.Sandbox if (_entityManager.TryGetComponent(pda.Owner, out ItemSlotsComponent? itemSlots)) { _entityManager.EntitySysManager.GetEntitySystem(). - TryInsert(wornItem.Owner, pda.IdSlot, newID); + TryInsert(wornItem.Owner, pda.IdSlot, newID, null); } } else diff --git a/Content.Server/Traitor/Uplink/UplinkSystem.cs b/Content.Server/Traitor/Uplink/UplinkSystem.cs index 458defd0e2..2e3bcb2915 100644 --- a/Content.Server/Traitor/Uplink/UplinkSystem.cs +++ b/Content.Server/Traitor/Uplink/UplinkSystem.cs @@ -2,13 +2,13 @@ using System.Linq; using Content.Server.Hands.Components; using Content.Server.Inventory.Components; using Content.Server.Items; -using Content.Server.PDA; using Content.Server.Traitor.Uplink.Account; using Content.Server.Traitor.Uplink.Components; using Content.Server.UserInterface; using Content.Shared.ActionBlocker; using Content.Shared.Hands.Components; using Content.Shared.Interaction; +using Content.Shared.PDA; using Content.Shared.Traitor.Uplink; using Robust.Server.GameObjects; using Robust.Server.Player; diff --git a/Content.Server/Verbs/VerbSystem.cs b/Content.Server/Verbs/VerbSystem.cs index 369ae31ae8..83d8a43886 100644 --- a/Content.Server/Verbs/VerbSystem.cs +++ b/Content.Server/Verbs/VerbSystem.cs @@ -1,53 +1,26 @@ +using Content.Server.Popups; +using Content.Shared.Administration.Logs; +using Content.Shared.Database; +using Content.Shared.Hands.Components; using Content.Shared.Verbs; using Robust.Server.Player; using Robust.Shared.GameObjects; +using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Player; namespace Content.Server.Verbs { public sealed class VerbSystem : SharedVerbSystem { + [Dependency] private readonly SharedAdminLogSystem _logSystem = default!; + [Dependency] private readonly PopupSystem _popupSystem = default!; + public override void Initialize() { base.Initialize(); SubscribeNetworkEvent(HandleVerbRequest); - SubscribeNetworkEvent(HandleTryExecuteVerb); - } - - /// - /// Called when asked over the network to run a given verb. - /// - public void HandleTryExecuteVerb(ExecuteVerbEvent args, EntitySessionEventArgs eventArgs) - { - var session = eventArgs.SenderSession; - - if (session.AttachedEntity is not {} userEntity) - { - Logger.Warning($"{nameof(HandleTryExecuteVerb)} called by player {session} with no attached entity."); - return; - } - - if (!EntityManager.EntityExists(args.Target)) - { - return; - } - - // Get the list of verbs. This effectively also checks that the requested verb is in fact a valid verb that - // the user can perform. - var verbs = GetLocalVerbs(args.Target, userEntity, args.Type)[args.Type]; - - // Note that GetLocalVerbs might waste time checking & preparing unrelated verbs even though we know - // precisely which one we want to run. However, MOST entities will only have 1 or 2 verbs of a given type. - // The one exception here is the "other" verb type, which has 3-4 verbs + all the debug verbs. - - // Find the requested verb. - if (verbs.TryGetValue(args.RequestedVerb, out var verb)) - ExecuteVerb(verb, userEntity, args.Target); - else - // 404 Verb not found. Note that this could happen due to something as simple as opening the verb menu, walking away, then trying - // to run the pickup-item verb. So maybe this shouldn't even be logged? - Logger.Info($"{nameof(HandleTryExecuteVerb)} called by player {session} with an invalid verb: {args.RequestedVerb.Category?.Text} {args.RequestedVerb.Text}"); } private void HandleVerbRequest(RequestServerVerbsEvent args, EntitySessionEventArgs eventArgs) @@ -73,5 +46,70 @@ namespace Content.Server.Verbs var response = new VerbsResponseEvent(args.EntityUid, GetLocalVerbs(args.EntityUid, attached, args.Type)); RaiseNetworkEvent(response, player.ConnectedClient); } + + /// + /// Execute the provided verb. + /// + /// + /// This will try to call the action delegates and raise the local events for the given verb. + /// + public override void ExecuteVerb(Verb verb, EntityUid user, EntityUid target, bool forced = false) + { + // is this verb actually valid? + if (verb.Disabled) + { + // Send an informative pop-up message + if (!string.IsNullOrWhiteSpace(verb.Message)) + _popupSystem.PopupEntity(verb.Message, user, Filter.Entities(user)); + + return; + } + + // first, lets log the verb. Just in case it ends up crashing the server or something. + LogVerb(verb, user, target, forced); + + // then invoke any relevant actions + verb.Act?.Invoke(); + + // Maybe raise a local event + if (verb.ExecutionEventArgs != null) + { + if (verb.EventTarget.IsValid()) + RaiseLocalEvent(verb.EventTarget, verb.ExecutionEventArgs); + else + RaiseLocalEvent(verb.ExecutionEventArgs); + } + } + + public void LogVerb(Verb verb, EntityUid user, EntityUid target, bool forced) + { + // first get the held item. again. + EntityUid? holding = null; + if (TryComp(user, out SharedHandsComponent? hands) && + hands.TryGetActiveHeldEntity(out var heldEntity)) + { + holding = heldEntity; + } + + // if this is a virtual pull, get the held entity + if (holding != null && TryComp(holding, out HandVirtualItemComponent? pull)) + holding = pull.BlockingEntity; + + var verbText = $"{verb.Category?.Text} {verb.Text}".Trim(); + + // lets not frame people, eh? + var executionText = forced ? "was forced to execute" : "executed"; + + if (holding == null) + { + _logSystem.Add(LogType.Verb, verb.Impact, + $"{ToPrettyString(user):user} {executionText} the [{verbText:verb}] verb targeting {ToPrettyString(target):target}"); + } + else + { + _logSystem.Add(LogType.Verb, verb.Impact, + $"{ToPrettyString(user):user} {executionText} the [{verbText:verb}] verb targeting {ToPrettyString(target):target} while holding {ToPrettyString(holding.Value):held}"); + } + } } } diff --git a/Content.Server/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs similarity index 60% rename from Content.Server/Access/Components/IdCardComponent.cs rename to Content.Shared/Access/Components/IdCardComponent.cs index 7e0f15a8ec..be555c3eda 100644 --- a/Content.Server/Access/Components/IdCardComponent.cs +++ b/Content.Shared/Access/Components/IdCardComponent.cs @@ -1,13 +1,15 @@ -using Content.Server.Access.Systems; -using Content.Server.PDA; +using Content.Shared.Access.Systems; +using Content.Shared.PDA; using Robust.Shared.Analyzers; using Robust.Shared.GameObjects; using Robust.Shared.Serialization.Manager.Attributes; -namespace Content.Server.Access.Components +namespace Content.Shared.Access.Components { + // TODO BUI NETWORKING if ever clients can open their own BUI's (id card console, pda), then this data should be + // networked. [RegisterComponent] - [Friend(typeof(IdCardSystem), typeof(PDASystem))] + [Friend(typeof(SharedIdCardSystem), typeof(SharedPDASystem))] public class IdCardComponent : Component { public override string Name => "IdCard"; diff --git a/Content.Shared/Access/SharedIdCardConsoleComponent.cs b/Content.Shared/Access/Components/SharedIdCardConsoleComponent.cs similarity index 98% rename from Content.Shared/Access/SharedIdCardConsoleComponent.cs rename to Content.Shared/Access/Components/SharedIdCardConsoleComponent.cs index 5b394a1554..0c7903ce28 100644 --- a/Content.Shared/Access/SharedIdCardConsoleComponent.cs +++ b/Content.Shared/Access/Components/SharedIdCardConsoleComponent.cs @@ -5,7 +5,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.Serialization; using Robust.Shared.Serialization.Manager.Attributes; -namespace Content.Shared.Access +namespace Content.Shared.Access.Components { public class SharedIdCardConsoleComponent : Component { diff --git a/Content.Shared/Access/SharedIdCardConsoleSystem.cs b/Content.Shared/Access/Systems/SharedIdCardConsoleSystem.cs similarity index 93% rename from Content.Shared/Access/SharedIdCardConsoleSystem.cs rename to Content.Shared/Access/Systems/SharedIdCardConsoleSystem.cs index ba9decb18f..cdd34f155f 100644 --- a/Content.Shared/Access/SharedIdCardConsoleSystem.cs +++ b/Content.Shared/Access/Systems/SharedIdCardConsoleSystem.cs @@ -1,9 +1,10 @@ +using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; using JetBrains.Annotations; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -namespace Content.Shared.Access +namespace Content.Shared.Access.Systems { [UsedImplicitly] public abstract class SharedIdCardConsoleSystem : EntitySystem diff --git a/Content.Shared/Access/Systems/SharedIdCardSystem.cs b/Content.Shared/Access/Systems/SharedIdCardSystem.cs new file mode 100644 index 0000000000..f612d29a0c --- /dev/null +++ b/Content.Shared/Access/Systems/SharedIdCardSystem.cs @@ -0,0 +1,8 @@ +using Robust.Shared.GameObjects; + +namespace Content.Shared.Access.Systems; + +public abstract class SharedIdCardSystem : EntitySystem +{ + // this class just exists to make friends. Will you be its friend? +} diff --git a/Content.Server/Chemistry/Components/SolutionManager/FitsInDispenserComponent.cs b/Content.Shared/Chemistry/Components/FitsInDispenserComponent.cs similarity index 75% rename from Content.Server/Chemistry/Components/SolutionManager/FitsInDispenserComponent.cs rename to Content.Shared/Chemistry/Components/FitsInDispenserComponent.cs index 4fada63168..8d7db5ca93 100644 --- a/Content.Server/Chemistry/Components/SolutionManager/FitsInDispenserComponent.cs +++ b/Content.Shared/Chemistry/Components/FitsInDispenserComponent.cs @@ -1,15 +1,17 @@ -using Robust.Shared.GameObjects; +using Robust.Shared.GameObjects; +using Robust.Shared.GameStates; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; -namespace Content.Server.Chemistry.Components.SolutionManager +namespace Content.Shared.Chemistry.Components { /// Allows the entity with this component to be placed in a SharedReagentDispenserComponent. /// Otherwise it's considered to be too large or the improper shape to fit. /// Allows us to have obscenely large containers that are harder to abuse in chem dispensers /// since they can't be placed directly in them. - /// + /// [RegisterComponent] + [NetworkedComponent] // only needed for white-lists. Client doesn't actually need Solution data; public class FitsInDispenserComponent : Component { public override string Name => "FitsInDispenser"; diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 765a838ba6..d93ba287df 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -3,6 +3,7 @@ using Content.Shared.Acts; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Content.Shared.Popups; +using Content.Shared.Sound; using Content.Shared.Verbs; using Robust.Shared.Audio; using Robust.Shared.Containers; @@ -12,6 +13,7 @@ using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Player; +using Robust.Shared.Timing; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -24,6 +26,7 @@ namespace Content.Shared.Containers.ItemSlots { [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; public override void Initialize() { @@ -174,7 +177,7 @@ namespace Content.Shared.Containers.ItemSlots if (slot.Item != null) hands.TryPutInAnyHand(slot.Item.Value); - Insert(uid, slot, args.Used); + Insert(uid, slot, args.Used, args.User, true); args.Handled = true; return; } @@ -186,13 +189,32 @@ namespace Content.Shared.Containers.ItemSlots /// Insert an item into a slot. This does not perform checks, so make sure to also use or just use instead. /// - private void Insert(EntityUid uid, ItemSlot slot, EntityUid item) + private void Insert(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user, bool userAudio = false) { slot.ContainerSlot.Insert(item); // ContainerSlot automatically raises a directed EntInsertedIntoContainerMessage - if (slot.InsertSound != null) - SoundSystem.Play(Filter.Pvs(uid), slot.InsertSound.GetSound(), uid, slot.SoundOptions); + PlaySound(uid, slot.InsertSound, slot.SoundOptions, userAudio ? null : user); + } + + /// + /// Plays a sound + /// + /// Source of the sound + /// The sound + /// Optional (server-side) argument used to prevent sending the audio to a specific + /// user. + private void PlaySound(EntityUid uid, SoundSpecifier? sound, AudioParams audioParams, EntityUid? excluded) + { + if (sound == null || !_gameTiming.IsFirstTimePredicted) + return; + + var filter = Filter.Pvs(uid); + + if (excluded != null) + filter = filter.RemoveWhereAttachedEntity(entity => entity == excluded.Value); + + SoundSystem.Play(filter, sound.GetSound(), uid, audioParams); } /// @@ -228,7 +250,7 @@ namespace Content.Shared.Containers.ItemSlots /// Tries to insert item into a specific slot. /// /// False if failed to insert item - public bool TryInsert(EntityUid uid, string id, EntityUid item, ItemSlotsComponent? itemSlots = null) + public bool TryInsert(EntityUid uid, string id, EntityUid item, EntityUid? user, ItemSlotsComponent? itemSlots = null) { if (!Resolve(uid, ref itemSlots)) return false; @@ -236,19 +258,19 @@ namespace Content.Shared.Containers.ItemSlots if (!itemSlots.Slots.TryGetValue(id, out var slot)) return false; - return TryInsert(uid, slot, item); + return TryInsert(uid, slot, item, user); } /// /// Tries to insert item into a specific slot. /// /// False if failed to insert item - public bool TryInsert(EntityUid uid, ItemSlot slot, EntityUid item) + public bool TryInsert(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user) { if (!CanInsert(uid, item, slot)) return false; - Insert(uid, slot, item); + Insert(uid, slot, item, user); return true; } @@ -271,7 +293,7 @@ namespace Content.Shared.Containers.ItemSlots if (!_actionBlockerSystem.CanInteract(user) && hands.Drop(item)) return false; - Insert(uid, slot, item); + Insert(uid, slot, item, user); return true; } #endregion @@ -281,20 +303,19 @@ namespace Content.Shared.Containers.ItemSlots /// Eject an item into a slot. This does not perform checks (e.g., is the slot locked?), so you should /// probably just use instead. /// - private void Eject(EntityUid uid, ItemSlot slot, EntityUid item) + private void Eject(EntityUid uid, ItemSlot slot, EntityUid item, EntityUid? user) { slot.ContainerSlot.Remove(item); // ContainerSlot automatically raises a directed EntRemovedFromContainerMessage - if (slot.EjectSound != null) - SoundSystem.Play(Filter.Pvs(uid), slot.EjectSound.GetSound(), uid, slot.SoundOptions); + PlaySound(uid, slot.EjectSound, slot.SoundOptions, user); } /// /// Try to eject an item from a slot. /// /// False if item slot is locked or has no item inserted - public bool TryEject(EntityUid uid, ItemSlot slot, [NotNullWhen(true)] out EntityUid? item) + public bool TryEject(EntityUid uid, ItemSlot slot, EntityUid? user, [NotNullWhen(true)] out EntityUid? item) { item = null; @@ -302,7 +323,7 @@ namespace Content.Shared.Containers.ItemSlots return false; item = slot.Item; - Eject(uid, slot, item.Value); + Eject(uid, slot, item.Value, user); return true; } @@ -310,7 +331,8 @@ namespace Content.Shared.Containers.ItemSlots /// Try to eject item from a slot. /// /// False if the id is not valid, the item slot is locked, or it has no item inserted - public bool TryEject(EntityUid uid, string id, [NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null) + public bool TryEject(EntityUid uid, string id, EntityUid user, + [NotNullWhen(true)] out EntityUid? item, ItemSlotsComponent? itemSlots = null) { item = null; @@ -320,7 +342,7 @@ namespace Content.Shared.Containers.ItemSlots if (!itemSlots.Slots.TryGetValue(id, out var slot)) return false; - return TryEject(uid, slot, out item); + return TryEject(uid, slot, user, out item); } /// @@ -333,7 +355,7 @@ namespace Content.Shared.Containers.ItemSlots /// public bool TryEjectToHands(EntityUid uid, ItemSlot slot, EntityUid? user) { - if (!TryEject(uid, slot, out var item)) + if (!TryEject(uid, slot, user, out var item)) return false; if (user != null && EntityManager.TryGetComponent(user.Value, out SharedHandsComponent? hands)) @@ -428,7 +450,7 @@ namespace Content.Shared.Containers.ItemSlots : EntityManager.GetComponent(args.Using.Value).EntityName ?? string.Empty; Verb insertVerb = new(); - insertVerb.Act = () => Insert(uid, slot, args.Using.Value); + insertVerb.Act = () => Insert(uid, slot, args.Using.Value, args.User); if (slot.InsertVerbText != null) { @@ -461,7 +483,7 @@ namespace Content.Shared.Containers.ItemSlots foreach (var slot in component.Slots.Values) { if (slot.EjectOnBreak && slot.HasItem) - TryEject(uid, slot, out var _); + TryEject(uid, slot, null, out var _); } } diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 1c5c1394a7..3b64493d67 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -1,12 +1,16 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; +using Content.Shared.CombatMode; using Content.Shared.Database; using Content.Shared.Hands; using Content.Shared.Hands.Components; +using Content.Shared.Input; +using Content.Shared.Interaction.Helpers; using Content.Shared.Inventory; using Content.Shared.Physics; using Content.Shared.Popups; @@ -16,13 +20,14 @@ using Content.Shared.Verbs; using JetBrains.Annotations; using Robust.Shared.Containers; using Robust.Shared.GameObjects; +using Robust.Shared.Input.Binding; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Physics; using Robust.Shared.Players; -using Robust.Shared.Random; using Robust.Shared.Serialization; #pragma warning disable 618 @@ -39,12 +44,166 @@ namespace Content.Shared.Interaction [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; [Dependency] private readonly SharedVerbSystem _verbSystem = default!; [Dependency] private readonly SharedAdminLogSystem _adminLogSystem = default!; + [Dependency] private readonly RotateToFaceSystem _rotateToFaceSystem = default!; public const float InteractionRange = 2; public const float InteractionRangeSquared = InteractionRange * InteractionRange; public delegate bool Ignored(EntityUid entity); + public override void Initialize() + { + SubscribeAllEvent(HandleInteractInventorySlotEvent); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.AltActivateItemInWorld, + new PointerInputCmdHandler(HandleAltUseInteraction)) + .Register(); + } + + public override void Shutdown() + { + CommandBinds.Unregister(); + base.Shutdown(); + } + + /// + /// Handles the event were a client uses an item in their inventory or in their hands, either by + /// alt-clicking it or pressing 'E' while hovering over it. + /// + private void HandleInteractInventorySlotEvent(InteractInventorySlotEvent msg, EntitySessionEventArgs args) + { + var coords = Transform(msg.ItemUid).Coordinates; + // client sanitization + if (!ValidateClientInput(args.SenderSession, coords, msg.ItemUid, out var user)) + { + Logger.InfoS("system.interaction", $"Inventory interaction validation failed. Session={args.SenderSession}"); + return; + } + + if (msg.AltInteract) + // Use 'UserInteraction' function - behaves as if the user alt-clicked the item in the world. + UserInteraction(user.Value, coords, msg.ItemUid, msg.AltInteract); + else + // User used 'E'. We want to activate it, not simulate clicking on the item + InteractionActivate(user.Value, msg.ItemUid); + } + + public bool HandleAltUseInteraction(ICommonSession? session, EntityCoordinates coords, EntityUid uid) + { + // client sanitization + if (!ValidateClientInput(session, coords, uid, out var user)) + { + Logger.InfoS("system.interaction", $"Alt-use input validation failed"); + return true; + } + + UserInteraction(user.Value, coords, uid, altInteract: true); + + return false; + } + + /// + /// Resolves user interactions with objects. + /// + /// + /// Checks Whether combat mode is enabled and whether the user can actually interact with the given entity. + /// + /// Whether to use default or alternative interactions (usually as a result of + /// alt+clicking). If combat mode is enabled, the alternative action is to perform the default non-combat + /// interaction. Having an item in the active hand also disables alternative interactions. + public async void UserInteraction(EntityUid user, EntityCoordinates coordinates, EntityUid? target, bool altInteract = false) + { + if (target != null && Deleted(target.Value)) + return; + + // TODO COMBAT Consider using alt-interact for advanced combat? maybe alt-interact disarms? + if (!altInteract && TryComp(user, out SharedCombatModeComponent? combatMode) && combatMode.IsInCombatMode) + { + DoAttack(user, coordinates, false, target); + return; + } + + if (!ValidateInteractAndFace(user, coordinates)) + return; + + if (!_actionBlockerSystem.CanInteract(user)) + return; + + // Check if interacted entity is in the same container, the direct child, or direct parent of the user. + // This is bypassed IF the interaction happened through an item slot (e.g., backpack UI) + if (target != null && !user.IsInSameOrParentContainer(target.Value) && !CanAccessViaStorage(user, target.Value)) + return; + + // Verify user has a hand, and find what object they are currently holding in their active hand + if (!TryComp(user, out SharedHandsComponent? hands)) + return; + + // TODO remove invalid/default uid and use nullable. + hands.TryGetActiveHeldEntity(out var heldEntity); + EntityUid? held = heldEntity.Valid ? heldEntity : null; + + // TODO: Replace with body interaction range when we get something like arm length or telekinesis or something. + var inRangeUnobstructed = user.InRangeUnobstructed(coordinates, ignoreInsideBlocker: true); + if (target == null || !inRangeUnobstructed) + { + if (held == null) + return; + + if (!await InteractUsingRanged(user, held.Value, target, coordinates, inRangeUnobstructed) && + !inRangeUnobstructed) + { + var message = Loc.GetString("interaction-system-user-interaction-cannot-reach"); + user.PopupMessage(message); + } + + return; + } + else + { + // We are close to the nearby object. + if (altInteract) + // Perform alternative interactions, using context menu verbs. + AltInteract(user, target.Value); + else if (held != null && held != target) + // We are performing a standard interaction with an item, and the target isn't the same as the item + // currently in our hand. We will use the item in our hand on the nearby object via InteractUsing + await InteractUsing(user, held.Value, target.Value, coordinates); + else if (held == null) + // Since our hand is empty we will use InteractHand/Activate + InteractHand(user, target.Value); + } + } + + public virtual void InteractHand(EntityUid user, EntityUid target) + { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. + } + + public virtual void DoAttack(EntityUid user, EntityCoordinates coordinates, bool wideAttack, + EntityUid? targetUid = null) + { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. + } + + public virtual async Task InteractUsingRanged(EntityUid user, EntityUid used, EntityUid? target, + EntityCoordinates clickLocation, bool inRangeUnobstructed) + { + // TODO PREDICTION move server-side interaction logic into the shared system for interaction prediction. + return await Task.FromResult(true); + } + + protected bool ValidateInteractAndFace(EntityUid user, EntityCoordinates coordinates) + { + // Verify user is on the same map as the entity they clicked on + if (coordinates.GetMapId(EntityManager) != Transform(user).MapID) + return false; + + _rotateToFaceSystem.TryFaceCoordinates(user, coordinates.ToMapPos(EntityManager)); + + return true; + } + /// /// Traces a ray from coords to otherCoords and returns the length /// of the vector between coords and the ray's first hit. @@ -154,7 +313,7 @@ namespace Content.Shared.Interaction foreach (var result in rayResults) { - if (!EntityManager.TryGetComponent(result.HitEntity, out IPhysBody? p)) + if (!TryComp(result.HitEntity, out IPhysBody? p)) { continue; } @@ -213,7 +372,7 @@ namespace Content.Shared.Interaction bool popup = false) { predicate ??= e => e == origin || e == other; - return InRangeUnobstructed(origin, EntityManager.GetComponent(other).MapPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); + return InRangeUnobstructed(origin, Transform(other).MapPosition, range, collisionMask, predicate, ignoreInsideBlocker, popup); } /// @@ -345,7 +504,7 @@ namespace Content.Shared.Interaction bool ignoreInsideBlocker = false, bool popup = false) { - var originPosition = EntityManager.GetComponent(origin).MapPosition; + var originPosition = Transform(origin).MapPosition; predicate ??= e => e == origin; var inRange = InRangeUnobstructed(originPosition, other, range, collisionMask, predicate, ignoreInsideBlocker); @@ -392,7 +551,7 @@ namespace Content.Shared.Interaction var interactUsingEventArgs = new InteractUsingEventArgs(user, clickLocation, used, target); - var interactUsings = EntityManager.GetComponents(target).OrderByDescending(x => x.Priority); + var interactUsings = AllComps(target).OrderByDescending(x => x.Priority); foreach (var interactUsing in interactUsings) { // If an InteractUsing returns a status completion we finish our interaction @@ -418,7 +577,7 @@ namespace Content.Shared.Interaction return true; var afterInteractEventArgs = new AfterInteractEventArgs(user, clickLocation, target, canReach); - var afterInteracts = EntityManager.GetComponents(used).OrderByDescending(x => x.Priority).ToList(); + var afterInteracts = AllComps(used).OrderByDescending(x => x.Priority).ToList(); foreach (var afterInteract in afterInteracts) { @@ -444,7 +603,7 @@ namespace Content.Shared.Interaction protected void InteractionActivate(EntityUid user, EntityUid used) { - if (EntityManager.TryGetComponent(used, out var delayComponent)) + if (TryComp(used, out UseDelayComponent? delayComponent)) { if (delayComponent.ActiveDelay) return; @@ -472,7 +631,7 @@ namespace Content.Shared.Interaction return; } - if (!EntityManager.TryGetComponent(used, out IActivate? activateComp)) + if (!TryComp(used, out IActivate? activateComp)) return; var activateEventArgs = new ActivateEventArgs(user, used); @@ -506,7 +665,7 @@ namespace Content.Shared.Interaction /// public void UseInteraction(EntityUid user, EntityUid used) { - if (EntityManager.TryGetComponent(used, out var delayComponent)) + if (TryComp(used, out UseDelayComponent? delayComponent)) { if (delayComponent.ActiveDelay) return; @@ -519,7 +678,7 @@ namespace Content.Shared.Interaction if (useMsg.Handled) return; - var uses = EntityManager.GetComponents(used).ToList(); + var uses = AllComps(used).ToList(); // Try to use item on any components which have the interface foreach (var use in uses) @@ -560,7 +719,7 @@ namespace Content.Shared.Interaction return; } - var comps = EntityManager.GetComponents(thrown).ToList(); + var comps = AllComps(thrown).ToList(); var args = new ThrownEventArgs(user); // Call Thrown on all components that implement the interface @@ -584,7 +743,7 @@ namespace Content.Shared.Interaction if (equipMsg.Handled) return; - var comps = EntityManager.GetComponents(equipped).ToList(); + var comps = AllComps(equipped).ToList(); // Call Thrown on all components that implement the interface foreach (var comp in comps) @@ -604,7 +763,7 @@ namespace Content.Shared.Interaction if (unequipMsg.Handled) return; - var comps = EntityManager.GetComponents(equipped).ToList(); + var comps = AllComps(equipped).ToList(); // Call Thrown on all components that implement the interface foreach (var comp in comps) @@ -625,7 +784,7 @@ namespace Content.Shared.Interaction if (equippedHandMessage.Handled) return; - var comps = EntityManager.GetComponents(item).ToList(); + var comps = AllComps(item).ToList(); foreach (var comp in comps) { @@ -644,7 +803,7 @@ namespace Content.Shared.Interaction if (unequippedHandMessage.Handled) return; - var comps = EntityManager.GetComponents(item).ToList(); + var comps = AllComps(item).ToList(); foreach (var comp in comps) { @@ -681,9 +840,9 @@ namespace Content.Shared.Interaction return; } - EntityManager.GetComponent(item).LocalRotation = Angle.Zero; + Transform(item).LocalRotation = Angle.Zero; - var comps = EntityManager.GetComponents(item).ToList(); + var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) @@ -706,7 +865,7 @@ namespace Content.Shared.Interaction if (handSelectedMsg.Handled) return; - var comps = EntityManager.GetComponents(item).ToList(); + var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) @@ -726,7 +885,7 @@ namespace Content.Shared.Interaction if (handDeselectedMsg.Handled) return; - var comps = EntityManager.GetComponents(item).ToList(); + var comps = AllComps(item).ToList(); // Call Land on all components that implement the interface foreach (var comp in comps) @@ -742,6 +901,36 @@ namespace Content.Shared.Interaction /// public abstract bool CanAccessViaStorage(EntityUid user, EntityUid target); + protected bool ValidateClientInput(ICommonSession? session, EntityCoordinates coords, + EntityUid uid, [NotNullWhen(true)] out EntityUid? userEntity) + { + userEntity = null; + + if (!coords.IsValid(EntityManager)) + { + Logger.InfoS("system.interaction", $"Invalid Coordinates: client={session}, coords={coords}"); + return false; + } + + if (uid.IsClientSide()) + { + Logger.WarningS("system.interaction", + $"Client sent interaction with client-side entity. Session={session}, Uid={uid}"); + return false; + } + + userEntity = session?.AttachedEntity; + + if (userEntity == null || !userEntity.Value.Valid) + { + Logger.WarningS("system.interaction", + $"Client sent interaction with no attached entity. Session={session}"); + return false; + } + + return true; + } + #endregion } diff --git a/Content.Server/PDA/PDAComponent.cs b/Content.Shared/PDA/PDAComponent.cs similarity index 90% rename from Content.Server/PDA/PDAComponent.cs rename to Content.Shared/PDA/PDAComponent.cs index f20524b80c..6740d766dd 100644 --- a/Content.Server/PDA/PDAComponent.cs +++ b/Content.Shared/PDA/PDAComponent.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using Content.Server.Access.Components; +using Content.Shared.Access.Components; using Content.Shared.Containers.ItemSlots; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; @@ -8,7 +6,7 @@ using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype; using Robust.Shared.ViewVariables; -namespace Content.Server.PDA +namespace Content.Shared.PDA { [RegisterComponent] public class PDAComponent : Component diff --git a/Content.Shared/PDA/SharedPDASystem.cs b/Content.Shared/PDA/SharedPDASystem.cs new file mode 100644 index 0000000000..8c9eee5f2f --- /dev/null +++ b/Content.Shared/PDA/SharedPDASystem.cs @@ -0,0 +1,65 @@ +using Content.Shared.Access.Components; +using Content.Shared.Containers.ItemSlots; +using Robust.Shared.Containers; +using Robust.Shared.GameObjects; +using Robust.Shared.IoC; + +namespace Content.Shared.PDA +{ + public abstract class SharedPDASystem : EntitySystem + { + [Dependency] protected readonly ItemSlotsSystem ItemSlotsSystem = default!; + + public override void Initialize() + { + base.Initialize(); + + SubscribeLocalEvent(OnComponentInit); + SubscribeLocalEvent(OnComponentRemove); + + SubscribeLocalEvent(OnItemInserted); + SubscribeLocalEvent(OnItemRemoved); + } + + protected virtual void OnComponentInit(EntityUid uid, PDAComponent pda, ComponentInit args) + { + if (pda.IdCard != null) + pda.IdSlot.StartingItem = pda.IdCard; + + ItemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-id", pda.IdSlot); + ItemSlotsSystem.AddItemSlot(uid, $"{pda.Name}-pen", pda.PenSlot); + + UpdatePDAAppearance(pda); + } + + private void OnComponentRemove(EntityUid uid, PDAComponent pda, ComponentRemove args) + { + ItemSlotsSystem.RemoveItemSlot(uid, pda.IdSlot); + ItemSlotsSystem.RemoveItemSlot(uid, pda.PenSlot); + } + + protected virtual void OnItemInserted(EntityUid uid, PDAComponent pda, EntInsertedIntoContainerMessage args) + { + if (!pda.Initialized) return; + + if (args.Container.ID == pda.IdSlot.ID) + pda.ContainedID = CompOrNull(args.Entity); + + UpdatePDAAppearance(pda); + } + + protected virtual void OnItemRemoved(EntityUid uid, PDAComponent pda, EntRemovedFromContainerMessage args) + { + if (args.Container.ID == pda.IdSlot.ID) + pda.ContainedID = null; + + UpdatePDAAppearance(pda); + } + + private void UpdatePDAAppearance(PDAComponent pda) + { + if (TryComp(pda.Owner, out AppearanceComponent ? appearance)) + appearance.SetData(PDAVisuals.IDCardInserted, pda.ContainedID != null); + } + } +} diff --git a/Content.Shared/Verbs/SharedVerbSystem.cs b/Content.Shared/Verbs/SharedVerbSystem.cs index d8b1fccae2..64c1a52afc 100644 --- a/Content.Shared/Verbs/SharedVerbSystem.cs +++ b/Content.Shared/Verbs/SharedVerbSystem.cs @@ -1,7 +1,5 @@ using System.Collections.Generic; using Content.Shared.ActionBlocker; -using Content.Shared.Administration.Logs; -using Content.Shared.Database; using Content.Shared.Hands.Components; using Content.Shared.Interaction; using Robust.Shared.Containers; @@ -12,10 +10,35 @@ namespace Content.Shared.Verbs { public abstract class SharedVerbSystem : EntitySystem { - [Dependency] private readonly SharedAdminLogSystem _logSystem = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly ActionBlockerSystem _actionBlockerSystem = default!; + public override void Initialize() + { + base.Initialize(); + + SubscribeAllEvent(HandleExecuteVerb); + } + + private void HandleExecuteVerb(ExecuteVerbEvent args, EntitySessionEventArgs eventArgs) + { + var user = eventArgs.SenderSession.AttachedEntity; + if (user == null) + return; + + // Get the list of verbs. This effectively also checks that the requested verb is in fact a valid verb that + // the user can perform. + var verbs = GetLocalVerbs(args.Target, user.Value, args.Type)[args.Type]; + + // Note that GetLocalVerbs might waste time checking & preparing unrelated verbs even though we know + // precisely which one we want to run. However, MOST entities will only have 1 or 2 verbs of a given type. + // The one exception here is the "other" verb type, which has 3-4 verbs + all the debug verbs. + + // Find the requested verb. + if (verbs.TryGetValue(args.RequestedVerb, out var verb)) + ExecuteVerb(verb, user.Value, args.Target); + } + /// /// Raises a number of events in order to get all verbs of the given type(s) defined in local systems. This /// does not request verbs from the server. @@ -92,60 +115,6 @@ namespace Content.Shared.Verbs /// /// This will try to call the action delegates and raise the local events for the given verb. /// - public void ExecuteVerb(Verb verb, EntityUid user, EntityUid target, bool forced = false) - { - // first, lets log the verb. Just in case it ends up crashing the server or something. - LogVerb(verb, user, target, forced); - - // then invoke any relevant actions - verb.Act?.Invoke(); - - // Maybe raise a local event - if (verb.ExecutionEventArgs != null) - { - if (verb.EventTarget.IsValid()) - RaiseLocalEvent(verb.EventTarget, verb.ExecutionEventArgs); - else - RaiseLocalEvent(verb.ExecutionEventArgs); - } - } - - public void LogVerb(Verb verb, EntityUid user, EntityUid target, bool forced) - { - // first get the held item. again. - EntityUid usedUid = default; - if (EntityManager.TryGetComponent(user, out SharedHandsComponent? hands) && - hands.TryGetActiveHeldEntity(out var heldEntity)) - { - usedUid = heldEntity; - if (usedUid != default && EntityManager.TryGetComponent(usedUid, out HandVirtualItemComponent? pull)) - usedUid = pull.BlockingEntity; - } - - // get all the entities - if (!user.IsValid() || !target.IsValid()) - return; - - EntityUid? used = null; - if (usedUid != default) - EntityManager.EntityExists(usedUid); - - var verbText = $"{verb.Category?.Text} {verb.Text}".Trim(); - - // lets not frame people, eh? - var executionText = forced ? "was forced to execute" : "executed"; - - if (used == null) - { - _logSystem.Add(LogType.Verb, verb.Impact, - $"{ToPrettyString(user):user} {executionText} the [{verbText:verb}] verb targeting {ToPrettyString(target):target}"); - } - else - { - _logSystem.Add(LogType.Verb, verb.Impact, - $"{ToPrettyString(user):user} {executionText} the [{verbText:verb}] verb targeting {ToPrettyString(target):target} while holding {ToPrettyString(used.Value):held}"); - } - - } + public abstract void ExecuteVerb(Verb verb, EntityUid user, EntityUid target, bool forced = false); } } diff --git a/Content.Shared/Weapons/Melee/AttackEvent.cs b/Content.Shared/Weapons/Melee/AttackEvent.cs index bf723ba586..2772aeb239 100644 --- a/Content.Shared/Weapons/Melee/AttackEvent.cs +++ b/Content.Shared/Weapons/Melee/AttackEvent.cs @@ -25,7 +25,7 @@ namespace Content.Shared.Weapons.Melee public EntityCoordinates ClickLocation { get; } /// - /// UID of the entity that was attacked. + /// The entity that was attacked. /// public EntityUid? Target { get; }