From 0f3edc0b3948c21e8c4dd76b450e8f3b2b73c996 Mon Sep 17 00:00:00 2001
From: deltanedas <39013340+deltanedas@users.noreply.github.com>
Date: Tue, 4 Mar 2025 12:10:32 +0000
Subject: [PATCH] port better borgs from frontier (#3110)
* BetterBorgs: droppable, swappable cyborg item interactions (#2766)
* WIP: droppable, swappable, insertable cyborg items
* Half-baked borg HandPlaceholderComponent
* cyborg: sprite representation for empty slots
* nullable prototype
---------
Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>
* BorgSystem: check droppable items for duped mods (#2887)
* BorgSystem: check droppable items for duped mods
* Cache item comparer
* BorgSystem: Unremoveable after equip (#2854)
* raise interaction events to add fibers to things
---------
Co-authored-by: Whatstone <166147148+whatston3@users.noreply.github.com>
Co-authored-by: Dvir <39403717+dvir001@users.noreply.github.com>
Co-authored-by: deltanedas <@deltanedas:kde.org>
---
.../Systems/Hands/HandsUIController.cs | 15 +++
.../_NF/Hands/UI/HandPlaceholderStatus.cs | 13 ++
.../_NF/Hands/UI/HandPlaceholderStatus.xaml | 3 +
.../HandPlaceholderVisualsComponent.cs | 10 ++
.../Systems/HandPlaceholderVisualsSystem.cs | 47 +++++++
.../Tests/Sprite/ItemSpriteTest.cs | 3 +-
.../Silicons/Borgs/BorgSystem.Modules.cs | 110 +++++++++++++++-
Content.Server/Silicons/Borgs/BorgSystem.cs | 1 +
.../Components/NFBookBagComponent.cs | 7 +
.../Components/NFLighterComponent.cs | 7 +
.../Whitelist/Components/NFOreBagComponent.cs | 7 +
.../Components/NFPlantBagComponent.cs | 7 +
.../Whitelist/Components/NFShakerComponent.cs | 7 +
.../Components/UnremoveableComponent.cs | 1 -
.../Components/ItemBorgModuleComponent.cs | 27 +++-
.../Components/HandPlaceholderComponent.cs | 22 ++++
.../HandPlaceholderRemoveableComponent.cs | 17 +++
.../Systems/SharedHandPlaceholderSystem.cs | 120 ++++++++++++++++++
.../Locale/en-US/_NF/hands/hands-system.ftl | 1 +
.../Consumable/Drinks/drinks_special.yml | 1 +
.../Objects/Specific/Hydroponics/tools.yml | 1 +
.../Objects/Specific/Librarian/books_bag.yml | 1 +
.../Specific/Robotics/borg_modules.yml | 81 ++++++++++--
.../Objects/Specific/Salvage/ore_bag.yml | 5 +-
.../Entities/Objects/Tools/lighters.yml | 1 +
.../Robotics/borg_hand_placeholder.yml | 10 ++
26 files changed, 507 insertions(+), 18 deletions(-)
create mode 100644 Content.Client/_NF/Hands/UI/HandPlaceholderStatus.cs
create mode 100644 Content.Client/_NF/Hands/UI/HandPlaceholderStatus.xaml
create mode 100644 Content.Client/_NF/Interaction/Components/HandPlaceholderVisualsComponent.cs
create mode 100644 Content.Client/_NF/Interaction/Systems/HandPlaceholderVisualsSystem.cs
create mode 100644 Content.Server/_NF/Whitelist/Components/NFBookBagComponent.cs
create mode 100644 Content.Server/_NF/Whitelist/Components/NFLighterComponent.cs
create mode 100644 Content.Server/_NF/Whitelist/Components/NFOreBagComponent.cs
create mode 100644 Content.Server/_NF/Whitelist/Components/NFPlantBagComponent.cs
create mode 100644 Content.Server/_NF/Whitelist/Components/NFShakerComponent.cs
create mode 100644 Content.Shared/_NF/Interaction/Components/HandPlaceholderComponent.cs
create mode 100644 Content.Shared/_NF/Interaction/Components/HandPlaceholderRemoveableComponent.cs
create mode 100644 Content.Shared/_NF/Interaction/Systems/SharedHandPlaceholderSystem.cs
create mode 100644 Resources/Locale/en-US/_NF/hands/hands-system.ftl
create mode 100644 Resources/Prototypes/_NF/Entities/Objects/Specific/Robotics/borg_hand_placeholder.yml
diff --git a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
index edc02600611..67a30c61f82 100644
--- a/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
+++ b/Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
@@ -13,6 +13,7 @@
using Robust.Shared.Input;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
+using Content.Shared._NF.Interaction.Components;
namespace Content.Client.UserInterface.Systems.Hands;
@@ -138,6 +139,13 @@ private void LoadPlayerHands(HandsComponent handsComp)
handButton.SetEntity(virt.BlockingEntity);
handButton.Blocked = true;
}
+ // Frontier - borg hand placeholder
+ else if (_entities.TryGetComponent(hand.HeldEntity, out HandPlaceholderVisualsComponent? placeholder))
+ {
+ handButton.SetEntity(placeholder.Dummy);
+ handButton.Blocked = true;
+ }
+ // End Frontier - borg hand placeholder
else
{
handButton.SetEntity(hand.HeldEntity);
@@ -189,6 +197,13 @@ private void OnItemAdded(string name, EntityUid entity)
hand.SetEntity(virt.BlockingEntity);
hand.Blocked = true;
}
+ // Frontier: borg hand placeholders
+ else if (_entities.TryGetComponent(entity, out HandPlaceholderVisualsComponent? placeholder))
+ {
+ hand.SetEntity(placeholder.Dummy);
+ hand.Blocked = true;
+ }
+ // End Frontier: borg hand placeholders
else
{
hand.SetEntity(entity);
diff --git a/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.cs b/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.cs
new file mode 100644
index 00000000000..745ffee5966
--- /dev/null
+++ b/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.cs
@@ -0,0 +1,13 @@
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client._NF.Hands.UI
+{
+ public sealed class HandPlaceholderStatus : Control
+ {
+ public HandPlaceholderStatus()
+ {
+ RobustXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.xaml b/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.xaml
new file mode 100644
index 00000000000..2eaf1145d12
--- /dev/null
+++ b/Content.Client/_NF/Hands/UI/HandPlaceholderStatus.xaml
@@ -0,0 +1,3 @@
+
+
+
diff --git a/Content.Client/_NF/Interaction/Components/HandPlaceholderVisualsComponent.cs b/Content.Client/_NF/Interaction/Components/HandPlaceholderVisualsComponent.cs
new file mode 100644
index 00000000000..72c5ae093d3
--- /dev/null
+++ b/Content.Client/_NF/Interaction/Components/HandPlaceholderVisualsComponent.cs
@@ -0,0 +1,10 @@
+namespace Content.Shared._NF.Interaction.Components;
+
+[RegisterComponent]
+// Client-side component of the HandPlaceholder. Creates and tracks a client-side entity for hand blocking visuals
+public sealed partial class HandPlaceholderVisualsComponent : Component
+{
+ [DataField]
+ public EntityUid Dummy;
+}
+
diff --git a/Content.Client/_NF/Interaction/Systems/HandPlaceholderVisualsSystem.cs b/Content.Client/_NF/Interaction/Systems/HandPlaceholderVisualsSystem.cs
new file mode 100644
index 00000000000..5d59b78aea0
--- /dev/null
+++ b/Content.Client/_NF/Interaction/Systems/HandPlaceholderVisualsSystem.cs
@@ -0,0 +1,47 @@
+using Content.Client._NF.Hands.UI;
+using Content.Client.Items;
+using Content.Client.Items.Systems;
+using Content.Shared._NF.Interaction.Components;
+using JetBrains.Annotations;
+using Robust.Client.GameObjects;
+
+namespace Content.Client._NF.Interaction.Systems;
+
+///
+/// Handles interactions with items that spawn HandPlaceholder items.
+///
+[UsedImplicitly]
+public sealed partial class HandPlaceholderVisualsSystem : EntitySystem
+{
+ [Dependency] ContainerSystem _container = default!;
+ [Dependency] ItemSystem _item = default!;
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ SubscribeLocalEvent(OnAfterAutoHandleState);
+
+ SubscribeLocalEvent(PlaceholderRemove);
+
+ Subs.ItemStatus(_ => new HandPlaceholderStatus());
+ }
+
+ private void OnAfterAutoHandleState(Entity ent, ref AfterAutoHandleStateEvent args)
+ {
+ if (!TryComp(ent, out HandPlaceholderVisualsComponent? placeholder))
+ return;
+
+ if (placeholder.Dummy != EntityUid.Invalid)
+ QueueDel(placeholder.Dummy);
+ placeholder.Dummy = Spawn(ent.Comp.Prototype);
+
+ if (_container.IsEntityInContainer(ent))
+ _item.VisualsChanged(ent);
+ }
+
+ private void PlaceholderRemove(Entity ent, ref ComponentRemove args)
+ {
+ if (ent.Comp.Dummy != EntityUid.Invalid)
+ QueueDel(ent.Comp.Dummy);
+ }
+}
diff --git a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
index da7e1e8e9b0..623044f617a 100644
--- a/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
+++ b/Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
@@ -28,7 +28,8 @@ public sealed class PrototypeSaveTest
{
// The only prototypes that should get ignored are those that REQUIRE setup to get a sprite. At that point it is
// the responsibility of the spawner to ensure that a valid sprite is set.
- "VirtualItem"
+ "VirtualItem",
+ "HandPlaceholder" // Frontier
};
[Test]
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
index f95a5807360..f6155bf8902 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
@@ -4,6 +4,7 @@
using Content.Shared.Silicons.Borgs.Components;
using Content.Shared.Whitelist;
using Robust.Shared.Containers;
+using Content.Shared._NF.Interaction.Components; // Frontier
namespace Content.Server.Silicons.Borgs;
@@ -197,6 +198,7 @@ private void ProvideItems(EntityUid chassis, EntityUid uid, BorgChassisComponent
if (!component.ItemsCreated)
{
item = Spawn(itemProto, xform.Coordinates);
+ _interaction.DoContactInteraction(chassis, item); // DeltaV - give items fibers before they might be dropped
}
else
{
@@ -225,6 +227,63 @@ private void ProvideItems(EntityUid chassis, EntityUid uid, BorgChassisComponent
component.ProvidedItems.Add(handId, item);
}
+ // Frontier: droppable cyborg items
+ foreach (var itemProto in component.DroppableItems)
+ {
+ EntityUid item;
+
+ if (!component.ItemsCreated)
+ {
+ item = Spawn(itemProto.ID, xform.Coordinates);
+ var placeComp = EnsureComp(item);
+ placeComp.Whitelist = itemProto.Whitelist;
+ placeComp.Prototype = itemProto.ID;
+ Dirty(item, placeComp);
+ }
+ else
+ {
+ item = component.ProvidedContainer.ContainedEntities
+ .FirstOrDefault(ent => _whitelistSystem.IsWhitelistPassOrNull(itemProto.Whitelist, ent) || TryComp(ent, out var placeholder));
+ if (!item.IsValid())
+ {
+ Log.Debug($"no items found: {component.ProvidedContainer.ContainedEntities.Count}");
+ continue;
+ }
+
+ // Just in case, make sure the borg can't drop the placeholder.
+ if (!HasComp(item))
+ {
+ var placeComp = EnsureComp(item);
+ placeComp.Whitelist = itemProto.Whitelist;
+ placeComp.Prototype = itemProto.ID;
+ Dirty(item, placeComp);
+ }
+ }
+
+ if (!item.IsValid())
+ {
+ Log.Debug("no valid item");
+ continue;
+ }
+
+ var handId = $"{uid}-item{component.HandCounter}";
+ component.HandCounter++;
+ _hands.AddHand(chassis, handId, HandLocation.Middle, hands);
+ _hands.DoPickup(chassis, hands.Hands[handId], item, hands);
+ if (hands.Hands[handId].HeldEntity != item)
+ {
+ // If we didn't pick up our expected item, delete the hand. No free hands!
+ _hands.RemoveHand(chassis, handId);
+ }
+ else if (HasComp(item))
+ {
+ // Placeholders can't be put down, must be changed after picked up (otherwise it'll fail to pick up)
+ EnsureComp(item);
+ }
+ component.DroppableProvidedItems.Add(handId, (item, itemProto));
+ }
+ // End Frontier: droppable cyborg items
+
component.ItemsCreated = true;
}
@@ -244,6 +303,14 @@ private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisCo
_hands.RemoveHand(chassis, hand, hands);
}
component.ProvidedItems.Clear();
+ // Frontier: droppable items
+ foreach (var (hand, item) in component.DroppableProvidedItems)
+ {
+ QueueDel(item.Item1);
+ _hands.RemoveHand(chassis, hand, hands);
+ }
+ component.DroppableProvidedItems.Clear();
+ // End Frontier: droppable items
return;
}
@@ -257,6 +324,20 @@ private void RemoveProvidedItems(EntityUid chassis, EntityUid uid, BorgChassisCo
_hands.RemoveHand(chassis, handId, hands);
}
component.ProvidedItems.Clear();
+ // Frontier: remove all items from borg hands directly, not from the provided items set
+ foreach (var (handId, _) in component.DroppableProvidedItems)
+ {
+ _hands.TryGetHand(chassis, handId, out var hand, hands);
+ if (hand?.HeldEntity != null)
+ {
+ RemComp(hand.HeldEntity.Value);
+ _container.Insert(hand.HeldEntity.Value, component.ProvidedContainer);
+ }
+
+ _hands.RemoveHand(chassis, handId, hands);
+ }
+ component.DroppableProvidedItems.Clear();
+ // End Frontier
}
///
@@ -283,13 +364,16 @@ public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponen
if (TryComp(module, out var itemModuleComp))
{
+ var droppableComparer = new DroppableBorgItemComparer(); // Frontier: cached comparer
foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities)
{
if (!TryComp(containedModuleUid, out var containedItemModuleComp))
continue;
if (containedItemModuleComp.Items.Count == itemModuleComp.Items.Count &&
- containedItemModuleComp.Items.All(itemModuleComp.Items.Contains))
+ containedItemModuleComp.DroppableItems.Count == itemModuleComp.DroppableItems.Count && // Frontier
+ containedItemModuleComp.Items.All(itemModuleComp.Items.Contains) &&
+ containedItemModuleComp.DroppableItems.All(x => itemModuleComp.DroppableItems.Contains(x, droppableComparer))) // Frontier
{
if (user != null)
Popup.PopupEntity(Loc.GetString("borg-module-duplicate"), uid, user.Value);
@@ -301,6 +385,30 @@ public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponen
return true;
}
+ // Frontier: droppable borg item comparator
+ private sealed class DroppableBorgItemComparer : IEqualityComparer
+ {
+ public bool Equals(DroppableBorgItem? x, DroppableBorgItem? y)
+ {
+ // Same object (or both null)
+ if (ReferenceEquals(x, y))
+ return true;
+ // One-side null
+ if (x == null || y == null)
+ return false;
+ // Otherwise, use EntProtoId of item
+ return x.ID == y.ID;
+ }
+
+ public int GetHashCode(DroppableBorgItem obj)
+ {
+ if (obj is null)
+ return 0;
+ return obj.ID.GetHashCode();
+ }
+ }
+ // End Frontier
+
///
/// Check if a module can be removed from a borg.
///
diff --git a/Content.Server/Silicons/Borgs/BorgSystem.cs b/Content.Server/Silicons/Borgs/BorgSystem.cs
index d8fa1f300be..4f69eaa6243 100644
--- a/Content.Server/Silicons/Borgs/BorgSystem.cs
+++ b/Content.Server/Silicons/Borgs/BorgSystem.cs
@@ -53,6 +53,7 @@ public sealed partial class BorgSystem : SharedBorgSystem
[Dependency] private readonly ThrowingSystem _throwing = default!;
[Dependency] private readonly UserInterfaceSystem _ui = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV
[Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
diff --git a/Content.Server/_NF/Whitelist/Components/NFBookBagComponent.cs b/Content.Server/_NF/Whitelist/Components/NFBookBagComponent.cs
new file mode 100644
index 00000000000..934fc2a971d
--- /dev/null
+++ b/Content.Server/_NF/Whitelist/Components/NFBookBagComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._NF.Whitelist.Components;
+
+///
+/// Whitelist component for book bags to avoid tag redefinition and collisions
+///
+[RegisterComponent]
+public sealed partial class NFBookBagComponent : Component;
diff --git a/Content.Server/_NF/Whitelist/Components/NFLighterComponent.cs b/Content.Server/_NF/Whitelist/Components/NFLighterComponent.cs
new file mode 100644
index 00000000000..acc04149379
--- /dev/null
+++ b/Content.Server/_NF/Whitelist/Components/NFLighterComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._NF.Whitelist.Components;
+
+///
+/// Whitelist component for lighters to avoid tag redefinition and collisions
+///
+[RegisterComponent]
+public sealed partial class NFLighterComponent : Component;
diff --git a/Content.Server/_NF/Whitelist/Components/NFOreBagComponent.cs b/Content.Server/_NF/Whitelist/Components/NFOreBagComponent.cs
new file mode 100644
index 00000000000..2072de58eb4
--- /dev/null
+++ b/Content.Server/_NF/Whitelist/Components/NFOreBagComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._NF.Whitelist.Components;
+
+///
+/// Whitelist component for ore bags to avoid tag redefinition and collisions
+///
+[RegisterComponent]
+public sealed partial class NFOreBagComponent : Component;
diff --git a/Content.Server/_NF/Whitelist/Components/NFPlantBagComponent.cs b/Content.Server/_NF/Whitelist/Components/NFPlantBagComponent.cs
new file mode 100644
index 00000000000..f3ec9a69352
--- /dev/null
+++ b/Content.Server/_NF/Whitelist/Components/NFPlantBagComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._NF.Whitelist.Components;
+
+///
+/// Whitelist component for plant bags to avoid tag redefinition and collisions
+///
+[RegisterComponent]
+public sealed partial class NFPlantBagComponent : Component;
diff --git a/Content.Server/_NF/Whitelist/Components/NFShakerComponent.cs b/Content.Server/_NF/Whitelist/Components/NFShakerComponent.cs
new file mode 100644
index 00000000000..78831efabce
--- /dev/null
+++ b/Content.Server/_NF/Whitelist/Components/NFShakerComponent.cs
@@ -0,0 +1,7 @@
+namespace Content.Server._NF.Whitelist.Components;
+
+///
+/// Whitelist component for shakers to avoid tag redefinition and collisions
+///
+[RegisterComponent]
+public sealed partial class NFShakerComponent : Component;
diff --git a/Content.Shared/Interaction/Components/UnremoveableComponent.cs b/Content.Shared/Interaction/Components/UnremoveableComponent.cs
index cd106948658..1b2328d00f9 100644
--- a/Content.Shared/Interaction/Components/UnremoveableComponent.cs
+++ b/Content.Shared/Interaction/Components/UnremoveableComponent.cs
@@ -1,4 +1,3 @@
-using Content.Shared.Whitelist;
using Robust.Shared.GameStates;
namespace Content.Shared.Interaction.Components
diff --git a/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs b/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs
index 75835d0cf01..ead55d97272 100644
--- a/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs
+++ b/Content.Shared/Silicons/Borgs/Components/ItemBorgModuleComponent.cs
@@ -1,4 +1,5 @@
-using Robust.Shared.Containers;
+using Content.Shared.Whitelist;
+using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.List;
@@ -14,15 +15,27 @@ public sealed partial class ItemBorgModuleComponent : Component
///
/// The items that are provided.
///
- [DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer), required: true)]
+ [DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer))] // Frontier: removed
public List Items = new();
+ ///
+ /// Frontier: The droppable items that are provided.
+ ///
+ [DataField]
+ public List DroppableItems = new();
+
///
/// The entities from that were spawned.
///
[DataField("providedItems")]
public SortedDictionary ProvidedItems = new();
+ ///
+ /// The entities from that were spawned.
+ ///
+ [DataField("droppableProvidedItems")]
+ public SortedDictionary DroppableProvidedItems = new();
+
///
/// A counter that ensures a unique
///
@@ -49,3 +62,13 @@ public sealed partial class ItemBorgModuleComponent : Component
public string ProvidedContainerId = "provided_container";
}
+// Frontier: droppable borg item data definitions
+[DataDefinition]
+public sealed partial class DroppableBorgItem
+{
+ [IdDataField]
+ public EntProtoId ID;
+
+ [DataField]
+ public EntityWhitelist Whitelist;
+}
diff --git a/Content.Shared/_NF/Interaction/Components/HandPlaceholderComponent.cs b/Content.Shared/_NF/Interaction/Components/HandPlaceholderComponent.cs
new file mode 100644
index 00000000000..51611889e45
--- /dev/null
+++ b/Content.Shared/_NF/Interaction/Components/HandPlaceholderComponent.cs
@@ -0,0 +1,22 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._NF.Interaction.Components;
+
+[RegisterComponent]
+[NetworkedComponent]
+[AutoGenerateComponentState(true)]
+// When an entity with this is removed from a hand, it is replaced with a placeholder entity that blocks the hand's use until re-equipped with the same prototype.
+public sealed partial class HandPlaceholderComponent : Component
+{
+ ///
+ /// A whitelist to match entities that this should accept.
+ ///
+ [ViewVariables, AutoNetworkedField]
+ public EntityWhitelist? Whitelist;
+
+ [ViewVariables, AutoNetworkedField]
+ public EntProtoId? Prototype;
+}
+
diff --git a/Content.Shared/_NF/Interaction/Components/HandPlaceholderRemoveableComponent.cs b/Content.Shared/_NF/Interaction/Components/HandPlaceholderRemoveableComponent.cs
new file mode 100644
index 00000000000..91062f35958
--- /dev/null
+++ b/Content.Shared/_NF/Interaction/Components/HandPlaceholderRemoveableComponent.cs
@@ -0,0 +1,17 @@
+using Content.Shared.Whitelist;
+using Robust.Shared.GameStates;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._NF.Interaction.Components;
+
+[RegisterComponent]
+[NetworkedComponent]
+// When an entity with this is removed from a hand, it is replaced with a placeholder entity that blocks the hand's use until re-equipped with the same prototype.
+public sealed partial class HandPlaceholderRemoveableComponent : Component
+{
+ [DataField]
+ public EntityWhitelist? Whitelist;
+
+ [DataField]
+ public EntProtoId? Prototype;
+}
diff --git a/Content.Shared/_NF/Interaction/Systems/SharedHandPlaceholderSystem.cs b/Content.Shared/_NF/Interaction/Systems/SharedHandPlaceholderSystem.cs
new file mode 100644
index 00000000000..db259ffc649
--- /dev/null
+++ b/Content.Shared/_NF/Interaction/Systems/SharedHandPlaceholderSystem.cs
@@ -0,0 +1,120 @@
+using Content.Shared._NF.Interaction.Components;
+using Content.Shared.Hands;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Item;
+using Content.Shared.Whitelist;
+using JetBrains.Annotations;
+using Robust.Shared.Containers;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared._NF.Interaction.Systems;
+
+///
+/// Handles interactions with items that spawn HandPlaceholder items.
+///
+[UsedImplicitly]
+public sealed partial class HandPlaceholderSystem : EntitySystem
+{
+ [Dependency] private readonly INetManager _net = default!;
+ [Dependency] private readonly SharedHandsSystem _hands = default!;
+ [Dependency] private readonly SharedContainerSystem _container = default!;
+ [Dependency] private readonly SharedInteractionSystem _interaction = default!; // DeltaV
+ [Dependency] private readonly SharedItemSystem _item = default!;
+ [Dependency] private readonly EntityWhitelistSystem _whitelist = default!;
+ [Dependency] private readonly MetaDataSystem _metadata = default!;
+ [Dependency] private readonly IPrototypeManager _proto = default!;
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent(OnUnequipHand);
+ SubscribeLocalEvent(OnDropped);
+
+ SubscribeLocalEvent(AfterInteract);
+ SubscribeLocalEvent(BeforeRangedInteract);
+ }
+
+ private void OnUnequipHand(Entity ent, ref GotUnequippedHandEvent args)
+ {
+ if (args.Handled)
+ return; // If this is happening in practice, this is a bug.
+
+ SpawnAndPickUpPlaceholder(ent, args.User);
+ RemCompDeferred(ent);
+ args.Handled = true;
+ }
+
+ private void OnDropped(Entity ent, ref DroppedEvent args)
+ {
+ if (args.Handled)
+ return; // If this is happening in practice, this is a bug.
+
+ SpawnAndPickUpPlaceholder(ent, args.User);
+ RemCompDeferred(ent);
+ args.Handled = true;
+ }
+
+ private void SpawnAndPickUpPlaceholder(Entity ent, EntityUid user)
+ {
+ if (_net.IsServer)
+ {
+ var placeholder = Spawn("HandPlaceholder");
+ if (TryComp(placeholder, out var placeComp))
+ {
+ placeComp.Whitelist = ent.Comp.Whitelist;
+ placeComp.Prototype = ent.Comp.Prototype;
+ Dirty(placeholder, placeComp);
+ }
+
+ if (_proto.TryIndex(ent.Comp.Prototype, out var itemProto))
+ _metadata.SetEntityName(placeholder, itemProto.Name);
+
+ if (!_hands.TryPickup(user, placeholder)) // Can we get the hand this came from?
+ QueueDel(placeholder);
+ }
+ }
+
+ private void BeforeRangedInteract(Entity ent, ref BeforeRangedInteractEvent args)
+ {
+ if (args.Target == null || args.Handled)
+ return;
+
+ args.Handled = true;
+ TryToPickUpTarget(ent, args.Target.Value, args.User);
+ }
+
+ private void AfterInteract(Entity ent, ref AfterInteractEvent args)
+ {
+ if (args.Target == null || args.Handled)
+ return;
+
+ args.Handled = true;
+ TryToPickUpTarget(ent, args.Target.Value, args.User);
+ }
+
+ private void TryToPickUpTarget(Entity ent, EntityUid target, EntityUid user)
+ {
+ if (_whitelist.IsWhitelistFail(ent.Comp.Whitelist, target))
+ return;
+
+ // Can't get the hand we're holding this with? Something's wrong, abort. No empty hands.
+ if (!_hands.IsHolding(user, ent, out var hand))
+ return;
+
+ // Cache the whitelist/prototype, entity might be deleted.
+ var whitelist = ent.Comp.Whitelist;
+ var prototype = ent.Comp.Prototype;
+
+ if (_net.IsServer)
+ Del(ent);
+
+ _hands.DoPickup(user, hand, target); // Force pickup - empty hands are not okay
+ var placeComp = EnsureComp(target);
+ placeComp.Whitelist = whitelist;
+ placeComp.Prototype = prototype;
+ Dirty(target, placeComp);
+ _interaction.DoContactInteraction(user, target); // DeltaV - borgs picking up items leaves fibers
+ }
+}
diff --git a/Resources/Locale/en-US/_NF/hands/hands-system.ftl b/Resources/Locale/en-US/_NF/hands/hands-system.ftl
new file mode 100644
index 00000000000..36b66a2ee52
--- /dev/null
+++ b/Resources/Locale/en-US/_NF/hands/hands-system.ftl
@@ -0,0 +1 @@
+hand-placeholder-name = Module slot for
\ No newline at end of file
diff --git a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml
index 930cf817576..34a36433531 100644
--- a/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml
+++ b/Resources/Prototypes/Entities/Objects/Consumable/Drinks/drinks_special.yml
@@ -39,6 +39,7 @@
mixOnInteract: false
reactionTypes:
- Shake
+ - type: NFShaker # Frontier
- type: entity
parent: DrinkGlassBase
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml
index fe7e0400af6..1e453e3d5a9 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Hydroponics/tools.yml
@@ -157,3 +157,4 @@
difficulty: 2
recipes:
- PlantBagOfHolding
+ - type: NFPlantBag # Frontier
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml
index 6eb56f3245b..9f28762a02e 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Librarian/books_bag.yml
@@ -28,3 +28,4 @@
- TabletopBoard
- Write
- type: Dumpable
+ - type: NFBookBag # Frontier
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
index 69580bd453c..a0a9f1e7c07 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Robotics/borg_modules.yml
@@ -134,8 +134,13 @@
- state: generic
- state: icon-fire-extinguisher
- type: ItemBorgModule
- items:
- - FireExtinguisher
+ # Frontier: droppable borg items
+ droppableItems:
+ - id: FireExtinguisher
+ whitelist:
+ tags:
+ - FireExtinguisher
+ # End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: extinguisher-module }
@@ -221,9 +226,16 @@
items:
- MiningDrill
- MineralScannerUnpowered
- - OreBag
+ # - OreBag # Frontier
- Crowbar
- RadioHandheld
+ # Frontier: droppable borg items
+ droppableItems:
+ - id: OreBag
+ whitelist:
+ components:
+ - NFOreBag
+ # End Frontier: droppable borg items
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: mining-module }
@@ -345,8 +357,15 @@
- type: ItemBorgModule
items:
- MopItem
- - Bucket
+ # - Bucket # Frontier
- TrashBag
+ # Frontier: droppable items
+ droppableItems:
+ - id: Bucket
+ whitelist:
+ tags:
+ - Bucket
+ # End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: cleaning-module }
@@ -433,10 +452,21 @@
- type: ItemBorgModule
items:
- HandheldHealthAnalyzerUnpowered
- - Beaker
- - Beaker
+ # - Beaker # Frontier
+ # - Beaker # Frontier
- BorgDropper
- BorgHypo
+ # Frontier: droppable borg items
+ droppableItems:
+ - id: Beaker
+ whitelist:
+ tags:
+ - GlassBeaker
+ - id: Beaker
+ whitelist:
+ tags:
+ - GlassBeaker
+ # End Frontier: droppable borg items
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: adv-diagnosis-module }
@@ -489,11 +519,26 @@
- type: ItemBorgModule
items:
- Pen
- - BooksBag
+ # - BooksBag # Frontier
- HandLabeler
- - Lighter
- - DrinkShaker
+ # - Lighter # Frontier
+ # - DrinkShaker # Frontier
- BorgDropper
+ # Frontier: droppable
+ droppableItems:
+ - id: BooksBag
+ whitelist:
+ components:
+ - NFBookBag
+ - id: Lighter
+ whitelist:
+ components:
+ - NFLighter
+ - id: DrinkShaker
+ whitelist:
+ components:
+ - NFShaker
+ # End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: service-module }
@@ -528,7 +573,14 @@
- HydroponicsToolMiniHoe
- HydroponicsToolSpade
- HydroponicsToolClippers
- - Bucket
+ # - Bucket # Frontier
+ # Frontier: droppable borg items
+ droppableItems:
+ - id: Bucket
+ whitelist:
+ tags:
+ - Bucket
+ # End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: gardening-module }
@@ -545,7 +597,14 @@
items:
- HydroponicsToolScythe
- HydroponicsToolHatchet
- - PlantBag
+ # - PlantBag # Frontier
+ # Frontier: droppable borg items
+ droppableItems:
+ - id: PlantBag
+ whitelist:
+ components:
+ - NFPlantBag
+ # End Frontier
- type: BorgModuleIcon
icon: { sprite: Interface/Actions/actions_borg.rsi, state: harvesting-module }
diff --git a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml
index 9b0a4361f9f..51792823217 100644
--- a/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml
+++ b/Resources/Prototypes/Entities/Objects/Specific/Salvage/ore_bag.yml
@@ -46,8 +46,9 @@
enum.ToggleVisuals.Layer:
True: { state: icon_on }
False: { state: icon }
- # End DeltaV Additions
- - type: ReverseEngineering # DeltaV
+ - type: ReverseEngineering
difficulty: 2
recipes:
- OreBagOfHolding
+ # End DeltaV Additions
+ - type: NFOreBag # Frontier
diff --git a/Resources/Prototypes/Entities/Objects/Tools/lighters.yml b/Resources/Prototypes/Entities/Objects/Tools/lighters.yml
index 94f501260b8..35187e20fc8 100644
--- a/Resources/Prototypes/Entities/Objects/Tools/lighters.yml
+++ b/Resources/Prototypes/Entities/Objects/Tools/lighters.yml
@@ -95,6 +95,7 @@
collection: lighterOnSounds
endSound:
collection: lighterOffSounds
+ - type: NFLighter # Frontier
- type: entity
name: cheap lighter
diff --git a/Resources/Prototypes/_NF/Entities/Objects/Specific/Robotics/borg_hand_placeholder.yml b/Resources/Prototypes/_NF/Entities/Objects/Specific/Robotics/borg_hand_placeholder.yml
new file mode 100644
index 00000000000..1c84c2b08e5
--- /dev/null
+++ b/Resources/Prototypes/_NF/Entities/Objects/Specific/Robotics/borg_hand_placeholder.yml
@@ -0,0 +1,10 @@
+- type: entity
+ id: HandPlaceholder
+ name: unknown tool
+ categories: [ HideSpawnMenu ]
+ components:
+ - type: Item
+ size: Ginormous # no storage insertion visuals
+ - type: Unremoveable
+ - type: HandPlaceholder
+ - type: HandPlaceholderVisuals