diff --git a/Content.Client/Content.Client.csproj b/Content.Client/Content.Client.csproj
index fb7d05835e..0c937fa2d0 100644
--- a/Content.Client/Content.Client.csproj
+++ b/Content.Client/Content.Client.csproj
@@ -86,6 +86,9 @@
+
+
+
@@ -114,6 +117,8 @@
+
+
diff --git a/Content.Client/EntryPoint.cs b/Content.Client/EntryPoint.cs
index d1c4affc94..5caf8bec12 100644
--- a/Content.Client/EntryPoint.cs
+++ b/Content.Client/EntryPoint.cs
@@ -28,11 +28,15 @@ using System;
using Content.Client.Chat;
using Content.Client.GameObjects.Components;
using Content.Client.GameObjects.Components.Mobs;
+using Content.Client.GameObjects.Components.Research;
using Content.Client.GameObjects.Components.Sound;
using Content.Client.Interfaces.Chat;
+using Content.Client.Research;
using Content.Client.UserInterface;
using Content.Shared.GameObjects.Components.Markers;
+using Content.Shared.GameObjects.Components.Materials;
using Content.Shared.GameObjects.Components.Mobs;
+using Content.Shared.GameObjects.Components.Research;
using Robust.Client.Interfaces.UserInterface;
using Robust.Shared.Log;
@@ -72,7 +76,6 @@ namespace Content.Client
factory.RegisterIgnore("Storeable");
- factory.RegisterIgnore("Material");
factory.RegisterIgnore("Stack");
factory.Register();
@@ -86,7 +89,10 @@ namespace Content.Client
factory.Register();
factory.Register();
factory.Register();
+ factory.Register();
factory.Register();
+ factory.Register();
+ factory.RegisterReference();
factory.RegisterReference();
@@ -114,6 +120,11 @@ namespace Content.Client
factory.Register();
+ factory.Register();
+ factory.Register();
+
+ factory.RegisterReference();
+
factory.Register();
factory.RegisterReference();
diff --git a/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs b/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs
new file mode 100644
index 0000000000..c8fbeb063a
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Research/LatheBoundUserInterface.cs
@@ -0,0 +1,107 @@
+using System.Collections.Generic;
+using Content.Client.Research;
+using Content.Shared.GameObjects.Components.Research;
+using Content.Shared.Research;
+using Robust.Client.GameObjects.Components.UserInterface;
+using Robust.Client.Interfaces.Graphics;
+using Robust.Shared.GameObjects.Components.UserInterface;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.GameObjects.Components.Research
+{
+ public class LatheBoundUserInterface : BoundUserInterface
+ {
+#pragma warning disable CS0649
+ [Dependency]
+ private IDisplayManager _displayManager;
+ [Dependency]
+ private IPrototypeManager _prototypeManager;
+#pragma warning restore
+ [ViewVariables]
+ private LatheMenu menu;
+ [ViewVariables]
+ private LatheQueueMenu queueMenu;
+
+ public MaterialStorageComponent Storage { get; private set; }
+ public SharedLatheComponent Lathe { get; private set; }
+ public LatheDatabaseComponent Database { get; private set; }
+
+ [ViewVariables]
+ public Queue QueuedRecipes => _queuedRecipes;
+ private Queue _queuedRecipes = new Queue();
+
+ public LatheBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ SendMessage(new SharedLatheComponent.LatheSyncRequestMessage());
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+ IoCManager.InjectDependencies(this);
+
+ if (!Owner.Owner.TryGetComponent(out MaterialStorageComponent storage)
+ || !Owner.Owner.TryGetComponent(out SharedLatheComponent lathe)
+ || !Owner.Owner.TryGetComponent(out LatheDatabaseComponent database)) return;
+
+ Storage = storage;
+ Lathe = lathe;
+ Database = database;
+
+ menu = new LatheMenu(_displayManager) {Owner = this};
+ queueMenu = new LatheQueueMenu(_displayManager) { Owner = this };
+
+ menu.OnClose += Close;
+
+ menu.AddToScreen();
+ menu.Populate();
+ menu.PopulateMaterials();
+ queueMenu.AddToScreen();
+
+ menu.QueueButton.OnPressed += (args) => { queueMenu.OpenCentered(); };
+
+ storage.OnMaterialStorageChanged += menu.PopulateDisabled;
+ storage.OnMaterialStorageChanged += menu.PopulateMaterials;
+
+ menu.OpenCentered();
+ }
+
+ public void Queue(LatheRecipePrototype recipe, int quantity = 1)
+ {
+ SendMessage(new SharedLatheComponent.LatheQueueRecipeMessage(recipe.ID, quantity));
+ }
+
+ protected override void ReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ switch (message)
+ {
+ case SharedLatheComponent.LatheProducingRecipeMessage msg:
+ if (!_prototypeManager.TryIndex(msg.ID, out LatheRecipePrototype recipe)) break;
+ queueMenu.SetInfo(recipe);
+ break;
+ case SharedLatheComponent.LatheStoppedProducingRecipeMessage msg:
+ queueMenu.ClearInfo();
+ break;
+ case SharedLatheComponent.LatheFullQueueMessage msg:
+ _queuedRecipes.Clear();
+ foreach (var id in msg.Recipes)
+ {
+ if (!_prototypeManager.TryIndex(id, out LatheRecipePrototype recipePrototype)) break;
+ _queuedRecipes.Enqueue(recipePrototype);
+ }
+ queueMenu.PopulateList();
+ break;
+ }
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing) return;
+ menu?.Dispose();
+ queueMenu?.Dispose();
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Research/LatheDatabaseComponent.cs b/Content.Client/GameObjects/Components/Research/LatheDatabaseComponent.cs
new file mode 100644
index 0000000000..b40d96319d
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Research/LatheDatabaseComponent.cs
@@ -0,0 +1,30 @@
+using Content.Shared.GameObjects.Components.Research;
+using Content.Shared.Research;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Network;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+
+namespace Content.Client.GameObjects.Components.Research
+{
+ public class LatheDatabaseComponent : SharedLatheDatabaseComponent
+ {
+#pragma warning disable CS0649
+ [Dependency]
+ private IPrototypeManager _prototypeManager;
+#pragma warning restore
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ base.HandleComponentState(curState, nextState);
+ if (!(curState is LatheDatabaseState state)) return;
+ Clear();
+ foreach (var ID in state.Recipes)
+ {
+ if(!_prototypeManager.TryIndex(ID, out LatheRecipePrototype recipe)) continue;
+ AddRecipe(recipe);
+ }
+ }
+ }
+}
diff --git a/Content.Client/GameObjects/Components/Research/MaterialStorageComponent.cs b/Content.Client/GameObjects/Components/Research/MaterialStorageComponent.cs
new file mode 100644
index 0000000000..de32c6f55c
--- /dev/null
+++ b/Content.Client/GameObjects/Components/Research/MaterialStorageComponent.cs
@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using Content.Shared.GameObjects.Components.Research;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.GameObjects;
+using Robust.Shared.Interfaces.Network;
+
+namespace Content.Client.GameObjects.Components.Research
+{
+ public class MaterialStorageComponent : SharedMaterialStorageComponent
+ {
+ protected override Dictionary Storage { get; set; } = new Dictionary();
+
+ public event Action OnMaterialStorageChanged;
+
+ public override void HandleComponentState(ComponentState curState, ComponentState nextState)
+ {
+ base.HandleComponentState(curState, nextState);
+ if (!(curState is MaterialStorageState state)) return;
+ Storage = state.Storage;
+ OnMaterialStorageChanged?.Invoke();
+ }
+ }
+}
diff --git a/Content.Client/Research/LatheMenu.cs b/Content.Client/Research/LatheMenu.cs
new file mode 100644
index 0000000000..5a7ab389a7
--- /dev/null
+++ b/Content.Client/Research/LatheMenu.cs
@@ -0,0 +1,255 @@
+using System.Collections.Generic;
+using Content.Client.GameObjects.Components.Research;
+using Content.Shared.Materials;
+using Content.Shared.Research;
+using Robust.Client.Interfaces.Graphics;
+using Robust.Client.Interfaces.ResourceManagement;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.Utility;
+using Robust.Shared.IoC;
+using Robust.Shared.Maths;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timers;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Research
+{
+ public class LatheMenu : SS14Window
+ {
+#pragma warning disable CS0649
+ [Dependency]
+ private IPrototypeManager PrototypeManager;
+ [Dependency]
+ private IResourceCache ResourceCache;
+#pragma warning restore
+
+ private ItemList Items;
+ private ItemList Materials;
+ private LineEdit AmountLineEdit;
+ private LineEdit SearchBar;
+ public Button QueueButton;
+ protected override Vector2? CustomSize => (300, 450);
+
+ public LatheBoundUserInterface Owner { get; set; }
+
+ private List _recipes = new List();
+ private List _shownRecipes = new List();
+
+ public LatheMenu(IDisplayManager displayMan) : base(displayMan)
+ {
+ }
+
+ public LatheMenu(IDisplayManager displayMan, string name) : base(displayMan, name)
+ {
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+ IoCManager.InjectDependencies(this);
+
+ HideOnClose = true;
+ Title = "Lathe Menu";
+ Visible = false;
+
+ var margin = new MarginContainer()
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ MarginTop = 5f,
+ MarginLeft = 5f,
+ MarginRight = -5f,
+ MarginBottom = -5f,
+ };
+
+ margin.SetAnchorAndMarginPreset(LayoutPreset.Wide);
+
+ var vbox = new VBoxContainer()
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SeparationOverride = 5,
+ };
+
+ vbox.SetAnchorAndMarginPreset(LayoutPreset.Wide);
+
+ var hboxButtons = new HBoxContainer()
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 1,
+ };
+
+ QueueButton = new Button()
+ {
+ Text = "Queue",
+ TextAlign = Button.AlignMode.Center,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 1,
+ };
+
+ var spacer = new Control()
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 3,
+ };
+
+ spacer.SetAnchorAndMarginPreset(LayoutPreset.Wide);
+
+ var hboxFilter = new HBoxContainer()
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 1
+ };
+
+ SearchBar = new LineEdit()
+ {
+ PlaceHolder = "Search Designs",
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 3
+ };
+
+ SearchBar.OnTextChanged += Populate;
+
+ var filterButton = new Button()
+ {
+ Text = "Filter",
+ TextAlign = Button.AlignMode.Center,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 1,
+ Disabled = true,
+ };
+
+ Items = new ItemList()
+ {
+ SizeFlagsStretchRatio = 8,
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ };
+
+
+
+ Items.OnItemSelected += ItemSelected;
+
+ AmountLineEdit = new LineEdit()
+ {
+ PlaceHolder = "Amount",
+ Text = "1",
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ };
+
+ AmountLineEdit.OnTextChanged += PopulateDisabled;
+
+ Materials = new ItemList()
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 3
+ };
+
+ hboxButtons.AddChild(spacer);
+ hboxButtons.AddChild(QueueButton);
+
+ hboxFilter.AddChild(SearchBar);
+ hboxFilter.AddChild(filterButton);
+
+ vbox.AddChild(hboxButtons);
+ vbox.AddChild(hboxFilter);
+ vbox.AddChild(Items);
+ vbox.AddChild(AmountLineEdit);
+ vbox.AddChild(Materials);
+
+ margin.AddChild(vbox);
+
+ Contents.AddChild(margin);
+ }
+
+ public void ItemSelected(ItemList.ItemListSelectedEventArgs args)
+ {
+ int.TryParse(AmountLineEdit.Text, out var quantity);
+ if (quantity <= 0) quantity = 1;
+ Owner.Queue(_shownRecipes[args.ItemIndex], quantity);
+ Items.SelectMode = ItemList.ItemListSelectMode.None;
+ Timer.Spawn(100, () =>
+ {
+ Items.Unselect(args.ItemIndex);
+ Items.SelectMode = ItemList.ItemListSelectMode.Single;
+ });
+ }
+
+ public void PopulateMaterials()
+ {
+ Materials.Clear();
+
+ foreach (var (id, amount) in Owner.Storage)
+ {
+ if (!PrototypeManager.TryIndex(id, out MaterialPrototype materialPrototype)) continue;
+ var material = materialPrototype.Material;
+ Materials.AddItem($"{material.Name} {amount} cm3", material.Icon.Frame0(), false);
+ }
+ }
+
+ ///
+ /// Disables or enables shown recipes depending on whether there are enough materials for it or not.
+ ///
+ public void PopulateDisabled()
+ {
+ int.TryParse(AmountLineEdit.Text, out var quantity);
+ if (quantity <= 0) quantity = 1;
+ for (var i = 0; i < _shownRecipes.Count; i++)
+ {
+ var prototype = _shownRecipes[i];
+ Items.SetItemDisabled(i, !Owner.Lathe.CanProduce(prototype, quantity));
+ }
+ }
+
+ ///
+ public void PopulateDisabled(LineEdit.LineEditEventArgs args)
+ {
+ PopulateDisabled();
+ }
+
+ ///
+ /// Adds shown recipes to the ItemList control.
+ ///
+ public void PopulateList()
+ {
+ Items.Clear();
+ for (var i = 0; i < _shownRecipes.Count; i++)
+ {
+ var prototype = _shownRecipes[i];
+ Items.AddItem(prototype.Name, prototype.Icon.Frame0());
+ }
+
+ PopulateDisabled();
+ }
+
+ ///
+ /// Populates the list of recipes that will actually be shown, using the current filters.
+ ///
+ public void Populate()
+ {
+ _shownRecipes.Clear();
+
+ foreach (var prototype in Owner.Database)
+ {
+ if (SearchBar.Text.Trim().Length != 0)
+ {
+ if (prototype.Name.ToLowerInvariant().Contains(SearchBar.Text.Trim().ToLowerInvariant()))
+ _shownRecipes.Add(prototype);
+ continue;
+ }
+
+ _shownRecipes.Add(prototype);
+ }
+
+ PopulateList();
+ }
+
+ ///
+ public void Populate(LineEdit.LineEditEventArgs args)
+ {
+ Populate();
+ }
+ }
+}
diff --git a/Content.Client/Research/LatheQueueMenu.cs b/Content.Client/Research/LatheQueueMenu.cs
new file mode 100644
index 0000000000..64b617ad78
--- /dev/null
+++ b/Content.Client/Research/LatheQueueMenu.cs
@@ -0,0 +1,149 @@
+using Content.Client.GameObjects.Components.Research;
+using Content.Shared.Research;
+using Robust.Client.Graphics;
+using Robust.Client.Graphics.Drawing;
+using Robust.Client.Interfaces.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.Utility;
+using Robust.Shared.IoC;
+using Robust.Shared.Log;
+using Robust.Shared.Maths;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Client.Research
+{
+ public class LatheQueueMenu : SS14Window
+ {
+ protected override Vector2? CustomSize => (300, 450);
+
+ public LatheBoundUserInterface Owner { get; set; }
+
+ [ViewVariables]
+ private ItemList QueueList;
+ private Label Name;
+ private Label Description;
+ private TextureRect Icon;
+
+ public LatheQueueMenu(IDisplayManager displayManager) : base(displayManager)
+ {
+
+ }
+
+ protected override void Initialize()
+ {
+ base.Initialize();
+
+ HideOnClose = true;
+ Title = "Lathe Queue";
+ Visible = false;
+
+ var margin = new MarginContainer()
+ {
+ MarginTop = 5f,
+ MarginLeft = 5f,
+ MarginRight = -5f,
+ MarginBottom = -5f,
+ };
+
+ margin.SetAnchorAndMarginPreset(LayoutPreset.Wide);
+
+ var vbox = new VBoxContainer();
+
+ vbox.SetAnchorAndMarginPreset(LayoutPreset.Wide);
+
+ var descMargin = new MarginContainer()
+ {
+ MarginTop = 5f,
+ MarginLeft = 5f,
+ MarginRight = -5f,
+ MarginBottom = -5f,
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 2,
+ };
+
+ var hbox = new HBoxContainer()
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ };
+
+ Icon = new TextureRect()
+ {
+ SizeFlagsHorizontal = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 2,
+ };
+
+ var vboxInfo = new VBoxContainer()
+ {
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 3,
+ };
+
+ Name = new Label()
+ {
+ RectClipContent = true,
+ SizeFlagsHorizontal = SizeFlags.Fill,
+ };
+
+ Description = new Label()
+ {
+ RectClipContent = true,
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsHorizontal = SizeFlags.Fill,
+
+ };
+
+ QueueList = new ItemList()
+ {
+ SizeFlagsHorizontal = SizeFlags.Fill,
+ SizeFlagsVertical = SizeFlags.FillExpand,
+ SizeFlagsStretchRatio = 3,
+ SelectMode = ItemList.ItemListSelectMode.None
+ };
+
+ vboxInfo.AddChild(Name);
+ vboxInfo.AddChild(Description);
+
+ hbox.AddChild(Icon);
+ hbox.AddChild(vboxInfo);
+
+ descMargin.AddChild(hbox);
+
+ vbox.AddChild(descMargin);
+ vbox.AddChild(QueueList);
+
+ margin.AddChild(vbox);
+
+ Contents.AddChild(margin);
+
+ ClearInfo();
+ }
+
+ public void SetInfo(LatheRecipePrototype recipe)
+ {
+ Icon.Texture = recipe.Icon.Frame0();
+ if (recipe.Name != null)
+ Name.Text = recipe.Name;
+ if (recipe.Description != null)
+ Description.Text = recipe.Description;
+ }
+
+ public void ClearInfo()
+ {
+ Icon.Texture = Texture.Transparent;
+ Name.Text = "-------";
+ Description.Text = "Not producing anything.";
+ }
+
+ public void PopulateList()
+ {
+ QueueList.Clear();
+ var idx = 1;
+ foreach (var recipe in Owner.QueuedRecipes)
+ {
+ QueueList.AddItem($"{idx}. {recipe.Name}", recipe.Icon.Frame0(), false);
+ idx++;
+ }
+ }
+ }
+}
diff --git a/Content.Server/Content.Server.csproj b/Content.Server/Content.Server.csproj
index 5fd73f4365..b08832352c 100644
--- a/Content.Server/Content.Server.csproj
+++ b/Content.Server/Content.Server.csproj
@@ -108,6 +108,9 @@
+
+
+
@@ -125,6 +128,7 @@
+
@@ -198,8 +202,6 @@
-
-
diff --git a/Content.Server/EntryPoint.cs b/Content.Server/EntryPoint.cs
index 885862d97a..a4ab7fccd8 100644
--- a/Content.Server/EntryPoint.cs
+++ b/Content.Server/EntryPoint.cs
@@ -26,7 +26,6 @@ using Content.Server.GameObjects.Components.Weapon.Ranged.Hitscan;
using Content.Server.GameObjects.Components.Weapon.Ranged.Projectile;
using Content.Server.GameObjects.Components.Projectiles;
using Content.Server.GameObjects.Components.Weapon.Melee;
-using Content.Server.GameObjects.Components.Materials;
using Content.Server.GameObjects.Components.Stack;
using Content.Server.GameObjects.Components.Construction;
using Content.Server.GameObjects.Components.Mobs;
@@ -40,6 +39,7 @@ using Content.Server.GameObjects.Components.Weapon.Ranged;
using Content.Server.GameTicking;
using Content.Server.Interfaces;
using Content.Server.Interfaces.GameTicking;
+using Content.Shared.GameObjects.Components.Materials;
using Content.Shared.GameObjects.Components.Inventory;
using Content.Shared.GameObjects.Components.Markers;
using Content.Shared.GameObjects.Components.Mobs;
@@ -50,6 +50,8 @@ using Content.Server.GameObjects.Components.Destructible;
using Content.Server.GameObjects.Components.Movement;
using Content.Server.Interfaces.Chat;
using Content.Server.Interfaces.GameObjects.Components.Movement;
+using Content.Server.GameObjects.Components.Research;
+using Content.Shared.GameObjects.Components.Research;
namespace Content.Server
{
@@ -127,6 +129,8 @@ namespace Content.Server
factory.Register();
factory.Register();
factory.Register();
+ factory.Register();
+ factory.RegisterReference();
factory.Register();
factory.Register();
@@ -139,6 +143,11 @@ namespace Content.Server
factory.Register();
factory.RegisterReference();
+ factory.Register();
+ factory.Register();
+
+ factory.RegisterReference();
+
factory.Register();
factory.Register();
diff --git a/Content.Server/GameObjects/Components/Research/LatheComponent.cs b/Content.Server/GameObjects/Components/Research/LatheComponent.cs
new file mode 100644
index 0000000000..5e55075e53
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Research/LatheComponent.cs
@@ -0,0 +1,152 @@
+using System.Collections.Generic;
+using Content.Server.GameObjects.Components.Stack;
+using Content.Server.GameObjects.EntitySystems;
+using Content.Shared.GameObjects.Components.Materials;
+using Content.Shared.GameObjects.Components.Research;
+using Content.Shared.Research;
+using Robust.Server.GameObjects.Components.UserInterface;
+using Robust.Server.Interfaces.GameObjects;
+using Robust.Shared.GameObjects.Components.UserInterface;
+using Robust.Shared.IoC;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Timers;
+using Robust.Shared.Utility;
+using Robust.Shared.ViewVariables;
+
+namespace Content.Server.GameObjects.Components.Research
+{
+ public class LatheComponent : SharedLatheComponent, IAttackHand, IAttackBy, IActivate
+ {
+ public const int VolumePerSheet = 3750;
+
+ private BoundUserInterface _userInterface;
+
+ [ViewVariables]
+ public Queue Queue { get; } = new Queue();
+
+ [ViewVariables]
+ public bool Producing { get; private set; } = false;
+
+ private LatheRecipePrototype _producingRecipe = null;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ _userInterface = Owner.GetComponent().GetBoundUserInterface(LatheUiKey.Key);
+ _userInterface.OnReceiveMessage += UserInterfaceOnOnReceiveMessage;
+ }
+
+ private void UserInterfaceOnOnReceiveMessage(BoundUserInterfaceMessage message)
+ {
+ switch (message)
+ {
+ case LatheQueueRecipeMessage msg:
+ _prototypeManager.TryIndex(msg.ID, out LatheRecipePrototype recipe);
+ if (recipe != null)
+ for (var i = 0; i < msg.Quantity; i++)
+ {
+ Queue.Enqueue(recipe);
+ _userInterface.SendMessage(new LatheFullQueueMessage(GetIDQueue()));
+ }
+ break;
+ case LatheSyncRequestMessage msg:
+ if (!Owner.TryGetComponent(out MaterialStorageComponent storage)) return;
+ _userInterface.SendMessage(new LatheFullQueueMessage(GetIDQueue()));
+ if (_producingRecipe != null)
+ _userInterface.SendMessage(new LatheProducingRecipeMessage(_producingRecipe.ID));
+ break;
+ }
+ }
+
+ internal bool Produce(LatheRecipePrototype recipe)
+ {
+ if (Producing || !CanProduce(recipe) || !Owner.TryGetComponent(out MaterialStorageComponent storage)) return false;
+
+ _userInterface.SendMessage(new LatheFullQueueMessage(GetIDQueue()));
+
+ Producing = true;
+ _producingRecipe = recipe;
+
+ foreach (var (material, amount) in recipe.RequiredMaterials)
+ {
+ // This should always return true, otherwise CanProduce fucked up.
+ storage.RemoveMaterial(material, amount);
+ }
+
+ _userInterface.SendMessage(new LatheProducingRecipeMessage(recipe.ID));
+
+ Timer.Spawn(recipe.CompleteTime, () =>
+ {
+ Producing = false;
+ _producingRecipe = null;
+ Owner.EntityManager.TrySpawnEntityAt(recipe.Result, Owner.Transform.GridPosition, out var entity);
+ _userInterface.SendMessage(new LatheStoppedProducingRecipeMessage());
+ });
+
+ return true;
+ }
+
+ void IActivate.Activate(ActivateEventArgs eventArgs)
+ {
+ if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
+ return;
+
+ _userInterface.Open(actor.playerSession);
+ return;
+ }
+
+ bool IAttackHand.AttackHand(AttackHandEventArgs eventArgs)
+ {
+
+ if (!eventArgs.User.TryGetComponent(out IActorComponent actor))
+ return false;
+
+ _userInterface.Open(actor.playerSession);
+ return true;
+ }
+
+ bool IAttackBy.AttackBy(AttackByEventArgs eventArgs)
+ {
+ if (!Owner.TryGetComponent(out MaterialStorageComponent storage)
+ || !eventArgs.AttackWith.TryGetComponent(out MaterialComponent material)) return false;
+
+ var multiplier = 1;
+
+ if (eventArgs.AttackWith.TryGetComponent(out StackComponent stack)) multiplier = stack.Count;
+
+ var totalAmount = 0;
+
+ // Check if it can insert all materials.
+ foreach (var mat in material.MaterialTypes.Values)
+ {
+ // TODO: Change how MaterialComponent works so this is not hard-coded.
+ if (!storage.CanInsertMaterial(mat.ID, VolumePerSheet * multiplier)) return false;
+ totalAmount += VolumePerSheet * multiplier;
+ }
+
+ // Check if it can take ALL of the material's volume.
+ if (storage.CanTakeAmount(totalAmount)) return false;
+
+ foreach (var mat in material.MaterialTypes.Values)
+ {
+
+ storage.InsertMaterial(mat.ID, VolumePerSheet * multiplier);
+ }
+
+ eventArgs.AttackWith.Delete();
+
+ return false;
+ }
+
+ private Queue GetIDQueue()
+ {
+ var queue = new Queue();
+ foreach (var recipePrototype in Queue)
+ {
+ queue.Enqueue(recipePrototype.ID);
+ }
+
+ return queue;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs b/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs
new file mode 100644
index 0000000000..b0d6c85e37
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Research/LatheDatabaseComponent.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+using Content.Shared.GameObjects.Components.Research;
+using Content.Shared.Research;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Research
+{
+ public class LatheDatabaseComponent : SharedLatheDatabaseComponent
+ {
+ ///
+ /// Whether new recipes can be added to this database or not.
+ ///
+ public bool Static => _static;
+ private bool _static = false;
+
+ public override ComponentState GetComponentState()
+ {
+ return new LatheDatabaseState(GetRecipeIdList());
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _static, "static", false);
+ }
+
+ public override void Clear()
+ {
+ if (Static) return;
+ Dirty();
+ }
+
+ public override void AddRecipe(LatheRecipePrototype recipe)
+ {
+ if (Static) return;
+ Dirty();
+ }
+
+ public override bool RemoveRecipe(LatheRecipePrototype recipe)
+ {
+ if (Static || !base.RemoveRecipe(recipe)) return false;
+ Dirty();
+ return true;
+ }
+
+ private List GetRecipeIdList()
+ {
+ var list = new List();
+
+ foreach (var recipe in this)
+ {
+ list.Add(recipe.ID);
+ }
+
+ return list;
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/Components/Research/MaterialStorageComponent.cs b/Content.Server/GameObjects/Components/Research/MaterialStorageComponent.cs
new file mode 100644
index 0000000000..2d1d22f061
--- /dev/null
+++ b/Content.Server/GameObjects/Components/Research/MaterialStorageComponent.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using Content.Shared.GameObjects.Components.Research;
+using Robust.Shared.GameObjects;
+using Robust.Shared.Interfaces.Network;
+using Robust.Shared.Serialization;
+
+namespace Content.Server.GameObjects.Components.Research
+{
+ public class MaterialStorageComponent : SharedMaterialStorageComponent
+ {
+ protected override Dictionary Storage { get; set; } = new Dictionary();
+
+ ///
+ /// How much material the storage can store in total.
+ ///
+ public int StorageLimit => _storageLimit;
+ private int _storageLimit;
+
+ public override ComponentState GetComponentState()
+ {
+ return new MaterialStorageState(Storage);
+ }
+
+ ///
+ /// Checks if the storage can take a volume of material without surpassing its own limits.
+ ///
+ /// The volume of material
+ ///
+ public bool CanTakeAmount(int amount)
+ {
+ return CurrentAmount + amount <= StorageLimit;
+ }
+
+ ///
+ /// Checks if it can insert a material.
+ ///
+ /// Material ID
+ /// How much to insert
+ /// Whether it can insert the material or not.
+ public bool CanInsertMaterial(string ID, int amount)
+ {
+ return (CanTakeAmount(amount) || StorageLimit < 0) && (!Storage.ContainsKey(ID) || Storage[ID] + amount >= 0);
+ }
+
+ ///
+ /// Inserts material into the storage.
+ ///
+ /// Material ID
+ /// How much to insert
+ /// Whether it inserted it or not.
+ public bool InsertMaterial(string ID, int amount)
+ {
+ if (!CanInsertMaterial(ID, amount)) return false;
+
+ if (!Storage.ContainsKey(ID))
+ Storage.Add(ID, 0);
+
+ Storage[ID] += amount;
+
+ Dirty();
+
+ return true;
+ }
+
+ ///
+ /// Removes material from the storage.
+ ///
+ /// Material ID
+ /// How much to remove
+ /// Whether it removed it or not.
+ public bool RemoveMaterial(string ID, int amount)
+ {
+ return InsertMaterial(ID, -amount);
+ }
+
+ public override void ExposeData(ObjectSerializer serializer)
+ {
+ base.ExposeData(serializer);
+
+ serializer.DataField(ref _storageLimit, "StorageLimit", -1);
+ }
+ }
+}
diff --git a/Content.Server/GameObjects/EntitySystems/LatheSystem.cs b/Content.Server/GameObjects/EntitySystems/LatheSystem.cs
new file mode 100644
index 0000000000..b85a251a4a
--- /dev/null
+++ b/Content.Server/GameObjects/EntitySystems/LatheSystem.cs
@@ -0,0 +1,26 @@
+using Content.Server.GameObjects.Components.Research;
+using Robust.Shared.GameObjects;
+using Robust.Shared.GameObjects.Systems;
+
+namespace Content.Server.GameObjects.EntitySystems
+{
+ public class LatheSystem : EntitySystem
+ {
+ public override void Initialize()
+ {
+ EntityQuery = new TypeEntityQuery(typeof(LatheComponent));
+ }
+
+ public override void Update(float frameTime)
+ {
+ foreach (var entity in RelevantEntities)
+ {
+ var comp = entity.GetComponent();
+ if (comp.Producing == false && comp.Queue.Count > 0)
+ {
+ comp.Produce(comp.Queue.Dequeue());
+ }
+ }
+ }
+ }
+}
diff --git a/Content.Shared/Content.Shared.csproj b/Content.Shared/Content.Shared.csproj
index 00dda7b7e2..94c90549c4 100644
--- a/Content.Shared/Content.Shared.csproj
+++ b/Content.Shared/Content.Shared.csproj
@@ -74,9 +74,13 @@
+
+
+
+
@@ -90,6 +94,7 @@
+
@@ -100,6 +105,7 @@
+
diff --git a/Content.Server/GameObjects/Components/Materials/MaterialComponent.cs b/Content.Shared/GameObjects/Components/Materials/MaterialComponent.cs
similarity index 81%
rename from Content.Server/GameObjects/Components/Materials/MaterialComponent.cs
rename to Content.Shared/GameObjects/Components/Materials/MaterialComponent.cs
index f1a3ae2edc..2514c9cdfc 100644
--- a/Content.Server/GameObjects/Components/Materials/MaterialComponent.cs
+++ b/Content.Shared/GameObjects/Components/Materials/MaterialComponent.cs
@@ -1,5 +1,5 @@
using System.Collections.Generic;
-using Content.Server.Materials;
+using Content.Shared.Materials;
using Robust.Shared.GameObjects;
using Robust.Shared.Interfaces.Reflection;
using Robust.Shared.Interfaces.Serialization;
@@ -8,7 +8,7 @@ using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
-namespace Content.Server.GameObjects.Components.Materials
+namespace Content.Shared.GameObjects.Components.Materials
{
///
/// Component to store data such as "this object is made out of steel".
@@ -19,7 +19,8 @@ namespace Content.Server.GameObjects.Components.Materials
public const string SerializationCache = "mat";
public override string Name => "Material";
- Dictionary