Skip to content

Commit

Permalink
port better borgs from frontier (#3110)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
Co-authored-by: Dvir <[email protected]>
Co-authored-by: deltanedas <@deltanedas:kde.org>
  • Loading branch information
3 people authored Mar 4, 2025
1 parent 3238476 commit 0f3edc0
Show file tree
Hide file tree
Showing 26 changed files with 507 additions and 18 deletions.
15 changes: 15 additions & 0 deletions Content.Client/UserInterface/Systems/Hands/HandsUIController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
13 changes: 13 additions & 0 deletions Content.Client/_NF/Hands/UI/HandPlaceholderStatus.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
3 changes: 3 additions & 0 deletions Content.Client/_NF/Hands/UI/HandPlaceholderStatus.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<Control xmlns="https://spacestation14.io">
<Label StyleClasses="ItemStatus" Text="{Loc 'hand-placeholder-name'}" />
</Control>
Original file line number Diff line number Diff line change
@@ -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;
}

Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Handles interactions with items that spawn HandPlaceholder items.
/// </summary>
[UsedImplicitly]
public sealed partial class HandPlaceholderVisualsSystem : EntitySystem
{
[Dependency] ContainerSystem _container = default!;
[Dependency] ItemSystem _item = default!;
public override void Initialize()
{
base.Initialize();

SubscribeLocalEvent<HandPlaceholderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);

SubscribeLocalEvent<HandPlaceholderVisualsComponent, ComponentRemove>(PlaceholderRemove);

Subs.ItemStatus<HandPlaceholderVisualsComponent>(_ => new HandPlaceholderStatus());
}

private void OnAfterAutoHandleState(Entity<HandPlaceholderComponent> 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<HandPlaceholderVisualsComponent> ent, ref ComponentRemove args)
{
if (ent.Comp.Dummy != EntityUid.Invalid)
QueueDel(ent.Comp.Dummy);
}
}
3 changes: 2 additions & 1 deletion Content.IntegrationTests/Tests/Sprite/ItemSpriteTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
110 changes: 109 additions & 1 deletion Content.Server/Silicons/Borgs/BorgSystem.Modules.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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<HandPlaceholderRemoveableComponent>(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<HandPlaceholderComponent>(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<HandPlaceholderComponent>(item))
{
var placeComp = EnsureComp<HandPlaceholderRemoveableComponent>(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<HandPlaceholderComponent>(item))
{
// Placeholders can't be put down, must be changed after picked up (otherwise it'll fail to pick up)
EnsureComp<UnremoveableComponent>(item);
}
component.DroppableProvidedItems.Add(handId, (item, itemProto));
}
// End Frontier: droppable cyborg items

component.ItemsCreated = true;
}

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

Expand All @@ -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<UnremoveableComponent>(hand.HeldEntity.Value);
_container.Insert(hand.HeldEntity.Value, component.ProvidedContainer);
}

_hands.RemoveHand(chassis, handId, hands);
}
component.DroppableProvidedItems.Clear();
// End Frontier
}

/// <summary>
Expand All @@ -283,13 +364,16 @@ public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponen

if (TryComp<ItemBorgModuleComponent>(module, out var itemModuleComp))
{
var droppableComparer = new DroppableBorgItemComparer(); // Frontier: cached comparer
foreach (var containedModuleUid in component.ModuleContainer.ContainedEntities)
{
if (!TryComp<ItemBorgModuleComponent>(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);
Expand All @@ -301,6 +385,30 @@ public bool CanInsertModule(EntityUid uid, EntityUid module, BorgChassisComponen
return true;
}

// Frontier: droppable borg item comparator
private sealed class DroppableBorgItemComparer : IEqualityComparer<DroppableBorgItem>
{
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

/// <summary>
/// Check if a module can be removed from a borg.
/// </summary>
Expand Down
1 change: 1 addition & 0 deletions Content.Server/Silicons/Borgs/BorgSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;


Expand Down
7 changes: 7 additions & 0 deletions Content.Server/_NF/Whitelist/Components/NFBookBagComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;

/// <summary>
/// Whitelist component for book bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFBookBagComponent : Component;
7 changes: 7 additions & 0 deletions Content.Server/_NF/Whitelist/Components/NFLighterComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;

/// <summary>
/// Whitelist component for lighters to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFLighterComponent : Component;
7 changes: 7 additions & 0 deletions Content.Server/_NF/Whitelist/Components/NFOreBagComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;

/// <summary>
/// Whitelist component for ore bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFOreBagComponent : Component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;

/// <summary>
/// Whitelist component for plant bags to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFPlantBagComponent : Component;
7 changes: 7 additions & 0 deletions Content.Server/_NF/Whitelist/Components/NFShakerComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Content.Server._NF.Whitelist.Components;

/// <summary>
/// Whitelist component for shakers to avoid tag redefinition and collisions
/// </summary>
[RegisterComponent]
public sealed partial class NFShakerComponent : Component;
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using Content.Shared.Whitelist;
using Robust.Shared.GameStates;

namespace Content.Shared.Interaction.Components
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -14,15 +15,27 @@ public sealed partial class ItemBorgModuleComponent : Component
/// <summary>
/// The items that are provided.
/// </summary>
[DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>), required: true)]
[DataField("items", customTypeSerializer: typeof(PrototypeIdListSerializer<EntityPrototype>))] // Frontier: removed
public List<string> Items = new();

/// <summary>
/// Frontier: The droppable items that are provided.
/// </summary>
[DataField]
public List<DroppableBorgItem> DroppableItems = new();

/// <summary>
/// The entities from <see cref="Items"/> that were spawned.
/// </summary>
[DataField("providedItems")]
public SortedDictionary<string, EntityUid> ProvidedItems = new();

/// <summary>
/// The entities from <see cref="Items"/> that were spawned.
/// </summary>
[DataField("droppableProvidedItems")]
public SortedDictionary<string, (EntityUid, DroppableBorgItem)> DroppableProvidedItems = new();

/// <summary>
/// A counter that ensures a unique
/// </summary>
Expand All @@ -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;
}
Loading

0 comments on commit 0f3edc0

Please sign in to comment.