diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index 6fad7de4a6..8dbc915a17 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -62,6 +62,9 @@
+
+
+
@@ -80,6 +83,10 @@
{31d24303-f6a9-4d53-bb03-a73edcb3186d}
sfml-system
+
+ {d17de83d-a592-461f-8af2-53f9e22e1d0f}
+ sfml-window
+
{302b877e-0000-0000-0000-000000000000}
SS14.Client.Graphics
@@ -103,4 +110,4 @@
-
+
\ No newline at end of file
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index 23c31b9e9b..cd7fc67701 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -1,4 +1,8 @@
-using SS14.Shared.ContentPack;
+using Content.Client.GameObjects;
+using Content.Client.Interfaces.GameObjects;
+using SS14.Shared.ContentPack;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
namespace Content.Client
{
@@ -6,7 +10,13 @@ namespace Content.Client
{
public override void Init()
{
- // TODO: Anything at all.
+ var factory = IoCManager.Resolve();
+
+ factory.RegisterIgnore("Inventory");
+ factory.RegisterIgnore("Item");
+
+ factory.Register();
+ factory.RegisterReference();
}
}
}
diff --git a/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs b/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs
new file mode 100644
index 0000000000..3688198afe
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Items/ClientHandsComponent.cs
@@ -0,0 +1,55 @@
+using Content.Client.Interfaces.GameObjects;
+using Content.Client.UserInterface;
+using Content.Shared.GameObjects;
+using Lidgren.Network;
+using SS14.Client.Interfaces.UserInterface;
+using SS14.Client.UserInterface;
+using SS14.Shared;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
+using System.Collections.Generic;
+
+namespace Content.Client.GameObjects
+{
+ public class HandsComponent : SharedHandsComponent, IHandsComponent
+ {
+ private readonly Dictionary hands = new Dictionary();
+ public string ActiveIndex { get; private set; }
+
+ public IEntity GetEntity(string index)
+ {
+ if (hands.TryGetValue(index, out var entity))
+ {
+ return entity;
+ }
+
+ return null;
+ }
+
+ public override void HandleComponentState(ComponentState state)
+ {
+ var cast = (HandsComponentState)state;
+ hands.Clear();
+ foreach (var hand in cast.Hands)
+ {
+ hands[hand.Key] = Owner.EntityManager.GetEntity(hand.Value);
+ }
+
+ ActiveIndex = cast.ActiveIndex;
+
+ var uiMgr = (UserInterfaceManager)IoCManager.Resolve();
+
+ if (uiMgr.GetSingleComponentByGuiComponentType(GuiComponentType.HandsUi) == null)
+ {
+ uiMgr.AddComponent(new HandsGui());
+ }
+ uiMgr.ComponentUpdate(GuiComponentType.HandsUi, this);
+ }
+
+ public void SendChangeHand(string index)
+ {
+ Owner.SendComponentNetworkMessage(this, NetDeliveryMethod.ReliableUnordered, index);
+ }
+ }
+}
diff --git a/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs b/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
new file mode 100644
index 0000000000..91002b3cc9
--- /dev/null
+++ b/Content.Client/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
@@ -0,0 +1,14 @@
+using SS14.Shared.Interfaces.GameObjects;
+
+namespace Content.Client.Interfaces.GameObjects
+{
+ // HYPER SIMPLE HANDS API CLIENT SIDE.
+ // To allow for showing the HUD, mostly.
+ public interface IHandsComponent
+ {
+ IEntity GetEntity(string index);
+ string ActiveIndex { get; }
+
+ void SendChangeHand(string index);
+ }
+}
diff --git a/Content.Client/UserInterface/HandsGui.cs b/Content.Client/UserInterface/HandsGui.cs
new file mode 100644
index 0000000000..92eb8be029
--- /dev/null
+++ b/Content.Client/UserInterface/HandsGui.cs
@@ -0,0 +1,192 @@
+using Content.Client.Interfaces.GameObjects;
+using OpenTK.Graphics;
+using SFML.Graphics;
+using SFML.Window;
+using SS14.Client.GameObjects;
+using SS14.Client.Graphics;
+using SS14.Client.Graphics.Utility;
+using SS14.Client.Interfaces.Player;
+using SS14.Client.Interfaces.Resource;
+using SS14.Client.Interfaces.UserInterface;
+using SS14.Client.UserInterface.Components;
+using SS14.Shared;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
+using SS14.Shared.Maths;
+
+namespace Content.Client.UserInterface
+{
+ public class HandsGui : GuiComponent
+ {
+ private readonly Color4 _inactiveColor = new Color4(90, 90, 90, 255);
+
+ private readonly IPlayerManager _playerManager = IoCManager.Resolve();
+ private readonly IUserInterfaceManager _userInterfaceManager = IoCManager.Resolve();
+ private readonly Sprite handSlot;
+ private readonly int spacing = 1;
+
+ private UiHandInfo LeftHand;
+ private UiHandInfo RightHand;
+ private Box2i handL;
+ private Box2i handR;
+
+ public HandsGui()
+ {
+ var _resMgr = IoCManager.Resolve();
+ ComponentClass = GuiComponentType.HandsUi;
+ handSlot = _resMgr.GetSprite("hand");
+ ZDepth = 5;
+ }
+
+ public override void ComponentUpdate(params object[] args)
+ {
+ base.ComponentUpdate(args);
+ UpdateHandIcons();
+ }
+
+ public override void Update(float frameTime)
+ {
+ var slotBounds = handSlot.GetLocalBounds();
+ var width = (int)((slotBounds.Width * 2) + spacing);
+ var height = (int)slotBounds.Height;
+ Position = new Vector2i((int)(CluwneLib.Window.Viewport.Width - width) / 2, (int)CluwneLib.Window.Viewport.Height - height - 10);
+ handL = Box2i.FromDimensions(Position.X, Position.Y, (int)slotBounds.Width, (int)slotBounds.Height);
+ handR = Box2i.FromDimensions(Position.X + (int)slotBounds.Width + spacing, Position.Y, (int)slotBounds.Width, (int)slotBounds.Height);
+ ClientArea = Box2i.FromDimensions(Position.X, Position.Y, width, (int)slotBounds.Height);
+ }
+
+ public override void Render()
+ {
+ if (_playerManager?.ControlledEntity == null)
+ {
+ return;
+ }
+
+ IEntity entity = _playerManager.ControlledEntity;
+ if (!entity.TryGetComponent(out var hands))
+ {
+ return;
+ }
+
+ var leftActive = hands.ActiveIndex == "left";
+
+ handSlot.Color = Color.White;
+ handSlot.SetTransformToRect(leftActive ? handL : handR);
+ handSlot.Draw();
+
+ handSlot.Color = _inactiveColor.Convert();
+ handSlot.SetTransformToRect(leftActive ? handR : handL);
+ handSlot.Draw();
+
+ if (LeftHand.Entity != null && LeftHand.HeldSprite != null)
+ {
+ var bounds = LeftHand.HeldSprite.GetLocalBounds();
+ LeftHand.HeldSprite.SetTransformToRect(
+ Box2i.FromDimensions(handL.Left + (int)(handL.Width / 2f - bounds.Width / 2f),
+ handL.Top + (int)(handL.Height / 2f - bounds.Height / 2f),
+ (int)bounds.Width, (int)bounds.Height));
+ LeftHand.HeldSprite.Draw();
+ }
+
+ if (RightHand.Entity != null && RightHand.HeldSprite != null)
+ {
+ var bounds = RightHand.HeldSprite.GetLocalBounds();
+ RightHand.HeldSprite.SetTransformToRect(
+ Box2i.FromDimensions(handR.Left + (int)(handR.Width / 2f - bounds.Width / 2f),
+ handR.Top + (int)(handR.Height / 2f - bounds.Height / 2f),
+ (int)bounds.Width, (int)bounds.Height));
+ RightHand.HeldSprite.Draw();
+ }
+ }
+
+ public void UpdateHandIcons()
+ {
+ if (_playerManager?.ControlledEntity == null)
+ {
+ return;
+ }
+
+ IEntity entity = _playerManager.ControlledEntity;
+ if (!entity.TryGetComponent(out var hands))
+ {
+ return;
+ }
+
+ var left = hands.GetEntity("left");
+ var right = hands.GetEntity("right");
+
+ if (left != null)
+ {
+ if (left != LeftHand.Entity)
+ {
+ LeftHand.Entity = left;
+ LeftHand.HeldSprite = GetIconSprite(left);
+ }
+ }
+ else
+ {
+ LeftHand.Entity = null;
+ LeftHand.HeldSprite = null;
+ }
+
+ if (right != null)
+ {
+ if (right != RightHand.Entity)
+ {
+ RightHand.Entity = right;
+ RightHand.HeldSprite = GetIconSprite(right);
+ }
+ }
+ else
+ {
+ RightHand.Entity = null;
+ RightHand.HeldSprite = null;
+ }
+ }
+
+ private void SendSwitchHandTo(string index)
+ {
+ IEntity entity = _playerManager.ControlledEntity;
+ if (!entity.TryGetComponent(out var hands))
+ {
+ return;
+ }
+ hands.SendChangeHand(index);
+ }
+
+ public override bool MouseDown(MouseButtonEventArgs e)
+ {
+ if (e.Button != Mouse.Button.Right)
+ {
+ return false;
+ }
+ if (handL.Contains(e.X, e.Y))
+ {
+ SendSwitchHandTo("left");
+ return true;
+ }
+ if (handR.Contains(e.X, e.Y))
+ {
+ SendSwitchHandTo("right");
+ return true;
+ }
+ return false;
+ }
+
+ private static Sprite GetIconSprite(IEntity entity)
+ {
+ Sprite icon = null;
+ if (entity.TryGetComponent(out var component))
+ {
+ icon = component.Icon;
+ }
+ return icon ?? IoCManager.Resolve().DefaultSprite();
+ }
+
+ private struct UiHandInfo
+ {
+ public IEntity Entity { get; set; }
+ public Sprite HeldSprite { get; set; }
+ }
+ }
+}
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 07b85bcc33..da2735d7f1 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -56,6 +56,12 @@
+
+
+
+
+
+
@@ -85,4 +91,4 @@
-
+
\ No newline at end of file
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 6483e6b6f4..c8c527c982 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -1,4 +1,8 @@
-using SS14.Shared.ContentPack;
+using Content.Server.GameObjects;
+using Content.Server.Interfaces.GameObjects;
+using SS14.Shared.ContentPack;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Shared.IoC;
namespace Content.Server
{
@@ -6,7 +10,16 @@ namespace Content.Server
{
public override void Init()
{
- // TODO: Anything at all.
+ var factory = IoCManager.Resolve();
+
+ factory.Register();
+ factory.RegisterReference();
+
+ factory.Register();
+ factory.RegisterReference();
+
+ factory.Register();
+ factory.RegisterReference();
}
}
}
diff --git a/Content.Server/GameObjects/Components/Items/InventoryComponent.cs b/Content.Server/GameObjects/Components/Items/InventoryComponent.cs
new file mode 100644
index 0000000000..585235d948
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Items/InventoryComponent.cs
@@ -0,0 +1,167 @@
+using Content.Server.Interfaces.GameObjects;
+using SS14.Server.GameObjects;
+using SS14.Server.GameObjects.Components.Container;
+using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared.Utility;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using System;
+using System.Collections.Generic;
+using YamlDotNet.RepresentationModel;
+
+namespace Content.Server.GameObjects
+{
+ public class InventoryComponent : Component, IInventoryComponent
+ {
+ public override string Name => "Inventory";
+
+ private Dictionary slots = new Dictionary();
+ private TransformComponent transform;
+ // TODO: Make this container unique per-slot.
+ private IContainer container;
+
+ public override void Initialize()
+ {
+ transform = Owner.GetComponent();
+ container = Container.Create("inventory", Owner);
+ base.Initialize();
+ }
+
+ public override void OnRemove()
+ {
+ foreach (var slot in slots.Keys)
+ {
+ RemoveSlot(slot);
+ }
+ transform = null;
+ container = null;
+ base.OnRemove();
+ }
+
+ public override void LoadParameters(YamlMappingNode mapping)
+ {
+ if (mapping.TryGetNode("slots", out var slotsNode))
+ {
+ foreach (var node in slotsNode)
+ {
+ AddSlot(node.AsString());
+ }
+ }
+ base.LoadParameters(mapping);
+ }
+
+ public IItemComponent Get(string slot)
+ {
+ return _GetSlot(slot).Item;
+ }
+
+ public IInventorySlot GetSlot(string slot)
+ {
+ return slots[slot];
+ }
+
+ // Private version that returns our concrete implementation.
+ private InventorySlot _GetSlot(string slot)
+ {
+ return slots[slot];
+ }
+
+ public bool Insert(string slot, IItemComponent item)
+ {
+ if (item == null)
+ {
+ throw new ArgumentNullException(nameof(item), "An item must be passed. To remove an item from a slot, use Drop()");
+ }
+
+ var inventorySlot = _GetSlot(slot);
+ if (!CanInsert(slot, item) || !container.Insert(item.Owner))
+ {
+ return false;
+ }
+
+ inventorySlot.Item = item;
+ item.EquippedToSlot(inventorySlot);
+ return true;
+ }
+
+ public bool CanInsert(string slot, IItemComponent item)
+ {
+ var inventorySlot = _GetSlot(slot);
+ return inventorySlot.Item == null && container.CanInsert(item.Owner);
+ }
+
+ public bool Drop(string slot)
+ {
+ if (!CanDrop(slot))
+ {
+ return false;
+ }
+
+ var inventorySlot = _GetSlot(slot);
+ var item = inventorySlot.Item;
+ if (!container.Remove(item.Owner))
+ {
+ return false;
+ }
+
+ item.RemovedFromSlot();
+ inventorySlot.Item = null;
+
+ // TODO: The item should be dropped to the container our owner is in, if any.
+ var itemTransform = item.Owner.GetComponent();
+ itemTransform.LocalPosition = transform.LocalPosition;
+ return true;
+ }
+
+ public bool CanDrop(string slot)
+ {
+ var inventorySlot = _GetSlot(slot);
+ var item = inventorySlot.Item;
+ return item != null && container.CanRemove(item.Owner);
+ }
+
+ public IInventorySlot AddSlot(string slot)
+ {
+ if (HasSlot(slot))
+ {
+ throw new InvalidOperationException($"Slot '{slot}' already exists.");
+ }
+
+ return slots[slot] = new InventorySlot(slot, this);
+ }
+
+ public void RemoveSlot(string slot)
+ {
+ if (!HasSlot(slot))
+ {
+ throw new InvalidOperationException($"Slow '{slot}' does not exist.");
+ }
+
+ if (Get(slot) != null && !Drop(slot))
+ {
+ // TODO: Handle this potential failiure better.
+ throw new InvalidOperationException("Unable to remove slot as the contained item could not be dropped");
+ }
+
+ slots.Remove(slot);
+ }
+
+ public bool HasSlot(string slot)
+ {
+ return slots.ContainsKey(slot);
+ }
+
+ private class InventorySlot : IInventorySlot
+ {
+ public IItemComponent Item { get; set; }
+ public string Name { get; }
+ public IInventoryComponent Owner { get; }
+
+ public InventorySlot(string name, IInventoryComponent owner)
+ {
+ Name = name;
+ Owner = owner;
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Items/ItemComponent.cs b/Content.Server/GameObjects/Components/Items/ItemComponent.cs
new file mode 100644
index 0000000000..113fc2c6ef
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Items/ItemComponent.cs
@@ -0,0 +1,46 @@
+using Content.Server.Interfaces.GameObjects;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using SS14.Server.Interfaces.GameObjects;
+using System;
+
+namespace Content.Server.GameObjects
+{
+ public class ItemComponent : Component, IItemComponent
+ {
+ public override string Name => "Item";
+
+ ///
+ public IInventorySlot ContainingSlot { get; private set; }
+
+ public void RemovedFromSlot()
+ {
+ if (ContainingSlot == null)
+ {
+ throw new InvalidOperationException("Item is not in a slot.");
+ }
+
+ ContainingSlot = null;
+
+ foreach (var component in Owner.GetComponents())
+ {
+ component.Visible = true;
+ }
+ }
+
+ public void EquippedToSlot(IInventorySlot slot)
+ {
+ if (ContainingSlot != null)
+ {
+ throw new InvalidOperationException("Item is already in a slot.");
+ }
+
+ ContainingSlot = slot;
+
+ foreach (var component in Owner.GetComponents())
+ {
+ component.Visible = false;
+ }
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Items/ServerHandsComponent.cs b/Content.Server/GameObjects/Components/Items/ServerHandsComponent.cs
new file mode 100644
index 0000000000..e5ea4c9572
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Items/ServerHandsComponent.cs
@@ -0,0 +1,293 @@
+using Content.Server.Interfaces.GameObjects;
+using Content.Shared.GameObjects;
+using SS14.Server.GameObjects.Events;
+using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared;
+using SS14.Shared.GameObjects;
+using SS14.Shared.Utility;
+using System;
+using System.Collections.Generic;
+using YamlDotNet.RepresentationModel;
+using Lidgren.Network;
+
+namespace Content.Server.GameObjects
+{
+ public class HandsComponent : SharedHandsComponent, IHandsComponent
+ {
+ private string activeIndex;
+ public string ActiveIndex
+ {
+ get => activeIndex;
+ set
+ {
+ if (!hands.ContainsKey(value))
+ {
+ throw new ArgumentException($"No hand '{value}'");
+ }
+
+ activeIndex = value;
+ }
+ }
+
+ private Dictionary hands = new Dictionary();
+ private List orderedHands = new List();
+ private IInventoryComponent inventory;
+ private IServerTransformComponent transform;
+ private YamlMappingNode tempParametersMapping;
+
+ // Mostly arbitrary.
+ public const float PICKUP_RANGE = 2;
+
+ public override void Initialize()
+ {
+ inventory = Owner.GetComponent();
+ transform = Owner.GetComponent();
+ if (tempParametersMapping != null)
+ {
+ foreach (var node in tempParametersMapping.GetNode("hands"))
+ {
+ AddHand(node.AsString());
+ }
+ }
+
+ Owner.SubscribeEvent(OnKeyChange, this);
+ Owner.SubscribeEvent(OnClick, this);
+ base.Initialize();
+ }
+
+ public override void OnRemove()
+ {
+ inventory = null;
+ Owner.UnsubscribeEvent(this);
+ Owner.UnsubscribeEvent(this);
+ base.OnRemove();
+ }
+
+ public override void LoadParameters(YamlMappingNode mapping)
+ {
+ tempParametersMapping = mapping;
+ base.LoadParameters(mapping);
+ }
+
+ public IEnumerable GetAllHeldItems()
+ {
+ foreach (var slot in hands.Values)
+ {
+ if (slot.Item != null)
+ {
+ yield return slot.Item;
+ }
+ }
+ }
+
+ public IItemComponent GetHand(string index)
+ {
+ var slot = hands[index];
+ return slot.Item;
+ }
+
+ ///
+ /// Enumerates over the hand keys, returning the active hand first.
+ ///
+ private IEnumerable ActivePriorityEnumerable()
+ {
+ yield return ActiveIndex;
+ foreach (var hand in hands.Keys)
+ {
+ if (hand == ActiveIndex)
+ {
+ continue;
+ }
+
+ yield return hand;
+ }
+ }
+
+ public bool PutInHand(IItemComponent item)
+ {
+ foreach (var hand in ActivePriorityEnumerable())
+ {
+ if (PutInHand(item, hand, fallback: false))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool PutInHand(IItemComponent item, string index, bool fallback = true)
+ {
+ if (!CanPutInHand(item, index))
+ {
+ return fallback && PutInHand(item);
+ }
+
+ var slot = hands[index];
+ return slot.Owner.Insert(slot.Name, item);
+ }
+
+ public bool CanPutInHand(IItemComponent item)
+ {
+ foreach (var hand in ActivePriorityEnumerable())
+ {
+ if (CanPutInHand(item, hand))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ public bool CanPutInHand(IItemComponent item, string index)
+ {
+ var slot = hands[index];
+ return slot.Owner.CanInsert(slot.Name, item);
+ }
+
+ public bool Drop(string index)
+ {
+ if (!CanDrop(index))
+ {
+ return false;
+ }
+
+ var slot = hands[index];
+ return slot.Owner.Drop(slot.Name);
+ }
+
+ public bool CanDrop(string index)
+ {
+ var slot = hands[index];
+ return slot.Item != null && slot.Owner.CanDrop(slot.Name);
+ }
+
+ public void AddHand(string index)
+ {
+ if (HasHand(index))
+ {
+ throw new InvalidOperationException($"Hand '{index}' already exists.");
+ }
+
+ var slot = inventory.AddSlot(HandSlotName(index));
+ hands[index] = slot;
+ orderedHands.Add(index);
+ if (ActiveIndex == null)
+ {
+ ActiveIndex = index;
+ }
+ }
+
+ public void RemoveHand(string index)
+ {
+ if (!HasHand(index))
+ {
+ throw new InvalidOperationException($"Hand '{index}' does not exist.");
+ }
+
+ inventory.RemoveSlot(HandSlotName(index));
+ hands.Remove(index);
+ orderedHands.Remove(index);
+
+ if (index == ActiveIndex)
+ {
+ if (orderedHands.Count == 0)
+ {
+ activeIndex = null;
+ }
+ else
+ {
+ activeIndex = orderedHands[0];
+ }
+ }
+ }
+
+ public bool HasHand(string index)
+ {
+ return hands.ContainsKey(index);
+ }
+
+ ///
+ /// Get the name of the slot passed to the inventory component.
+ ///
+ private string HandSlotName(string index) => $"_hand_{index}";
+
+ public override ComponentState GetComponentState()
+ {
+ var dict = new Dictionary(hands.Count);
+ foreach (var hand in hands)
+ {
+ if (hand.Value.Item != null)
+ {
+ dict[hand.Key] = hand.Value.Item.Owner.Uid;
+ }
+ }
+ return new HandsComponentState(dict, ActiveIndex);
+ }
+
+ // Game logic goes here.
+ public void OnKeyChange(object sender, EntityEventArgs uncast)
+ {
+ var cast = (BoundKeyChangeEventArgs)uncast;
+ if (cast.Actor != Owner || cast.KeyState != BoundKeyState.Down)
+ {
+ return;
+ }
+
+ switch (cast.KeyFunction)
+ {
+ case BoundKeyFunctions.SwitchHands:
+ SwapHands();
+ break;
+ case BoundKeyFunctions.Drop:
+ Drop(ActiveIndex);
+ break;
+ }
+ }
+
+ private void SwapHands()
+ {
+ var index = orderedHands.FindIndex(x => x == ActiveIndex);
+ index++;
+ if (index >= orderedHands.Count)
+ {
+ index = 0;
+ }
+
+ ActiveIndex = orderedHands[index];
+ }
+
+ public void OnClick(object sender, EntityEventArgs uncast)
+ {
+ var cast = (ClickedOnEntityEventArgs)uncast;
+ if (cast.MouseButton != MouseClickType.Left || Owner.EntityManager.GetEntity(cast.Clicker) != Owner)
+ {
+ return;
+ }
+
+ var target = Owner.EntityManager.GetEntity(cast.Clicked);
+ var targetTransform = target.GetComponent();
+ if (!target.TryGetComponent(out var item) || (targetTransform.WorldPosition - transform.WorldPosition).Length > PICKUP_RANGE)
+ {
+ return;
+ }
+
+ PutInHand(item, ActiveIndex, fallback: false);
+ }
+
+ public override void HandleNetworkMessage(IncomingEntityComponentMessage message, NetConnection sender)
+ {
+ if (message.MessageParameters.Count != 1)
+ {
+ return;
+ }
+ var index = message.MessageParameters[0];
+ if (index is string newIndex && HasHand(newIndex))
+ {
+ ActiveIndex = newIndex;
+ }
+ base.HandleNetworkMessage(message, sender);
+ }
+ }
+}
diff --git a/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
new file mode 100644
index 0000000000..0d8fc902d4
--- /dev/null
+++ b/Content.Server/Interfaces/GameObjects/Components/Items/IHandsComponent.cs
@@ -0,0 +1,99 @@
+using SS14.Shared.Interfaces.GameObjects;
+using System.Collections.Generic;
+
+namespace Content.Server.Interfaces.GameObjects
+{
+ public interface IHandsComponent : IComponent
+ {
+ ///
+ /// The hand index of the currently active hand.
+ ///
+ string ActiveIndex { get; set; }
+
+ ///
+ /// Enumerates over every held item.
+ ///
+ IEnumerable GetAllHeldItems();
+
+ ///
+ /// Gets the item held by a hand.
+ ///
+ /// The index of the hand to get.
+ /// The item in the held, null if no item is held
+ IItemComponent GetHand(string index);
+
+ ///
+ /// Puts an item into any empty hand, preferring the active hand.
+ ///
+ /// The item to put in a hand.
+ /// True if the item was inserted, false otherwise.
+ bool PutInHand(IItemComponent item);
+
+ ///
+ /// Puts an item into a specific hand.
+ ///
+ /// The item to put in the hand.
+ /// The index of the hand to put the item into.
+ ///
+ /// If true and the provided hand is full, the method will fall back to
+ ///
+ /// True if the item was inserted into a hand, false otherwise.
+ bool PutInHand(IItemComponent item, string index, bool fallback=true);
+
+ ///
+ /// Checks to see if an item can be put in any hand.
+ ///
+ /// The item to check for.
+ /// True if the item can be inserted, false otherwise.
+ bool CanPutInHand(IItemComponent item);
+
+ ///
+ /// Checks to see if an item can be put in the specified hand.
+ ///
+ /// The item to check for.
+ /// The index for the hand to check for.
+ /// True if the item can be inserted, false otherwise.
+ bool CanPutInHand(IItemComponent item, string index);
+
+ ///
+ /// Drops an item on the ground, removing it from the hand.
+ ///
+ /// The hand to drop from.
+ /// True if an item was successfully dropped, false otherwise.
+ bool Drop(string index);
+
+ ///
+ /// Checks whether the item in the specified hand can be dropped.
+ ///
+ /// The hand to check for.
+ ///
+ /// True if the item can be dropped, false if the hand is empty or the item in the hand cannot be dropped.
+ ///
+ bool CanDrop(string index);
+
+ ///
+ /// Adds a new hand to this hands component.
+ ///
+ /// The name of the hand to add.
+ ///
+ /// Thrown if a hand with specified name already exists.
+ ///
+ void AddHand(string index);
+
+ ///
+ /// Removes a hand from this hands component.
+ ///
+ ///
+ /// If the hand contains an item, the item is dropped.
+ ///
+ /// The name of the hand to remove.
+ void RemoveHand(string index);
+
+ ///
+ /// Checks whether a hand with the specified name exists.
+ ///
+ /// The hand name to check.
+ /// True if the hand exists, false otherwise.
+ bool HasHand(string index);
+ }
+}
diff --git a/Content.Server/Interfaces/GameObjects/Components/Items/IInventoryComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Items/IInventoryComponent.cs
new file mode 100644
index 0000000000..6fe70fc1b6
--- /dev/null
+++ b/Content.Server/Interfaces/GameObjects/Components/Items/IInventoryComponent.cs
@@ -0,0 +1,101 @@
+using SS14.Server.Interfaces.GameObjects;
+using SS14.Shared.Interfaces.GameObjects;
+using System;
+
+namespace Content.Server.Interfaces.GameObjects
+{
+ public interface IInventoryComponent : IComponent
+ {
+ ///
+ /// Gets the item in the specified slot.
+ ///
+ /// The slot to get the item for.
+ /// Null if the slot is empty, otherwise the item.
+ IItemComponent Get(string slot);
+
+ ///
+ /// Gets the slot with specified name.
+ /// This gets the slot, NOT the item contained therein.
+ ///
+ /// The name of the slot to get.
+ IInventorySlot GetSlot(string slot);
+
+ ///
+ /// Puts an item in a slot.
+ ///
+ ///
+ /// This will fail if there is already an item in the specified slot.
+ ///
+ /// The slot to put the item in.
+ /// The item to insert into the slot.
+ /// True if the item was successfully inserted, false otherwise.
+ bool Insert(string slot, IItemComponent item);
+
+ ///
+ /// Checks whether an item can be put in the specified slot.
+ ///
+ /// The slot to check for.
+ /// The item to check for.
+ /// True if the item can be inserted into the specified slot.
+ bool CanInsert(string slot, IItemComponent item);
+
+ ///
+ /// Drops the item in a slot.
+ ///
+ /// The slot to drop the item from.
+ /// True if an item was dropped, false otherwise.
+ bool Drop(string slot);
+
+ ///
+ /// Checks whether an item can be dropped from the specified slot.
+ ///
+ /// The slot to check for.
+ ///
+ /// True if there is an item in the slot and it can be dropped, false otherwise.
+ ///
+ bool CanDrop(string slot);
+
+ ///
+ /// Adds a new slot to this inventory component.
+ ///
+ /// The name of the slot to add.
+ ///
+ /// Thrown if the slot with specified name already exists.
+ ///
+ IInventorySlot AddSlot(string slot);
+
+ ///
+ /// Removes a slot from this inventory component.
+ ///
+ ///
+ /// If the slot contains an item, the item is dropped.
+ ///
+ /// The name of the slot to remove.
+ void RemoveSlot(string slot);
+
+ ///
+ /// Checks whether a slot with the specified name exists.
+ ///
+ /// The slot name to check.
+ /// True if the slot exists, false otherwise.
+ bool HasSlot(string slot);
+ }
+
+ public interface IInventorySlot
+ {
+ ///
+ /// The name of the slot.
+ ///
+ string Name { get; }
+
+ ///
+ /// The item contained in the slot, can be null.
+ ///
+ IItemComponent Item { get; }
+
+ ///
+ /// The component owning us.
+ ///
+ IInventoryComponent Owner { get; }
+ }
+}
diff --git a/Content.Server/Interfaces/GameObjects/Components/Items/IItemComponent.cs b/Content.Server/Interfaces/GameObjects/Components/Items/IItemComponent.cs
new file mode 100644
index 0000000000..cd54ef375c
--- /dev/null
+++ b/Content.Server/Interfaces/GameObjects/Components/Items/IItemComponent.cs
@@ -0,0 +1,22 @@
+using SS14.Shared.Interfaces.GameObjects;
+
+namespace Content.Server.Interfaces.GameObjects
+{
+ public interface IItemComponent : IComponent
+ {
+ ///
+ /// The inventory slot this item is stored in, if any.
+ ///
+ IInventorySlot ContainingSlot { get; }
+
+ ///
+ /// Called when the item is removed from its inventory slot.
+ ///
+ void RemovedFromSlot();
+
+ ///
+ /// Called when the item is inserted into a new inventory slot.
+ ///
+ void EquippedToSlot(IInventorySlot slot);
+ }
+}
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index af0cea6ca3..c200d1c88f 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -56,6 +56,8 @@
+
+
@@ -95,4 +97,4 @@
-
+
\ No newline at end of file
diff --git a/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs b/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs
new file mode 100644
index 0000000000..a6bd76d5d4
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/Items/SharedHandsComponent.cs
@@ -0,0 +1,27 @@
+using SS14.Shared.GameObjects;
+using System;
+using System.Collections.Generic;
+
+namespace Content.Shared.GameObjects
+{
+ public abstract class SharedHandsComponent : Component
+ {
+ public sealed override string Name => "Hands";
+ public sealed override uint? NetID => ContentNetIDs.HANDS;
+ public sealed override Type StateType => typeof(HandsComponentState);
+ }
+
+ // The IDs of the items get synced over the network.
+ [Serializable]
+ public class HandsComponentState : ComponentState
+ {
+ public readonly Dictionary Hands;
+ public readonly string ActiveIndex;
+
+ public HandsComponentState(Dictionary hands, string activeIndex) : base(ContentNetIDs.HANDS)
+ {
+ Hands = hands;
+ ActiveIndex = activeIndex;
+ }
+ }
+}
diff --git a/Content.Shared/GameObjects/Components/NetIDs.cs b/Content.Shared/GameObjects/Components/NetIDs.cs
new file mode 100644
index 0000000000..fe77227233
--- /dev/null
+++ b/Content.Shared/GameObjects/Components/NetIDs.cs
@@ -0,0 +1,7 @@
+namespace Content.Shared.GameObjects
+{
+ public static class ContentNetIDs
+ {
+ public const uint HANDS = 1000;
+ }
+}
diff --git a/Resources/Prototypes/Entities/Items.yml b/Resources/Prototypes/Entities/Items.yml
new file mode 100644
index 0000000000..28667c3eba
--- /dev/null
+++ b/Resources/Prototypes/Entities/Items.yml
@@ -0,0 +1,14 @@
+- type: entity
+ name: "Toolbox 2: Handle edition"
+ parent: Toolbox
+ id: ToolboxItem
+ components:
+ - type: Item
+
+- type: entity
+ name: "Mop 2: Handle edition"
+ parent: Mop
+ id: MopItem
+ components:
+ - type: Item
+
diff --git a/Resources/Prototypes/Entities/Mobs.yml b/Resources/Prototypes/Entities/Mobs.yml
new file mode 100644
index 0000000000..6e829fec7e
--- /dev/null
+++ b/Resources/Prototypes/Entities/Mobs.yml
@@ -0,0 +1,10 @@
+- type: entity
+ name: Urist McHands
+ id: HumanMob_Content
+ parent: HumanMob
+ components:
+ - type: Hands
+ hands:
+ - left
+ - right
+ - type: Inventory