From 47e597ca470564e1eaaf664878259f04999bc119 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sun, 30 Jan 2022 13:50:10 +1300 Subject: [PATCH] Predict inventory slot interactions. (#6033) --- .../Inventory/ClientInventorySystem.cs | 30 +--- .../Inventory/ServerInventorySystem.cs | 38 ----- .../Containers/ItemSlot/ItemSlotsSystem.cs | 2 +- Content.Shared/Interaction/IInteractUsing.cs | 9 +- .../Interaction/SharedInteractionSystem.cs | 4 +- .../Events/TryEquipNetworkMessage.cs | 26 ---- .../Events/TryUnequipNetworkMessage.cs | 24 --- .../Inventory/Events/UseSlotNetworkMessage.cs | 6 +- .../Inventory/InventorySystem.Equip.cs | 139 +++++++++++++++--- 9 files changed, 138 insertions(+), 140 deletions(-) delete mode 100644 Content.Shared/Inventory/Events/TryEquipNetworkMessage.cs delete mode 100644 Content.Shared/Inventory/Events/TryUnequipNetworkMessage.cs diff --git a/Content.Client/Inventory/ClientInventorySystem.cs b/Content.Client/Inventory/ClientInventorySystem.cs index 41e961b2bf..2fb289660c 100644 --- a/Content.Client/Inventory/ClientInventorySystem.cs +++ b/Content.Client/Inventory/ClientInventorySystem.cs @@ -70,20 +70,6 @@ namespace Content.Client.Inventory _config.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); } - public override bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent = false, bool force = false, - InventoryComponent? inventory = null, SharedItemComponent? item = null) - { - if(!target.IsClientSide() && !actor.IsClientSide() && !itemUid.IsClientSide()) RaiseNetworkEvent(new TryEquipNetworkMessage(actor, target, itemUid, slot, silent, force)); - return base.TryEquip(actor, target, itemUid, slot, silent, force, inventory, item); - } - - public override bool TryUnequip(EntityUid actor, EntityUid target, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, bool force = false, - InventoryComponent? inventory = null) - { - if(!target.IsClientSide() && !actor.IsClientSide()) RaiseNetworkEvent(new TryUnequipNetworkMessage(actor, target, slot, silent, force)); - return base.TryUnequip(actor, target, slot, out removedItem, silent, force, inventory); - } - private void OnDidUnequip(EntityUid uid, ClientInventoryComponent component, DidUnequipEvent args) { UpdateComponentUISlot(uid, args.Slot, null, component); @@ -213,17 +199,15 @@ namespace Content.Client.Inventory private void HandleSlotButtonPressed(EntityUid uid, string slot, ItemSlotButton button, GUIBoundKeyEventArgs args) { - if (TryGetSlotEntity(uid, slot, out var itemUid)) - { - if (!_itemSlotManager.OnButtonPressed(args, itemUid.Value) && args.Function == EngineKeyFunctions.UIClick) - { - RaiseNetworkEvent(new UseSlotNetworkMessage(uid, slot)); - } + if (TryGetSlotEntity(uid, slot, out var itemUid) && _itemSlotManager.OnButtonPressed(args, itemUid.Value)) return; - } - if (args.Function != EngineKeyFunctions.UIClick) return; - TryEquipActiveHandTo(uid, slot); + if (args.Function != EngineKeyFunctions.UIClick) + return; + + // only raise event if either itemUid is not null, or the user is holding something + if (itemUid != null || TryComp(uid, out SharedHandsComponent? hands) && hands.TryGetActiveHeldEntity(out _)) + EntityManager.RaisePredictiveEvent(new UseSlotNetworkMessage(slot)); } private bool TryGetUIElements(EntityUid uid, [NotNullWhen(true)] out DefaultWindow? invWindow, diff --git a/Content.Server/Inventory/ServerInventorySystem.cs b/Content.Server/Inventory/ServerInventorySystem.cs index c3cfb67268..9920f3f909 100644 --- a/Content.Server/Inventory/ServerInventorySystem.cs +++ b/Content.Server/Inventory/ServerInventorySystem.cs @@ -1,22 +1,14 @@ using Content.Server.Atmos; -using Content.Server.Hands.Components; -using Content.Server.Interaction; using Content.Server.Storage.Components; using Content.Server.Temperature.Systems; using Content.Shared.Inventory; using Content.Shared.Inventory.Events; -using Robust.Shared.Containers; -using Robust.Shared.GameObjects; -using Robust.Shared.IoC; -using Robust.Shared.Map; using InventoryComponent = Content.Shared.Inventory.InventoryComponent; namespace Content.Server.Inventory { class ServerInventorySystem : InventorySystem { - [Dependency] private readonly InteractionSystem _interactionSystem = default!; - public override void Initialize() { base.Initialize(); @@ -25,27 +17,7 @@ namespace Content.Server.Inventory SubscribeLocalEvent(RelayInventoryEvent); SubscribeLocalEvent(RelayInventoryEvent); - SubscribeNetworkEvent(OnNetworkEquip); - SubscribeNetworkEvent(OnNetworkUnequip); SubscribeNetworkEvent(OnOpenSlotStorage); - SubscribeNetworkEvent(OnUseSlot); - } - - private void OnUseSlot(UseSlotNetworkMessage ev) - { - if (!TryComp(ev.Uid, out var hands) || !TryGetSlotEntity(ev.Uid, ev.Slot, out var itemUid)) - return; - - var activeHand = hands.GetActiveHandItem; - if (activeHand != null) - { - _interactionSystem.InteractUsing(ev.Uid, activeHand.Owner, itemUid.Value, - new EntityCoordinates()); - } - else if (TryUnequip(ev.Uid, ev.Slot)) - { - hands.PutInHand(itemUid.Value); - } } private void OnOpenSlotStorage(OpenSlotStorageNetworkMessage ev) @@ -55,15 +27,5 @@ namespace Content.Server.Inventory storageComponent.OpenStorageUI(ev.Uid); } } - - private void OnNetworkUnequip(TryUnequipNetworkMessage ev) - { - TryUnequip(ev.Actor, ev.Target, ev.Slot, ev.Silent, ev.Force); - } - - private void OnNetworkEquip(TryEquipNetworkMessage ev) - { - TryEquip(ev.Actor, ev.Target, ev.ItemUid, ev.Slot, ev.Silent, ev.Force); - } } } diff --git a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs index 6f753b56a3..7e0231806d 100644 --- a/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs +++ b/Content.Shared/Containers/ItemSlot/ItemSlotsSystem.cs @@ -180,7 +180,7 @@ namespace Content.Shared.Containers.ItemSlots if (slot.Item != null) hands.TryPutInAnyHand(slot.Item.Value); - Insert(uid, slot, args.Used, args.User); + Insert(uid, slot, args.Used, args.User, excludeUserAudio: args.Predicted); args.Handled = true; return; } diff --git a/Content.Shared/Interaction/IInteractUsing.cs b/Content.Shared/Interaction/IInteractUsing.cs index c3a380e414..4b3877c840 100644 --- a/Content.Shared/Interaction/IInteractUsing.cs +++ b/Content.Shared/Interaction/IInteractUsing.cs @@ -70,12 +70,19 @@ namespace Content.Shared.Interaction /// public EntityCoordinates ClickLocation { get; } - public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation) + /// + /// If true, this prediction is also being predicted client-side. So care has to be taken to avoid audio + /// duplication. + /// + public bool Predicted { get; } + + public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation, bool predicted = false) { User = user; Used = used; Target = target; ClickLocation = clickLocation; + Predicted = predicted; } } } diff --git a/Content.Shared/Interaction/SharedInteractionSystem.cs b/Content.Shared/Interaction/SharedInteractionSystem.cs index 88342f9a2d..dc4200aa1f 100644 --- a/Content.Shared/Interaction/SharedInteractionSystem.cs +++ b/Content.Shared/Interaction/SharedInteractionSystem.cs @@ -532,7 +532,7 @@ namespace Content.Shared.Interaction /// Finds components with the InteractUsing interface and calls their function /// NOTE: Does not have an InRangeUnobstructed check /// - public async Task InteractUsing(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation) + public async Task InteractUsing(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation, bool predicted = false) { if (!_actionBlockerSystem.CanInteract(user)) return; @@ -541,7 +541,7 @@ namespace Content.Shared.Interaction return; // all interactions should only happen when in range / unobstructed, so no range check is needed - var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation); + var interactUsingEvent = new InteractUsingEvent(user, used, target, clickLocation, predicted); RaiseLocalEvent(target, interactUsingEvent); if (interactUsingEvent.Handled) return; diff --git a/Content.Shared/Inventory/Events/TryEquipNetworkMessage.cs b/Content.Shared/Inventory/Events/TryEquipNetworkMessage.cs deleted file mode 100644 index 83df152213..0000000000 --- a/Content.Shared/Inventory/Events/TryEquipNetworkMessage.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization; - -namespace Content.Shared.Inventory.Events; - -[NetSerializable, Serializable] -public class TryEquipNetworkMessage : EntityEventArgs -{ - public readonly EntityUid Actor; - public readonly EntityUid Target; - public readonly EntityUid ItemUid; - public readonly string Slot; - public readonly bool Silent; - public readonly bool Force; - - public TryEquipNetworkMessage(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent, bool force) - { - Actor = actor; - Target = target; - ItemUid = itemUid; - Slot = slot; - Silent = silent; - Force = force; - } -} diff --git a/Content.Shared/Inventory/Events/TryUnequipNetworkMessage.cs b/Content.Shared/Inventory/Events/TryUnequipNetworkMessage.cs deleted file mode 100644 index 3933c8162e..0000000000 --- a/Content.Shared/Inventory/Events/TryUnequipNetworkMessage.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using Robust.Shared.GameObjects; -using Robust.Shared.Serialization; - -namespace Content.Shared.Inventory.Events; - -[NetSerializable, Serializable] -public class TryUnequipNetworkMessage : EntityEventArgs -{ - public readonly EntityUid Actor; - public readonly EntityUid Target; - public readonly string Slot; - public readonly bool Silent; - public readonly bool Force; - - public TryUnequipNetworkMessage(EntityUid actor, EntityUid target, string slot, bool silent, bool force) - { - Actor = actor; - Target = target; - Slot = slot; - Silent = silent; - Force = force; - } -} diff --git a/Content.Shared/Inventory/Events/UseSlotNetworkMessage.cs b/Content.Shared/Inventory/Events/UseSlotNetworkMessage.cs index e563896433..e99856830c 100644 --- a/Content.Shared/Inventory/Events/UseSlotNetworkMessage.cs +++ b/Content.Shared/Inventory/Events/UseSlotNetworkMessage.cs @@ -7,12 +7,12 @@ namespace Content.Shared.Inventory.Events; [NetSerializable, Serializable] public class UseSlotNetworkMessage : EntityEventArgs { - public readonly EntityUid Uid; + // The slot-owner is implicitly the client that is sending this message. + // Otherwise clients could start forcefully undressing other clients. public readonly string Slot; - public UseSlotNetworkMessage(EntityUid uid, string slot) + public UseSlotNetworkMessage(string slot) { - Uid = uid; Slot = slot; } } diff --git a/Content.Shared/Inventory/InventorySystem.Equip.cs b/Content.Shared/Inventory/InventorySystem.Equip.cs index 79c23ec868..0c13929529 100644 --- a/Content.Shared/Inventory/InventorySystem.Equip.cs +++ b/Content.Shared/Inventory/InventorySystem.Equip.cs @@ -1,16 +1,21 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using Content.Shared.Hands.Components; +using Content.Shared.Interaction; +using Content.Shared.Interaction.Helpers; using Content.Shared.Inventory.Events; using Content.Shared.Item; using Content.Shared.Movement.EntitySystems; using Content.Shared.Popups; +using Content.Shared.Strip.Components; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; +using Robust.Shared.Map; using Robust.Shared.Player; +using Robust.Shared.Timing; namespace Content.Shared.Inventory; @@ -18,12 +23,16 @@ public abstract partial class InventorySystem { [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; + [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; private void InitializeEquip() { //these events ensure that the client also gets its proper events raised when getting its containerstate updated SubscribeLocalEvent(OnEntInserted); SubscribeLocalEvent(OnEntRemoved); + + SubscribeAllEvent(OnUseSlot); } private void OnEntRemoved(EntityUid uid, InventoryComponent component, EntRemovedFromContainerMessage args) @@ -50,50 +59,98 @@ public abstract partial class InventorySystem RaiseLocalEvent(args.Entity, gotEquippedEvent); } - public bool TryEquipActiveHandTo(EntityUid uid, string slot, bool silent = false, bool force = false, - InventoryComponent? component = null, SharedHandsComponent? hands = null) + /// + /// Will attempt to equip or unequip an item to/from the clicked slot. If the user clicked on an occupied slot + /// with some entity, will instead attempt to interact with this entity. + /// + private void OnUseSlot(UseSlotNetworkMessage ev, EntitySessionEventArgs eventArgs) { - if (!Resolve(uid, ref component, false) || !Resolve(uid, ref hands, false)) - return false; + if (eventArgs.SenderSession.AttachedEntity is not EntityUid { Valid: true } actor) + return; - if (!hands.TryGetActiveHeldEntity(out var heldEntity)) - return false; + if (!TryComp(actor, out InventoryComponent? inventory) || !TryComp(actor, out var hands)) + return; - return TryEquip(uid, heldEntity.Value, slot, silent, force, component); - } + hands.TryGetActiveHeldEntity(out var held); + TryGetSlotEntity(actor, ev.Slot, out var itemUid, inventory); - public bool TryEquip(EntityUid uid, EntityUid itemUid, string slot, bool silent = false, bool force = false, + // attempt to perform some interaction + if (held != null && itemUid != null) + { + _interactionSystem.InteractUsing(actor, held.Value, itemUid.Value, + new EntityCoordinates(), predicted: true); + return; + } + + // un-equip to hands + if (itemUid != null) + { + if (hands.CanPickupEntityToActiveHand(itemUid.Value) && TryUnequip(actor, ev.Slot, inventory: inventory)) + hands.PutInHand(itemUid.Value, false); + return; + } + + // finally, just try to equip the held item. + if (held == null) + return; + + // before we drop the item, check that it can be equipped in the first place. + if (!CanEquip(actor, held.Value, ev.Slot, out var reason)) + { + if (_gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString(reason), Filter.Local()); + return; + } + + if (hands.TryDropNoInteraction()) + TryEquip(actor, actor, held.Value, ev.Slot, predicted: true, inventory: inventory); + } + + public bool TryEquip(EntityUid uid, EntityUid itemUid, string slot, bool silent = false, bool force = false, bool predicted = false, InventoryComponent? inventory = null, SharedItemComponent? item = null) => - TryEquip(uid, uid, itemUid, slot, silent, force, inventory, item); + TryEquip(uid, uid, itemUid, slot, silent, force, predicted, inventory, item); - public virtual bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent = false, bool force = false, InventoryComponent? inventory = null, SharedItemComponent? item = null) + public bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent = false, bool force = false, bool predicted = false, + InventoryComponent? inventory = null, SharedItemComponent? item = null) { if (!Resolve(target, ref inventory, false) || !Resolve(itemUid, ref item, false)) { - if(!silent) _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), Filter.Local()); return false; } if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) { - if(!silent) _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString("inventory-component-can-equip-cannot"), Filter.Local()); return false; } if (!force && !CanEquip(actor, target, itemUid, slot, out var reason, slotDefinition, inventory, item)) { - if(!silent) _popup.PopupCursor(Loc.GetString(reason), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString(reason), Filter.Local()); return false; } if (!slotContainer.Insert(itemUid)) { - if(!silent) _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); return false; } - if(!silent && item.EquipSound != null) - SoundSystem.Play(Filter.Pvs(target), item.EquipSound.GetSound(), target, AudioParams.Default.WithVolume(-2f)); + if(!silent && item.EquipSound != null && _gameTiming.IsFirstTimePredicted) + { + var filter = Filter.Pvs(target); + + // don't play double audio for predicted interactions + if (predicted) + filter.RemoveWhereAttachedEntity(entity => entity == actor); + + SoundSystem.Play(filter, item.EquipSound.GetSound(), target, AudioParams.Default.WithVolume(-2f)); + } inventory.Dirty(); @@ -102,6 +159,28 @@ public abstract partial class InventorySystem return true; } + public bool CanAccess(EntityUid actor, EntityUid target, EntityUid itemUid) + { + // Can the actor reach the target? + if (actor != target && !( actor.InRangeUnobstructed(target) && actor.IsInSameOrParentContainer(target))) + return false; + + // Can the actor reach the item? + if (actor.InRangeUnobstructed(itemUid) && actor.IsInSameOrParentContainer(itemUid)) + return true; + + // Is the item in an open storage UI, i.e., is the user quick-equipping from an open backpack? + if (_interactionSystem.CanAccessViaStorage(actor, itemUid)) + return true; + + // Is the actor currently stripping the target? Here we could check if the actor has the stripping UI open, but + // that requires server/client specific code. so lets just check if they **could** open the stripping UI. + // Note that this doesn't check that the item is equipped by the target, as this is done elsewhere. + return actor != target + && TryComp(target, out SharedStrippableComponent? strip) + && strip.CanBeStripped(actor); + } + public bool CanEquip(EntityUid uid, EntityUid itemUid, string slot, [NotNullWhen(false)] out string? reason, SlotDefinition? slotDefinition = null, InventoryComponent? inventory = null, SharedItemComponent? item = null) => @@ -125,6 +204,12 @@ public abstract partial class InventorySystem return false; } + if (!CanAccess(actor, target, itemUid)) + { + reason = "interaction-system-user-interaction-cannot-reach"; + return false; + } + var attemptEvent = new IsEquippingAttemptEvent(actor, target, itemUid, slotDefinition); RaiseLocalEvent(target, attemptEvent); if (attemptEvent.Cancelled) @@ -166,19 +251,21 @@ public abstract partial class InventorySystem public bool TryUnequip(EntityUid uid, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, bool force = false, InventoryComponent? inventory = null) => TryUnequip(uid, uid, slot, out removedItem, silent, force, inventory); - public virtual bool TryUnequip(EntityUid actor, EntityUid target, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, + public bool TryUnequip(EntityUid actor, EntityUid target, string slot, [NotNullWhen(true)] out EntityUid? removedItem, bool silent = false, bool force = false, InventoryComponent? inventory = null) { removedItem = null; if (!Resolve(target, ref inventory, false)) { - if(!silent) _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); return false; } if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) { - if(!silent) _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString("inventory-component-can-unequip-cannot"), Filter.Local()); return false; } @@ -188,7 +275,8 @@ public abstract partial class InventorySystem if (!force && !CanUnequip(actor, target, slot, out var reason, slotContainer, slotDefinition, inventory)) { - if(!silent) _popup.PopupCursor(Loc.GetString(reason), Filter.Local()); + if(!silent && _gameTiming.IsFirstTimePredicted) + _popup.PopupCursor(Loc.GetString(reason), Filter.Local()); return false; } @@ -249,6 +337,13 @@ public abstract partial class InventorySystem var itemUid = containerSlot.ContainedEntity.Value; + // make sure the user can actually reach the target + if (!CanAccess(actor, target, itemUid)) + { + reason = "interaction-system-user-interaction-cannot-reach"; + return false; + } + var attemptEvent = new IsUnequippingAttemptEvent(actor, target, itemUid, slotDefinition); RaiseLocalEvent(target, attemptEvent); if (attemptEvent.Cancelled)