diff --git a/Content.Client/Input/ContentContexts.cs b/Content.Client/Input/ContentContexts.cs index 8c8dd1a4748..ac59adc1611 100644 --- a/Content.Client/Input/ContentContexts.cs +++ b/Content.Client/Input/ContentContexts.cs @@ -34,6 +34,9 @@ namespace Content.Client.Input common.AddFunction(ContentKeyFunctions.RotateStoredItem); common.AddFunction(ContentKeyFunctions.SaveItemLocation); common.AddFunction(ContentKeyFunctions.Point); + // Floofstation section + common.AddFunction(ContentKeyFunctions.OfferItem); + // Floofstation section end common.AddFunction(ContentKeyFunctions.ZoomOut); common.AddFunction(ContentKeyFunctions.ZoomIn); common.AddFunction(ContentKeyFunctions.ResetZoom); diff --git a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs index 5e1bcd59857..79c36c969e5 100644 --- a/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs +++ b/Content.Client/Options/UI/Tabs/KeyRebindTab.xaml.cs @@ -208,6 +208,7 @@ namespace Content.Client.Options.UI.Tabs AddButton(ContentKeyFunctions.MovePulledObject); AddButton(ContentKeyFunctions.ReleasePulledObject); AddButton(ContentKeyFunctions.Point); + AddButton(ContentKeyFunctions.OfferItem); // Floofstation AddButton(ContentKeyFunctions.RotateObjectClockwise); AddButton(ContentKeyFunctions.RotateObjectCounterclockwise); AddButton(ContentKeyFunctions.FlipObject); diff --git a/Content.Client/_Floof/OfferItem/OfferItemIndicatorsOverlay.cs b/Content.Client/_Floof/OfferItem/OfferItemIndicatorsOverlay.cs new file mode 100644 index 00000000000..c306303b5b3 --- /dev/null +++ b/Content.Client/_Floof/OfferItem/OfferItemIndicatorsOverlay.cs @@ -0,0 +1,71 @@ +using System.Numerics; +using Robust.Client.GameObjects; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.UserInterface; +using Robust.Shared.Enums; +using Robust.Shared.Utility; + +namespace Content.Client._Floof.OfferItem; + +public sealed class OfferItemIndicatorsOverlay : Overlay +{ + private readonly IInputManager _inputManager; + private readonly IEntityManager _entMan; + private readonly IEyeManager _eye; + private readonly OfferItemSystem _offer; + + private readonly Texture _sight; + + public override OverlaySpace Space => OverlaySpace.ScreenSpace; + + private readonly Color _mainColor = Color.White.WithAlpha(0.3f); + private readonly Color _strokeColor = Color.Black.WithAlpha(0.5f); + private readonly float _scale = 0.6f; // 1 is a little big + + public OfferItemIndicatorsOverlay(IInputManager input, IEntityManager entMan, + IEyeManager eye, OfferItemSystem offerSys) + { + _inputManager = input; + _entMan = entMan; + _eye = eye; + _offer = offerSys; + + var spriteSys = _entMan.EntitySysManager.GetEntitySystem(); + _sight = spriteSys.Frame0(new SpriteSpecifier.Rsi(new("/Textures/_Floof/Interface/Misc/give_item.rsi"), "give_item")); + } + + protected override bool BeforeDraw(in OverlayDrawArgs args) + { + if (!_offer.IsInOfferMode()) + return false; + + return base.BeforeDraw(in args); + } + + protected override void Draw(in OverlayDrawArgs args) + { + var mouseScreenPosition = _inputManager.MouseScreenPosition; + var mousePosMap = _eye.PixelToMap(mouseScreenPosition); + if (mousePosMap.MapId != args.MapId) + return; + + + var mousePos = mouseScreenPosition.Position; + var uiScale = (args.ViewportControl as Control)?.UIScale ?? 1f; + var limitedScale = uiScale > 1.25f ? 1.25f : uiScale; + + DrawSight(_sight, args.ScreenHandle, mousePos, limitedScale * _scale); + } + + private void DrawSight(Texture sight, DrawingHandleScreen screen, Vector2 centerPos, float scale) + { + var sightSize = sight.Size * scale; + var expandedSize = sightSize + new Vector2(7f, 7f); + + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - sightSize * 0.5f, sightSize), _strokeColor); + screen.DrawTextureRect(sight, + UIBox2.FromDimensions(centerPos - expandedSize * 0.5f, expandedSize), _mainColor); + } +} diff --git a/Content.Client/_Floof/OfferItem/OfferItemSystem.cs b/Content.Client/_Floof/OfferItem/OfferItemSystem.cs new file mode 100644 index 00000000000..0487d361fc0 --- /dev/null +++ b/Content.Client/_Floof/OfferItem/OfferItemSystem.cs @@ -0,0 +1,39 @@ +using Content.Shared._Floof.OfferItem; +using Robust.Client.Graphics; +using Robust.Client.Input; +using Robust.Client.Player; + +namespace Content.Client._Floof.OfferItem; + +public sealed class OfferItemSystem : SharedOfferItemSystem +{ + [Dependency] private readonly IOverlayManager _overlayManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IInputManager _inputManager = default!; + [Dependency] private readonly IEyeManager _eye = default!; + + public override void Initialize() + { + base.Initialize(); + _overlayManager.AddOverlay(new OfferItemIndicatorsOverlay( + _inputManager, + EntityManager, + _eye, + this)); + } + public override void Shutdown() + { + _overlayManager.RemoveOverlay(); + base.Shutdown(); + } + + public bool IsInOfferMode() + { + var entity = _playerManager.LocalEntity; + + if (entity == null) + return false; + + return IsInOfferMode(entity.Value); + } +} diff --git a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs index b4a3649c5cb..953370aa9b8 100644 --- a/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs +++ b/Content.Server/Nyanotrasen/Item/PseudoItem/PseudoItemSystem.cs @@ -55,9 +55,13 @@ public sealed class PseudoItemSystem : SharedPseudoItemSystem protected override void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) { + // Floof - changed this a bit to actually start a do-after // Try to pick the entity up instead first - if (args.User != args.Item && _carrying.TryCarry(args.User, uid)) + if (args.User != args.Item + && TryComp(uid, out var carriable) + && _carrying.CanCarry(args.User, (uid, carriable))) { + _carrying.StartCarryDoAfter(args.User, (uid, carriable)); args.Cancel(); return; } diff --git a/Content.Server/_Floof/OfferItem/OfferItemSystem.cs b/Content.Server/_Floof/OfferItem/OfferItemSystem.cs new file mode 100644 index 00000000000..b72ce2d5849 --- /dev/null +++ b/Content.Server/_Floof/OfferItem/OfferItemSystem.cs @@ -0,0 +1,5 @@ +using Content.Shared._Floof.OfferItem; + +namespace Content.Server._Floof.OfferItem; + +public sealed partial class OfferItemSystem : SharedOfferItemSystem; diff --git a/Content.Shared/Input/ContentKeyFunctions.cs b/Content.Shared/Input/ContentKeyFunctions.cs index a7e4164b920..6b633d39b13 100644 --- a/Content.Shared/Input/ContentKeyFunctions.cs +++ b/Content.Shared/Input/ContentKeyFunctions.cs @@ -68,6 +68,7 @@ namespace Content.Shared.Input public static readonly BoundKeyFunction TakeScreenshotNoUI = "TakeScreenshotNoUI"; public static readonly BoundKeyFunction ToggleFullscreen = "ToggleFullscreen"; public static readonly BoundKeyFunction Point = "Point"; + public static readonly BoundKeyFunction OfferItem = "OfferItem"; // Floofstation public static readonly BoundKeyFunction ZoomOut = "ZoomOut"; public static readonly BoundKeyFunction ZoomIn = "ZoomIn"; public static readonly BoundKeyFunction ResetZoom = "ResetZoom"; diff --git a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs index 490050553d3..62aaae1e691 100644 --- a/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs +++ b/Content.Shared/Movement/Pulling/Systems/PullingSystem.cs @@ -1,4 +1,5 @@ using Content.Shared._ST.Interaction; // Stellar - interaction particles +using Content.Shared._Floof.OfferItem; // Floof using Content.Shared.ActionBlocker; using Content.Shared.Administration.Logs; using Content.Shared.Alert; @@ -117,10 +118,11 @@ public sealed class PullingSystem : EntitySystem if (TryComp(args.PullerUid, out PullerComponent? pullerComp) && !pullerComp.NeedsHands) return; - if (!_virtual.TrySpawnVirtualItemInHand(args.PulledUid, uid)) + if (!_virtual.TrySpawnVirtualItemInHand(args.PulledUid, uid, out var virt)) // Floofstation - store item { DebugTools.Assert("Unable to find available hand when starting pulling??"); } + EnsureComp(virt.Value); // Floofstation - add a special component to allow offering it } private void HandlePullStopped(EntityUid uid, HandsComponent component, PullStoppedMessage args) diff --git a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs index bcbdd4b8629..c7a4e0f74b6 100644 --- a/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs +++ b/Content.Shared/Nyanotrasen/Item/PseudoItem/SharedPseudoItemSystem.cs @@ -1,8 +1,7 @@ using Content.Shared.Actions; -using Content.Shared.Bed.Sleep; using Content.Shared.DoAfter; using Content.Shared.Hands; -using Content.Shared.IdentityManagement; +using Content.Shared.Hands.EntitySystems; using Content.Shared.Interaction.Events; using Content.Shared.Item; using Content.Shared.Item.PseudoItem; @@ -24,7 +23,8 @@ public abstract partial class SharedPseudoItemSystem : EntitySystem [Dependency] private readonly TagSystem _tag = default!; [Dependency] private readonly SharedPopupSystem _popupSystem = default!; [Dependency] private readonly SharedActionsSystem _actions = default!; - [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedContainerSystem _container = default!; // Floofstation + [Dependency] private readonly SharedHandsSystem _hands = default!; // Floofstation private readonly ProtoId PreventTag = "PreventLabel"; private readonly EntProtoId SleepActionId = "ActionSleep"; // The action used for sleeping inside bags. Currently uses the default sleep action (same as beds) @@ -116,17 +116,35 @@ public abstract partial class SharedPseudoItemSystem : EntitySystem protected virtual void OnGettingPickedUpAttempt(EntityUid uid, PseudoItemComponent component, GettingPickedUpAttemptEvent args) { - if (args.User == args.Item) - return; + args.Cancel(); // Floof - this is a terrible idea. This triggers every time ANY system checks if a pseudo-item can be picked up. + // WHY DID YOU DO THAT, NYANOTRASEN??? - _transform.AttachToGridOrMap(uid); - args.Cancel(); + // if (args.User == args.Item) + // return; + // + // _transform.AttachToGridOrMap(uid); + // args.Cancel(); } private void OnDropAttempt(EntityUid uid, PseudoItemComponent component, DropAttemptEvent args) { - if (component.Active) - args.Cancel(); + if (!component.Active) + return; + + // Floof - we try to get the containing container and try to drop it into it + // If possible, we do it, since a bagged cat probably can put things back into the bag just like they can pick them up. + string? failReason = null; + if (_hands.GetActiveItem(uid) is { Valid: true, } droppedItem + && _container.TryGetContainingContainer(Transform(uid).ParentUid, uid, out var pseudoItemContainer) + && TryComp(pseudoItemContainer.Owner, out var targetStorage) + && _storage.CanInsert(pseudoItemContainer.Owner, droppedItem, out failReason, targetStorage, ignoreStacks: true) + ) + _storage.Insert(pseudoItemContainer.Owner, droppedItem, out _, uid, targetStorage, stackAutomatically: false); + + if (failReason != null) + _popupSystem.PopupEntity(Loc.GetString(failReason), uid, uid); + + args.Cancel(); } private void OnInsertAttempt(EntityUid uid, PseudoItemComponent component, @@ -141,8 +159,9 @@ public abstract partial class SharedPseudoItemSystem : EntitySystem // Prevents moving within the bag :) private void OnInteractAttempt(EntityUid uid, PseudoItemComponent component, InteractionAttemptEvent args) { - if (args.Uid == args.Target && component.Active) - args.Cancelled = true; + // Floof - why the fuck. + // if (args.Uid == args.Target && component.Active) + // args.Cancelled = true; } private void OnDoAfter(EntityUid uid, PseudoItemComponent component, DoAfterEvent args) diff --git a/Content.Shared/_DV/Carrying/CarryingSystem.cs b/Content.Shared/_DV/Carrying/CarryingSystem.cs index aa836f81cd0..a6a62d7ff66 100644 --- a/Content.Shared/_DV/Carrying/CarryingSystem.cs +++ b/Content.Shared/_DV/Carrying/CarryingSystem.cs @@ -28,6 +28,7 @@ using Robust.Shared.Network; using Robust.Shared.Physics.Components; using System.Numerics; using Content.Shared._DV.Polymorph; +using Content.Shared._Floof.OfferItem; using Content.Shared.Hands.EntitySystems; namespace Content.Shared._DV.Carrying; @@ -181,14 +182,15 @@ public sealed class CarryingSystem : EntitySystem /// private void OnInteractionAttempt(Entity ent, ref InteractionAttemptEvent args) { - if (args.Target is not {} target) - return; - - var targetParent = Transform(target).ParentUid; - - var carrier = ent.Comp.Carrier; - if (target != carrier && targetParent != carrier && targetParent != ent.Owner) - args.Cancelled = true; + // Floofstation - no - this prevents the person from escaping and more. + // if (args.Target is not {} target) + // return; + // + // var targetParent = Transform(target).ParentUid; + // + // var carrier = ent.Comp.Carrier; + // if (target != carrier && targetParent != carrier && targetParent != ent.Owner) + // args.Cancelled = true; } private void OnMoveAttempt(Entity ent, ref UpdateCanMoveEvent args) @@ -203,8 +205,9 @@ public sealed class CarryingSystem : EntitySystem private void OnInteractedWith(Entity ent, ref GettingInteractedWithAttemptEvent args) { - if (args.Uid != ent.Comp.Carrier) - args.Cancelled = true; + // Floofstation - why? + // if (args.Uid != ent.Comp.Carrier) + // args.Cancelled = true; } private void OnPullAttempt(Entity ent, ref PullAttemptEvent args) @@ -242,7 +245,8 @@ public sealed class CarryingSystem : EntitySystem args.Handled = true; } - private void StartCarryDoAfter(EntityUid carrier, Entity carried) + // Floofstation - made public + public void StartCarryDoAfter(EntityUid carrier, Entity carried) { TimeSpan length = GetPickupDuration(carrier, carried); @@ -299,7 +303,8 @@ public sealed class CarryingSystem : EntitySystem for (var x = 0; x < Comp(carried).FreeHandsRequired; x++) { - _virtualItem.TrySpawnVirtualItemInHand(carried, carrier); + if (_virtualItem.TrySpawnVirtualItemInHand(carried, carrier, out var virtualItem)) + EnsureComp(virtualItem.Value); } } diff --git a/Content.Shared/_Floof/OfferItem/AcceptOfferAlertEvent.cs b/Content.Shared/_Floof/OfferItem/AcceptOfferAlertEvent.cs new file mode 100644 index 00000000000..1f5e2e851ad --- /dev/null +++ b/Content.Shared/_Floof/OfferItem/AcceptOfferAlertEvent.cs @@ -0,0 +1,5 @@ +using Content.Shared.Alert; + +namespace Content.Shared._Floof.OfferItem; + +public sealed partial class AcceptOfferAlertEvent : BaseAlertEvent; diff --git a/Content.Shared/_Floof/OfferItem/OfferItemComponent.cs b/Content.Shared/_Floof/OfferItem/OfferItemComponent.cs new file mode 100644 index 00000000000..926f397ce14 --- /dev/null +++ b/Content.Shared/_Floof/OfferItem/OfferItemComponent.cs @@ -0,0 +1,52 @@ +using Content.Shared.Alert; +using Content.Shared.Inventory.VirtualItem; +using Robust.Shared.GameStates; +using Robust.Shared.Prototypes; + +namespace Content.Shared._Floof.OfferItem; + +[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(true)] +[Access(typeof(SharedOfferItemSystem))] +public sealed partial class OfferItemComponent : Component +{ + /// + /// Apparently this indicates whether the entity is currently choosing an entity to offer (right after pressing F). + /// + [ViewVariables(VVAccess.ReadWrite), DataField, AutoNetworkedField] + public bool IsInOfferMode; + + /// + /// If this is true, then someone is currently offering an item to this entity, and + /// stores the ID of that entity. + /// + [DataField, AutoNetworkedField] + public bool IsInReceiveMode; + + [DataField, AutoNetworkedField] + public string? Hand; + + [DataField, AutoNetworkedField] + public EntityUid? Item; + + /// + /// Floofstation note. So, this is EE shitcode, so prepare for an emotional rollercoaster. + /// This field can mean TWO things. It's either the target entity this entity is offering an item to, + /// or an entity that is offering an item to this entity. + /// Whether it's one or the other is distinguished by .

+ /// + /// In rare cases it can be both. According to my research, if entity A offers an item to entity B, and entity B offers to entity A, + /// then both entities will end up in receive mode, and they will have each other as targets. There's a check preventing offer loops + /// of length more than 2. + ///
+ [DataField, AutoNetworkedField] + public EntityUid? ReceivingFrom; + + [DataField] + public float MaxOfferDistance = 2f; + + [DataField] + public ProtoId OfferAlert = "Offer"; + + public EntityUid GetRealEntity(EntityManager entityManager) => + entityManager.GetComponentOrNull(Item)?.BlockingEntity ?? Item ?? EntityUid.Invalid; +} diff --git a/Content.Shared/_Floof/OfferItem/OfferableVirtualItemComponent.cs b/Content.Shared/_Floof/OfferItem/OfferableVirtualItemComponent.cs new file mode 100644 index 00000000000..0c4b994c400 --- /dev/null +++ b/Content.Shared/_Floof/OfferItem/OfferableVirtualItemComponent.cs @@ -0,0 +1,11 @@ +using Robust.Shared.GameStates; + +namespace Content.Shared._Floof.OfferItem; + +/// +/// A marker component that, when applied to a virtual item, allows it to be offered using item offering. +/// Implementors have to listen on ItemTransferredEvent. +/// +[RegisterComponent] +[NetworkedComponent] +public sealed partial class OfferableVirtualItemComponent : Component; diff --git a/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.Interactions.cs b/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.Interactions.cs new file mode 100644 index 00000000000..cac0280cd1f --- /dev/null +++ b/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.Interactions.cs @@ -0,0 +1,93 @@ +using Content.Shared.ActionBlocker; +using Content.Shared.Alert; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.Input; +using Content.Shared.Popups; +using Robust.Shared.Input.Binding; +using Robust.Shared.Player; + +namespace Content.Shared._Floof.OfferItem; + +public abstract partial class SharedOfferItemSystem +{ + [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!; + [Dependency] private readonly SharedPopupSystem _popup = default!; + + + private void InitializeInteractions() + { + base.Initialize(); + + CommandBinds.Builder + .Bind(ContentKeyFunctions.OfferItem, InputCmdHandler.FromDelegate(SetInOfferMode, handle: false, outsidePrediction: false)) + .Register(); + } + + public override void Shutdown() + { + base.Shutdown(); + + CommandBinds.Unregister(); + } + + /// + /// This sets IsInOfferMode to true, allowing the player to select whom to offer an item to with interaction. + /// + private void SetInOfferMode(ICommonSession? offerer) + { + if (offerer is not { } playerSession) + return; + + if (playerSession.AttachedEntity is not { Valid: true } uid) + return; + + if (!Exists(uid)) + return; + + if (!_actionBlocker.CanInteract(uid, null)) + return; + + if (!TryComp(uid, out var offerItem)) + return; + + if (!TryComp(uid, out var hands)) + return; + + if (_hands.GetActiveHand((uid, hands)) is not { } activeHandName) + return; + + if (!_hands.TryGetHeldItem((uid, hands), activeHandName, out var heldItem)) + return; + + offerItem.Item = heldItem; + if (!offerItem.IsInOfferMode) + { + if (offerItem.Item == null) + { + _popup.PopupEntity(Loc.GetString("offer-item-empty-hand"), uid, uid); + return; + } + + if (offerItem.Hand == null || offerItem.ReceivingFrom == null) + { + offerItem.IsInOfferMode = true; + offerItem.Hand = activeHandName; + + Dirty(uid, offerItem); + return; + } + } + + // If we're already offering an item to someone, cancel that offer + if (offerItem.ReceivingFrom != null) + { + UnReceive(offerItem.ReceivingFrom.Value, offererComp: offerItem); + offerItem.IsInOfferMode = false; + Dirty(uid, offerItem); + return; + } + + UnOffer(uid, offerItem); + } +} diff --git a/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.cs b/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.cs new file mode 100644 index 00000000000..9392a226fa2 --- /dev/null +++ b/Content.Shared/_Floof/OfferItem/SharedOfferItemSystem.cs @@ -0,0 +1,414 @@ +using Content.Shared._DV.Carrying; +using Content.Shared.Alert; +using Content.Shared.Hands.Components; +using Content.Shared.Hands.EntitySystems; +using Content.Shared.IdentityManagement; +using Content.Shared.Interaction; +using Content.Shared.Movement.Pulling.Components; +using Content.Shared.Movement.Pulling.Systems; +using Content.Shared.Nutrition.EntitySystems; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Timing; + +// Dear contributor. +// This system is fucking unmaintainable. +// If you ever happen to touch this again, please do your best to document your changes and try to resolve mysteries surrounding this code. +// I did what I could to document the parts I managed to understand, but there is still more truth to be unveiled. +// +// HOURS_WASTED_HERE_FLOOFSTATION = 10 +// HOURS_WASTED_HERE_DELTAV = 1 + +namespace Content.Shared._Floof.OfferItem; + +public abstract partial class SharedOfferItemSystem : EntitySystem +{ + [Dependency] private readonly IGameTiming _timing = default!; + [Dependency] private readonly INetManager _net = default!; + [Dependency] private readonly SharedTransformSystem _transform = default!; + [Dependency] private readonly SharedHandsSystem _hands = default!; + [Dependency] private readonly AlertsSystem _alertsSystem = default!; + [Dependency] private readonly CarryingSystem _carrying = default!; + [Dependency] private readonly PullingSystem _pulling = default!; + + public override void Initialize() + { + base.Initialize(); + SubscribeLocalEvent(OnAcceptOffer); + SubscribeLocalEvent(OnInteractWithReceiver, before: [typeof(IngestionSystem)]); + SubscribeLocalEvent(OnRangedInteractWithReceiver); + SubscribeLocalEvent(OnMove); + + SubscribeLocalEvent(OnCarryTransfer); + SubscribeLocalEvent(OnPulledTransfer); + + InitializeInteractions(); + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + + var query = EntityQueryEnumerator(); + while (query.MoveNext(out var uid, out var offerItem, out var hands)) + { + // If the mob no longer holds an item in the original offering hand, clear offering mode + if (offerItem.Hand != null && !_hands.TryGetHeldItem((uid, hands), offerItem.Hand, out _)) + { + if (offerItem.ReceivingFrom != null) + { + UnReceive(offerItem.ReceivingFrom.Value, offererComp: offerItem); + offerItem.IsInOfferMode = false; + Dirty(uid, offerItem); + } + else + UnOffer(uid, offerItem); + } + + if (!offerItem.IsInReceiveMode) + { + _alertsSystem.ClearAlert(uid, offerItem.OfferAlert); + continue; + } + + _alertsSystem.ShowAlert(uid, offerItem.OfferAlert); + } + } + + #region Events + private void OnAcceptOffer(Entity ent, ref AcceptOfferAlertEvent args) + { + Receive((ent, ent.Comp)); + } + + private void OnInteractWithReceiver(Entity receiver, ref InteractUsingEvent args) + { + if (!_timing.IsFirstTimePredicted || _timing.ApplyingState || args.Handled) + return; + + if (!TryComp(args.User, out var offererComponent)) + return; + + args.Handled = CreateOffer(receiver, (args.User, offererComponent)); + } + + private void OnRangedInteractWithReceiver(Entity virtItem, ref BeforeRangedInteractEvent args) + { + // If the entity being offered is a virtual item, InteractUsing will not be raised + // because virtual items exclude themselves from being marked as used + // If this is the case, InteractHand will be raised instead, which we can use anyway because OfferItem.Item stores the offered item + // + // We also can't check Handled here because VirtualItemSystem handles it, ffs + // This won't lead you to accidentally offering someone your gun + // + // This is shitcode, this time my shitcode. My changes to the offering system allow you to transfer carrying and pulling, + // but in order to handle these, we need to be able to intercept interactions with virtual items. + // + // Ideally this code should be rewritten to: + // a) Have each different virtual item have a distinct component (e.g. CarryingVirtualItem) which would allow to distinguish them from the rest + // b) Not rely on the InteractionSystem. + // However, I'm not in the mood to do either. And I'm too deep into the rabbit hole of getting this shit to work. + if (!_timing.IsFirstTimePredicted || _timing.ApplyingState) + return; + + var receiver = args.Target; + if (!TryComp(receiver, out var receiverComponent)) + return; + + var offerer = args.User; + if (!TryComp(offerer, out var offererComponent) || offererComponent.Item == null) + return; + + // Since this is ranged, we must also check distance, because the interaction system wont check it for us in this case + if (!Transform(offerer).Coordinates.TryDistance(EntityManager, _transform, Transform(receiver.Value).Coordinates, out var dst) + || dst > offererComponent.MaxOfferDistance) + return; + + args.Handled = CreateOffer((receiver.Value, receiverComponent), (offerer, offererComponent)); + } + + private void OnMove(EntityUid uid, OfferItemComponent component, MoveEvent args) + { + if (_net.IsClient) // Client often mispredicts movement, we cant trust it here + return; + + if (component.ReceivingFrom == null) + return; + + if (_transform.InRange(args.NewPosition, Transform(component.ReceivingFrom.Value).Coordinates, component.MaxOfferDistance)) + return; + + UnOffer(uid, component); + } + + private void OnCarryTransfer(Entity ent, ref ItemTransferredEvent args) + { + if (args.Handled + || args.PassedItem == args.RealItem // Means the entity is transferred NOT via carrying + || args.RealItem is not { Valid: true } carried + || ent.Comp.Carrier is not { Valid: true } oldCarrier) + return; + + _carrying.DropCarried(oldCarrier, ent); + args.Handled = _carrying.TryCarry(args.Target, carried); + } + + private void OnPulledTransfer(Entity ent, ref ItemTransferredEvent args) + { + if (args.Handled + || args.PassedItem == args.RealItem // Means the entity is transferred NOT via pulling + || args.RealItem is not { Valid: true } pulled) + return; + + _pulling.TryStopPull(pulled, ent); + args.Handled = _pulling.TryStartPull(args.Target, ent, null, ent.Comp); + } + + #endregion + + #region Offering / Recieving + /// + /// Attempts to create an offer. Expects offerer.Item to already be set to the offered item, offererComponent.InReceiveMode == true. + /// Will fail if offerer == receiver or if receiver already has a set TargetOrOfferer, and that person is not the current offerer + /// + private bool CreateOffer(Entity receiver, Entity offerer) + { + var offererComponent = offerer.Comp; + var receiverComponent = receiver.Comp; + if (offerer == receiver || receiverComponent.IsInReceiveMode || !offererComponent.IsInOfferMode) + return false; + + if (offererComponent.IsInReceiveMode && offererComponent.ReceivingFrom != receiver) + return false; + + receiverComponent.IsInReceiveMode = true; + receiverComponent.ReceivingFrom = offerer; + + Dirty(receiver, receiverComponent); + + offererComponent.ReceivingFrom = receiver; // TODO this is ee shitcode, may not be necessary? + offererComponent.IsInOfferMode = false; + + Dirty(offerer, offererComponent); + + if (offererComponent.Item == null) + return false; + + // Sender popup (client-side only) + _popup.PopupClient( + Loc.GetString("offer-item-try-give", + ("item", Identity.Entity(offererComponent.GetRealEntity(EntityManager), EntityManager)), + ("target", Identity.Entity(receiver, EntityManager))), + offerer, + offerer); + // Receiver popup (server side only, not predicted because recipient != local player) + _popup.PopupEntity( + Loc.GetString("offer-item-try-give-target", + ("user", Identity.Entity(receiverComponent.ReceivingFrom.Value, EntityManager)), + ("item", Identity.Entity(offererComponent.GetRealEntity(EntityManager), EntityManager))), + offerer, + receiver, + Popups.PopupType.Medium); + + return true; + } + + + + /// + /// Resets the of the user and the target + /// + protected void UnOffer(EntityUid thisEntity, OfferItemComponent offererComp) + { + if (!TryComp(thisEntity, out var hands) || _hands.GetActiveHand((thisEntity, hands)) is null) + return; + + if (offererComp.ReceivingFrom is { } otherEntity && TryComp(otherEntity, out var otherOfferer)) + { + // So this tries to figure out which of these entities do what... + // if A.OfferItemComponent.Item != null, then A is currently offering an item to A.OfferItemComponent.TargetOrOfferer + // If it is null, then it is ONLY being offered an item TO. + if (offererComp.Item != null && _net.IsServer) + { + _popup.PopupEntity( + Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(offererComp.GetRealEntity(EntityManager), EntityManager)), // Floof - resolve virtual items + ("target", Identity.Entity(otherEntity, EntityManager))), + thisEntity, + thisEntity); + _popup.PopupEntity( + Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(thisEntity, EntityManager)), + ("item", Identity.Entity(offererComp.GetRealEntity(EntityManager), EntityManager))), + thisEntity, + otherEntity); + } + + else if (otherOfferer.Item != null && _net.IsServer) + { + _popup.PopupEntity( + Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(otherOfferer.GetRealEntity(EntityManager), EntityManager)), // Floof - resolve virtual items + ("target", Identity.Entity(thisEntity, EntityManager))), + otherEntity, + otherEntity); + _popup.PopupEntity( + Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(otherEntity, EntityManager)), + ("item", Identity.Entity(otherOfferer.GetRealEntity(EntityManager), EntityManager))), + otherEntity, + thisEntity); + } + + otherOfferer.IsInOfferMode = false; + otherOfferer.IsInReceiveMode = false; + otherOfferer.Hand = null; + otherOfferer.ReceivingFrom = null; + otherOfferer.Item = null; + + Dirty(otherEntity, otherOfferer); + } + + offererComp.IsInOfferMode = false; + offererComp.IsInReceiveMode = false; + offererComp.Hand = null; + offererComp.ReceivingFrom = null; + offererComp.Item = null; + + Dirty(thisEntity, offererComp); + } + + + /// + /// Cancels the transfer of the item + /// + protected void UnReceive(EntityUid receiver, OfferItemComponent? receiverComp = null, OfferItemComponent? offererComp = null) + { + if (!Resolve(receiver, ref receiverComp) + || receiverComp.ReceivingFrom is not {} offerer + || !Resolve(offerer, ref offererComp)) + return; + + // Idk why this check is here + if (!TryComp(receiver, out var hands) || _hands.GetActiveHand((receiver, hands)) == null || receiverComp.ReceivingFrom == null) + return; + + // If offererComp.Item != null, then they are actively offering to TargetOrOfferer + // Normally this method is called right after a transfer is done, but this part can be called from SetInOfferMode when the player presses F again to cancel an ongoing offer + if (offererComp.Item != null) + { + _popup.PopupClient( + Loc.GetString("offer-item-no-give", + ("item", Identity.Entity(offererComp.GetRealEntity(EntityManager), EntityManager)), // Floof - resolve virtual items + ("target", Identity.Entity(receiver, EntityManager))), + offerer, + offerer); + _popup.PopupEntity( + Loc.GetString("offer-item-no-give-target", + ("user", Identity.Entity(receiverComp.ReceivingFrom.Value, EntityManager)), // Floof - resolve virtual items + ("item", Identity.Entity(offererComp.GetRealEntity(EntityManager), EntityManager))), + offerer, + receiver); + } + + if (!offererComp.IsInReceiveMode) + { + offererComp.ReceivingFrom = null; + receiverComp.ReceivingFrom = null; + } + + offererComp.Item = null; + offererComp.Hand = null; + receiverComp.IsInReceiveMode = false; + + Dirty(receiver, receiverComp); + } + + /// + /// Accepting the offer and receive item + /// + public void Receive(Entity receiver) + { + if (!_timing.IsFirstTimePredicted) + return; + + if (!Resolve(receiver, ref receiver.Comp)) + return; + + if (!TryComp(receiver.Comp.ReceivingFrom, out var offererComponent) || + offererComponent.Hand == null || + receiver.Comp.ReceivingFrom is not {} sender || + !TryComp(receiver, out var hands)) + return; + + if (offererComponent.Item != null) + { + // Floof - check if there's something else handling it first + var realItem = offererComponent.GetRealEntity(EntityManager); + if (!TryHandleExtendedTransfer(sender, receiver, offererComponent.Item.Value, realItem) + && !_hands.TryPickup(receiver, offererComponent.Item.Value, handsComp: hands)) + { + _popup.PopupEntity(Loc.GetString("offer-item-full-hand"), receiver, receiver); + return; + } + + _popup.PopupEntity( + Loc.GetString("offer-item-give", + ("item", Identity.Entity(realItem, EntityManager)), // FLoof - resolve virtual items + ("target", Identity.Entity(receiver, EntityManager))), + sender, + sender); + _popup.PopupEntity( + Loc.GetString("offer-item-give-other", + ("user", Identity.Entity(receiver.Comp.ReceivingFrom.Value, EntityManager)), + ("item", Identity.Entity(realItem, EntityManager)), // FLoof - resolve virtual items + ("target", Identity.Entity(receiver, EntityManager))), + sender, + Filter.PvsExcept(sender, entityManager: EntityManager), + true); + } + + offererComponent.Item = null; + UnReceive(receiver, receiver.Comp, offererComponent); + } + #endregion + /// + /// Returns true if = true + /// + protected bool IsInOfferMode(Entity ent) + { + return Resolve(ent, ref ent.Comp, false) && ent.Comp.IsInOfferMode; + } + + private bool TryHandleExtendedTransfer(EntityUid user, EntityUid target, EntityUid offeredItem, EntityUid realItem) + { + var ev = new ItemTransferredEvent + { + User = user, + Target = target, + PassedItem = offeredItem, + RealItem = realItem, + }; + RaiseLocalEvent(realItem, ref ev); + return ev.Handled; + } +} + +/// +/// Raised on the entity that was transferred via item offering. +/// +[ByRefEvent] +public sealed class ItemTransferredEvent : HandledEntityEventArgs +{ + public EntityUid User; + public EntityUid Target; + + /// + /// The actual item being passed around. Can be a virtual item. + /// + public EntityUid PassedItem; + /// + /// If is a virtual item, this field contains the real item that was transferred. + /// + public EntityUid? RealItem; +} \ No newline at end of file diff --git a/Resources/Locale/en-US/_Floof/interaction/offer-item-system.ftl b/Resources/Locale/en-US/_Floof/interaction/offer-item-system.ftl new file mode 100644 index 00000000000..5c8e2057a22 --- /dev/null +++ b/Resources/Locale/en-US/_Floof/interaction/offer-item-system.ftl @@ -0,0 +1,17 @@ +offer-item-empty-hand = You don't have anything in your hand to give! + +offer-item-full-hand = Your hand isn't free to receive the item. + +offer-item-try-give = You offer {THE($item)} to {$target}. +offer-item-try-give-target = {CAPITALIZE(THE($user))} offers you {THE($item)}. + +offer-item-give = You handed {THE($item)} to {$target}. +offer-item-give-other = {CAPITALIZE(THE($user))} handed {THE($item)} to {$target}. +offer-item-give-target = {CAPITALIZE(THE($user))} handed you {THE($item)}. + +offer-item-no-give = You stop offering {THE($item)} to {$target}. +offer-item-no-give-target = {CAPITALIZE(THE($user))} is no longer offering {THE($item)} to you. + + +alerts-offer-name = Accept Offer +alerts-offer-desc = Click this alert to accept the item offered to you. diff --git a/Resources/Prototypes/Alerts/alerts.yml b/Resources/Prototypes/Alerts/alerts.yml index 532d4b436bf..a0ee84b00d8 100644 --- a/Resources/Prototypes/Alerts/alerts.yml +++ b/Resources/Prototypes/Alerts/alerts.yml @@ -28,6 +28,7 @@ - alertType: Rooted - alertType: Pacified - alertType: Stealthy + - alertType: Offer # Floofstation - port from EE - type: entity id: AlertSpriteView diff --git a/Resources/Prototypes/Body/species_appearance.yml b/Resources/Prototypes/Body/species_appearance.yml index 10c8df10c81..e5812802a00 100644 --- a/Resources/Prototypes/Body/species_appearance.yml +++ b/Resources/Prototypes/Body/species_appearance.yml @@ -97,3 +97,4 @@ type: HumanoidMarkingModifierBoundUserInterface enum.StrippingUiKey.Key: type: StrippableBoundUserInterface + - type: OfferItem # Floofstation diff --git a/Resources/Prototypes/_Floof/alerts.yml b/Resources/Prototypes/_Floof/alerts.yml new file mode 100644 index 00000000000..2718ac0fa5b --- /dev/null +++ b/Resources/Prototypes/_Floof/alerts.yml @@ -0,0 +1,8 @@ +- type: alert + id: Offer + clickEvent: !type:AcceptOfferAlertEvent { } + icons: + - sprite: /Textures/_Floof/Interface/Alerts/offer_item.rsi + state: offer_item + name: alerts-offer-name + description: alerts-offer-desc diff --git a/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/meta.json b/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/meta.json new file mode 100644 index 00000000000..9b5f9361314 --- /dev/null +++ b/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "Original icon taken from https://github.com/ss220-space/Paradise/blob/master220/icons/mob/screen_alert.dmi, modified by Mocho", + "size": { + "x": 32, + "y": 32 + }, + "states": [ + { + "name": "offer_item" + } + ] +} diff --git a/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/offer_item.png b/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/offer_item.png new file mode 100644 index 00000000000..44ac380ddb1 Binary files /dev/null and b/Resources/Textures/_Floof/Interface/Alerts/offer_item.rsi/offer_item.png differ diff --git a/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/give_item.png b/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/give_item.png new file mode 100644 index 00000000000..26bc88fb229 Binary files /dev/null and b/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/give_item.png differ diff --git a/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/meta.json b/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/meta.json new file mode 100644 index 00000000000..94cd6fa2e1e --- /dev/null +++ b/Resources/Textures/_Floof/Interface/Misc/give_item.rsi/meta.json @@ -0,0 +1,14 @@ +{ + "version": 1, + "license": "CC-BY-SA-3.0", + "copyright": "https://github.com/ss220-space/Paradise/blob/master220/icons/misc/mouse_icons/give_item.dmi", + "size": { + "x": 64, + "y": 64 + }, + "states": [ + { + "name": "give_item" + } + ] +} diff --git a/Resources/keybinds.yml b/Resources/keybinds.yml index 522b008c767..43361ec39f8 100644 --- a/Resources/keybinds.yml +++ b/Resources/keybinds.yml @@ -544,6 +544,11 @@ binds: type: State key: MouseMiddle mod1: Shift +# Floofstation section +- function: OfferItem + type: State + key: F +# Floofstation section end - function: ArcadeUp type: State key: Up