Predict inventory slot interactions. (#6033)

This commit is contained in:
Leon Friedrich
2022-01-30 13:50:10 +13:00
committed by GitHub
parent c465715273
commit 47e597ca47
9 changed files with 138 additions and 140 deletions

View File

@@ -70,20 +70,6 @@ namespace Content.Client.Inventory
_config.OnValueChanged(CCVars.HudTheme, UpdateHudTheme); _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) private void OnDidUnequip(EntityUid uid, ClientInventoryComponent component, DidUnequipEvent args)
{ {
UpdateComponentUISlot(uid, args.Slot, null, component); UpdateComponentUISlot(uid, args.Slot, null, component);
@@ -213,17 +199,15 @@ namespace Content.Client.Inventory
private void HandleSlotButtonPressed(EntityUid uid, string slot, ItemSlotButton button, private void HandleSlotButtonPressed(EntityUid uid, string slot, ItemSlotButton button,
GUIBoundKeyEventArgs args) GUIBoundKeyEventArgs args)
{ {
if (TryGetSlotEntity(uid, slot, out var itemUid)) if (TryGetSlotEntity(uid, slot, out var itemUid) && _itemSlotManager.OnButtonPressed(args, itemUid.Value))
{
if (!_itemSlotManager.OnButtonPressed(args, itemUid.Value) && args.Function == EngineKeyFunctions.UIClick)
{
RaiseNetworkEvent(new UseSlotNetworkMessage(uid, slot));
}
return; return;
}
if (args.Function != EngineKeyFunctions.UIClick) return; if (args.Function != EngineKeyFunctions.UIClick)
TryEquipActiveHandTo(uid, slot); 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, private bool TryGetUIElements(EntityUid uid, [NotNullWhen(true)] out DefaultWindow? invWindow,

View File

@@ -1,22 +1,14 @@
using Content.Server.Atmos; using Content.Server.Atmos;
using Content.Server.Hands.Components;
using Content.Server.Interaction;
using Content.Server.Storage.Components; using Content.Server.Storage.Components;
using Content.Server.Temperature.Systems; using Content.Server.Temperature.Systems;
using Content.Shared.Inventory; using Content.Shared.Inventory;
using Content.Shared.Inventory.Events; 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; using InventoryComponent = Content.Shared.Inventory.InventoryComponent;
namespace Content.Server.Inventory namespace Content.Server.Inventory
{ {
class ServerInventorySystem : InventorySystem class ServerInventorySystem : InventorySystem
{ {
[Dependency] private readonly InteractionSystem _interactionSystem = default!;
public override void Initialize() public override void Initialize()
{ {
base.Initialize(); base.Initialize();
@@ -25,27 +17,7 @@ namespace Content.Server.Inventory
SubscribeLocalEvent<InventoryComponent, LowPressureEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, LowPressureEvent>(RelayInventoryEvent);
SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent); SubscribeLocalEvent<InventoryComponent, ModifyChangedTemperatureEvent>(RelayInventoryEvent);
SubscribeNetworkEvent<TryEquipNetworkMessage>(OnNetworkEquip);
SubscribeNetworkEvent<TryUnequipNetworkMessage>(OnNetworkUnequip);
SubscribeNetworkEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage); SubscribeNetworkEvent<OpenSlotStorageNetworkMessage>(OnOpenSlotStorage);
SubscribeNetworkEvent<UseSlotNetworkMessage>(OnUseSlot);
}
private void OnUseSlot(UseSlotNetworkMessage ev)
{
if (!TryComp<HandsComponent>(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) private void OnOpenSlotStorage(OpenSlotStorageNetworkMessage ev)
@@ -55,15 +27,5 @@ namespace Content.Server.Inventory
storageComponent.OpenStorageUI(ev.Uid); 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);
}
} }
} }

View File

@@ -180,7 +180,7 @@ namespace Content.Shared.Containers.ItemSlots
if (slot.Item != null) if (slot.Item != null)
hands.TryPutInAnyHand(slot.Item.Value); 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; args.Handled = true;
return; return;
} }

View File

@@ -70,12 +70,19 @@ namespace Content.Shared.Interaction
/// </summary> /// </summary>
public EntityCoordinates ClickLocation { get; } public EntityCoordinates ClickLocation { get; }
public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation) /// <summary>
/// If true, this prediction is also being predicted client-side. So care has to be taken to avoid audio
/// duplication.
/// </summary>
public bool Predicted { get; }
public InteractUsingEvent(EntityUid user, EntityUid used, EntityUid target, EntityCoordinates clickLocation, bool predicted = false)
{ {
User = user; User = user;
Used = used; Used = used;
Target = target; Target = target;
ClickLocation = clickLocation; ClickLocation = clickLocation;
Predicted = predicted;
} }
} }
} }

View File

@@ -532,7 +532,7 @@ namespace Content.Shared.Interaction
/// Finds components with the InteractUsing interface and calls their function /// Finds components with the InteractUsing interface and calls their function
/// NOTE: Does not have an InRangeUnobstructed check /// NOTE: Does not have an InRangeUnobstructed check
/// </summary> /// </summary>
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)) if (!_actionBlockerSystem.CanInteract(user))
return; return;
@@ -541,7 +541,7 @@ namespace Content.Shared.Interaction
return; return;
// all interactions should only happen when in range / unobstructed, so no range check is needed // 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); RaiseLocalEvent(target, interactUsingEvent);
if (interactUsingEvent.Handled) if (interactUsingEvent.Handled)
return; return;

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -7,12 +7,12 @@ namespace Content.Shared.Inventory.Events;
[NetSerializable, Serializable] [NetSerializable, Serializable]
public class UseSlotNetworkMessage : EntityEventArgs 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 readonly string Slot;
public UseSlotNetworkMessage(EntityUid uid, string slot) public UseSlotNetworkMessage(string slot)
{ {
Uid = uid;
Slot = slot; Slot = slot;
} }
} }

View File

@@ -1,16 +1,21 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Content.Shared.Hands.Components; using Content.Shared.Hands.Components;
using Content.Shared.Interaction;
using Content.Shared.Interaction.Helpers;
using Content.Shared.Inventory.Events; using Content.Shared.Inventory.Events;
using Content.Shared.Item; using Content.Shared.Item;
using Content.Shared.Movement.EntitySystems; using Content.Shared.Movement.EntitySystems;
using Content.Shared.Popups; using Content.Shared.Popups;
using Content.Shared.Strip.Components;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.Containers; using Robust.Shared.Containers;
using Robust.Shared.GameObjects; using Robust.Shared.GameObjects;
using Robust.Shared.IoC; using Robust.Shared.IoC;
using Robust.Shared.Localization; using Robust.Shared.Localization;
using Robust.Shared.Map;
using Robust.Shared.Player; using Robust.Shared.Player;
using Robust.Shared.Timing;
namespace Content.Shared.Inventory; namespace Content.Shared.Inventory;
@@ -18,12 +23,16 @@ public abstract partial class InventorySystem
{ {
[Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!; [Dependency] private readonly MovementSpeedModifierSystem _movementSpeed = default!;
[Dependency] private readonly SharedInteractionSystem _interactionSystem = default!;
[Dependency] private readonly IGameTiming _gameTiming = default!;
private void InitializeEquip() private void InitializeEquip()
{ {
//these events ensure that the client also gets its proper events raised when getting its containerstate updated //these events ensure that the client also gets its proper events raised when getting its containerstate updated
SubscribeLocalEvent<InventoryComponent, EntInsertedIntoContainerMessage>(OnEntInserted); SubscribeLocalEvent<InventoryComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent<InventoryComponent, EntRemovedFromContainerMessage>(OnEntRemoved); SubscribeLocalEvent<InventoryComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeAllEvent<UseSlotNetworkMessage>(OnUseSlot);
} }
private void OnEntRemoved(EntityUid uid, InventoryComponent component, EntRemovedFromContainerMessage args) private void OnEntRemoved(EntityUid uid, InventoryComponent component, EntRemovedFromContainerMessage args)
@@ -50,50 +59,98 @@ public abstract partial class InventorySystem
RaiseLocalEvent(args.Entity, gotEquippedEvent); RaiseLocalEvent(args.Entity, gotEquippedEvent);
} }
public bool TryEquipActiveHandTo(EntityUid uid, string slot, bool silent = false, bool force = false, /// <summary>
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.
/// </summary>
private void OnUseSlot(UseSlotNetworkMessage ev, EntitySessionEventArgs eventArgs)
{ {
if (!Resolve(uid, ref component, false) || !Resolve(uid, ref hands, false)) if (eventArgs.SenderSession.AttachedEntity is not EntityUid { Valid: true } actor)
return false; return;
if (!hands.TryGetActiveHeldEntity(out var heldEntity)) if (!TryComp(actor, out InventoryComponent? inventory) || !TryComp<SharedHandsComponent>(actor, out var hands))
return false; return;
return TryEquip(uid, heldEntity.Value, slot, silent, force, component); hands.TryGetActiveHeldEntity(out var held);
TryGetSlotEntity(actor, ev.Slot, out var itemUid, inventory);
// attempt to perform some interaction
if (held != null && itemUid != null)
{
_interactionSystem.InteractUsing(actor, held.Value, itemUid.Value,
new EntityCoordinates(), predicted: true);
return;
} }
public bool TryEquip(EntityUid uid, EntityUid itemUid, string slot, bool silent = false, bool force = false, // un-equip to hands
InventoryComponent? inventory = null, SharedItemComponent? item = null) => if (itemUid != null)
TryEquip(uid, uid, itemUid, slot, silent, force, inventory, item); {
if (hands.CanPickupEntityToActiveHand(itemUid.Value) && TryUnequip(actor, ev.Slot, inventory: inventory))
hands.PutInHand(itemUid.Value, false);
return;
}
public virtual bool TryEquip(EntityUid actor, EntityUid target, EntityUid itemUid, string slot, bool silent = false, bool force = false, InventoryComponent? inventory = null, SharedItemComponent? item = null) // 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, predicted, inventory, item);
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 (!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; return false;
} }
if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) 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; return false;
} }
if (!force && !CanEquip(actor, target, itemUid, slot, out var reason, slotDefinition, inventory, item)) 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; return false;
} }
if (!slotContainer.Insert(itemUid)) 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; return false;
} }
if(!silent && item.EquipSound != null) if(!silent && item.EquipSound != null && _gameTiming.IsFirstTimePredicted)
SoundSystem.Play(Filter.Pvs(target), item.EquipSound.GetSound(), target, AudioParams.Default.WithVolume(-2f)); {
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(); inventory.Dirty();
@@ -102,6 +159,28 @@ public abstract partial class InventorySystem
return true; 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, public bool CanEquip(EntityUid uid, EntityUid itemUid, string slot, [NotNullWhen(false)] out string? reason,
SlotDefinition? slotDefinition = null, InventoryComponent? inventory = null, SlotDefinition? slotDefinition = null, InventoryComponent? inventory = null,
SharedItemComponent? item = null) => SharedItemComponent? item = null) =>
@@ -125,6 +204,12 @@ public abstract partial class InventorySystem
return false; return false;
} }
if (!CanAccess(actor, target, itemUid))
{
reason = "interaction-system-user-interaction-cannot-reach";
return false;
}
var attemptEvent = new IsEquippingAttemptEvent(actor, target, itemUid, slotDefinition); var attemptEvent = new IsEquippingAttemptEvent(actor, target, itemUid, slotDefinition);
RaiseLocalEvent(target, attemptEvent); RaiseLocalEvent(target, attemptEvent);
if (attemptEvent.Cancelled) 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, 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); 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) bool force = false, InventoryComponent? inventory = null)
{ {
removedItem = null; removedItem = null;
if (!Resolve(target, ref inventory, false)) 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; return false;
} }
if (!TryGetSlotContainer(target, slot, out var slotContainer, out var slotDefinition, inventory)) 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; return false;
} }
@@ -188,7 +275,8 @@ public abstract partial class InventorySystem
if (!force && !CanUnequip(actor, target, slot, out var reason, slotContainer, slotDefinition, inventory)) 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; return false;
} }
@@ -249,6 +337,13 @@ public abstract partial class InventorySystem
var itemUid = containerSlot.ContainedEntity.Value; 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); var attemptEvent = new IsUnequippingAttemptEvent(actor, target, itemUid, slotDefinition);
RaiseLocalEvent(target, attemptEvent); RaiseLocalEvent(target, attemptEvent);
if (attemptEvent.Cancelled) if (attemptEvent.Cancelled)