]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Batchable lathe jobs, editable lathe job order (#38624)
authorWhatstone <166147148+whatston3@users.noreply.github.com>
Sun, 24 Aug 2025 15:02:47 +0000 (11:02 -0400)
committerGitHub <noreply@github.com>
Sun, 24 Aug 2025 15:02:47 +0000 (17:02 +0200)
* batchable lathe jobs, editable order

* requested changes

* LatheComponent comment, menu strings

Content.Client/Lathe/UI/LatheBoundUserInterface.cs
Content.Client/Lathe/UI/LatheMenu.xaml
Content.Client/Lathe/UI/LatheMenu.xaml.cs
Content.Client/Lathe/UI/QueuedRecipeControl.xaml [new file with mode: 0644]
Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs [new file with mode: 0644]
Content.Server/Lathe/LatheSystem.cs
Content.Shared/Lathe/LatheComponent.cs
Content.Shared/Lathe/LatheMessages.cs
Content.Shared/Lathe/PrototypeIdLinkedListSerializer.cs [new file with mode: 0644]
Content.Shared/Lathe/SharedLatheSystem.cs
Resources/Locale/en-US/lathe/ui/lathe-menu.ftl

index 4ddde885fa572c9938b7b573137c60ff56cf6664..75b1704b0d80853182b89645f14b3eda1b750648 100644 (file)
@@ -30,6 +30,10 @@ namespace Content.Client.Lathe.UI
             {
                 SendMessage(new LatheQueueRecipeMessage(recipe, amount));
             };
+            _menu.QueueDeleteAction += index => SendMessage(new LatheDeleteRequestMessage(index));
+            _menu.QueueMoveUpAction += index => SendMessage(new LatheMoveRequestMessage(index, -1));
+            _menu.QueueMoveDownAction += index => SendMessage(new LatheMoveRequestMessage(index, 1));
+            _menu.DeleteFabricatingAction += () => SendMessage(new LatheAbortFabricationMessage());
         }
 
         protected override void UpdateState(BoundUserInterfaceState state)
index 28b79254c02f73147540189bce9ef91d3f963781..a5c8f6a85cb2169d1310bd1c892fc88c0962d243 100644 (file)
@@ -1,6 +1,7 @@
 <DefaultWindow
     xmlns="https://spacestation14.io"
     xmlns:gfx="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+    xmlns:system="clr-namespace:System;assembly=System.Runtime"
     xmlns:ui="clr-namespace:Content.Client.Materials.UI"
     Title="{Loc 'lathe-menu-title'}"
     MinSize="550 450"
                             HorizontalAlignment="Left"
                             Margin="130 0 0 0">
                         </Label>
+                        <Button
+                            Name="DeleteFabricating"
+                            Margin="0"
+                            Text="✖"
+                            SetSize="38 32"
+                            HorizontalAlignment="Right"
+                            ToolTip="{Loc 'lathe-menu-delete-fabricating-tooltip'}">
+                            <Button.StyleClasses>
+                                <system:String>Caution</system:String>
+                                <system:String>OpenLeft</system:String>
+                            </Button.StyleClasses>
+                        </Button>
                     </PanelContainer>
                 </BoxContainer>
                 <ScrollContainer VerticalExpand="True" HScrollEnabled="False">
index 66d875b0f233e7883b29e81971574338d713a4f1..ce190464d2c2cf87170a1087fa05809a14458de1 100644 (file)
@@ -26,6 +26,10 @@ public sealed partial class LatheMenu : DefaultWindow
 
     public event Action<BaseButton.ButtonEventArgs>? OnServerListButtonPressed;
     public event Action<string, int>? RecipeQueueAction;
+    public event Action<int>? QueueDeleteAction;
+    public event Action<int>? QueueMoveUpAction;
+    public event Action<int>? QueueMoveDownAction;
+    public event Action? DeleteFabricatingAction;
 
     public List<ProtoId<LatheRecipePrototype>> Recipes = new();
 
@@ -50,12 +54,21 @@ public sealed partial class LatheMenu : DefaultWindow
         };
         AmountLineEdit.OnTextChanged += _ =>
         {
+            if (int.TryParse(AmountLineEdit.Text, out var amount))
+            {
+                if (amount > LatheSystem.MaxItemsPerRequest)
+                    AmountLineEdit.Text = LatheSystem.MaxItemsPerRequest.ToString();
+                else if (amount < 0)
+                    AmountLineEdit.Text = "0";
+            }
+
             PopulateRecipes();
         };
 
         FilterOption.OnItemSelected += OnItemSelected;
 
         ServerListButton.OnPressed += a => OnServerListButtonPressed?.Invoke(a);
+        DeleteFabricating.OnPressed += _ => DeleteFabricatingAction?.Invoke();
     }
 
     public void SetEntity(EntityUid uid)
@@ -223,22 +236,27 @@ public sealed partial class LatheMenu : DefaultWindow
     /// Populates the build queue list with all queued items
     /// </summary>
     /// <param name="queue"></param>
-    public void PopulateQueueList(IReadOnlyCollection<ProtoId<LatheRecipePrototype>> queue)
+    public void PopulateQueueList(IReadOnlyCollection<LatheRecipeBatch> queue)
     {
         QueueList.DisposeAllChildren();
 
         var idx = 1;
-        foreach (var recipeProto in queue)
+        foreach (var batch in queue)
         {
-            var recipe = _prototypeManager.Index(recipeProto);
-            var queuedRecipeBox = new BoxContainer();
-            queuedRecipeBox.Orientation = BoxContainer.LayoutOrientation.Horizontal;
+            var recipe = _prototypeManager.Index(batch.Recipe);
+
+            var itemName = _lathe.GetRecipeName(batch.Recipe);
+            string displayText;
+            if (batch.ItemsRequested > 1)
+                displayText = Loc.GetString("lathe-menu-item-batch", ("index", idx), ("name", itemName), ("printed", batch.ItemsPrinted), ("total", batch.ItemsRequested));
+            else
+                displayText = Loc.GetString("lathe-menu-item-single", ("index", idx), ("name", itemName));
 
-            queuedRecipeBox.AddChild(GetRecipeDisplayControl(recipe));
+            var queuedRecipeBox = new QueuedRecipeControl(displayText, idx - 1, GetRecipeDisplayControl(recipe));
+            queuedRecipeBox.OnDeletePressed += s => QueueDeleteAction?.Invoke(s);
+            queuedRecipeBox.OnMoveUpPressed += s => QueueMoveUpAction?.Invoke(s);
+            queuedRecipeBox.OnMoveDownPressed += s => QueueMoveDownAction?.Invoke(s);
 
-            var queuedRecipeLabel = new Label();
-            queuedRecipeLabel.Text = $"{idx}. {_lathe.GetRecipeName(recipe)}";
-            queuedRecipeBox.AddChild(queuedRecipeLabel);
             QueueList.AddChild(queuedRecipeBox);
             idx++;
         }
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml
new file mode 100644 (file)
index 0000000..b1d4b49
--- /dev/null
@@ -0,0 +1,35 @@
+<Control xmlns="https://spacestation14.io"
+    xmlns:system="clr-namespace:System;assembly=System.Runtime">
+    <BoxContainer Orientation="Horizontal">
+        <BoxContainer
+            Name="RecipeDisplayContainer"
+            Margin="0 0 4 0"
+            HorizontalAlignment="Center"
+            VerticalAlignment="Center"
+            MinSize="32 32"
+            />
+        <Label Name="RecipeName" HorizontalExpand="True" />
+        <Button
+            Name="MoveUp"
+            Margin="0"
+            Text="⏶"
+            StyleClasses="OpenRight"
+            ToolTip="{Loc 'lathe-menu-move-up-tooltip'}"/>
+        <Button
+            Name="MoveDown"
+            Margin="0"
+            Text="⏷"
+            StyleClasses="OpenBoth"
+            ToolTip="{Loc 'lathe-menu-move-down-tooltip'}"/>
+        <Button
+            Name="Delete"
+            Margin="0"
+            Text="✖"
+            ToolTip="{Loc 'lathe-menu-delete-item-tooltip'}">
+            <Button.StyleClasses>
+                <system:String>Caution</system:String>
+                <system:String>OpenLeft</system:String>
+            </Button.StyleClasses>
+        </Button>
+    </BoxContainer>
+</Control>
diff --git a/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs b/Content.Client/Lathe/UI/QueuedRecipeControl.xaml.cs
new file mode 100644 (file)
index 0000000..c4ba980
--- /dev/null
@@ -0,0 +1,36 @@
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Lathe.UI;
+
+[GenerateTypedNameReferences]
+public sealed partial class QueuedRecipeControl : Control
+{
+    public Action<int>? OnDeletePressed;
+    public Action<int>? OnMoveUpPressed;
+    public Action<int>? OnMoveDownPressed;
+
+    public QueuedRecipeControl(string displayText, int index, Control displayControl)
+    {
+        RobustXamlLoader.Load(this);
+
+        RecipeName.Text = displayText;
+        RecipeDisplayContainer.AddChild(displayControl);
+
+        MoveUp.OnPressed += (_) =>
+        {
+            OnMoveUpPressed?.Invoke(index);
+        };
+
+        MoveDown.OnPressed += (_) =>
+        {
+            OnMoveDownPressed?.Invoke(index);
+        };
+
+        Delete.OnPressed += (_) =>
+        {
+            OnDeletePressed?.Invoke(index);
+        };
+    }
+}
index 6ecd5bdf62cf9a7a6b27fa47e12bba06726d6c64..02abb0779121a50cf2c76d13645cf3008c2ad695 100644 (file)
@@ -74,6 +74,9 @@ namespace Content.Server.Lathe
 
             SubscribeLocalEvent<LatheComponent, LatheQueueRecipeMessage>(OnLatheQueueRecipeMessage);
             SubscribeLocalEvent<LatheComponent, LatheSyncRequestMessage>(OnLatheSyncRequestMessage);
+            SubscribeLocalEvent<LatheComponent, LatheDeleteRequestMessage>(OnLatheDeleteRequestMessage);
+            SubscribeLocalEvent<LatheComponent, LatheMoveRequestMessage>(OnLatheMoveRequestMessage);
+            SubscribeLocalEvent<LatheComponent, LatheAbortFabricationMessage>(OnLatheAbortFabricationMessage);
 
             SubscribeLocalEvent<LatheComponent, BeforeActivatableUIOpenEvent>((u, c, _) => UpdateUserInterfaceState(u, c));
             SubscribeLocalEvent<LatheComponent, MaterialAmountChangedEvent>(OnMaterialAmountChanged);
@@ -167,23 +170,32 @@ namespace Content.Server.Lathe
             return ev.Recipes.ToList();
         }
 
-        public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, LatheComponent? component = null)
+        public bool TryAddToQueue(EntityUid uid, LatheRecipePrototype recipe, int quantity, LatheComponent? component = null)
         {
             if (!Resolve(uid, ref component))
                 return false;
 
-            if (!CanProduce(uid, recipe, 1, component))
+            if (quantity <= 0)
+                return false;
+            quantity = int.Min(quantity, MaxItemsPerRequest);
+
+            if (!CanProduce(uid, recipe, quantity, component))
                 return false;
 
             foreach (var (mat, amount) in recipe.Materials)
             {
                 var adjustedAmount = recipe.ApplyMaterialDiscount
-                    ? (int) (-amount * component.MaterialUseMultiplier)
+                    ? (int)(-amount * component.MaterialUseMultiplier)
                     : -amount;
+                adjustedAmount *= quantity;
 
                 _materialStorage.TryChangeMaterialAmount(uid, mat, adjustedAmount);
             }
-            component.Queue.Enqueue(recipe);
+
+            if (component.Queue.Last is { } node && node.ValueRef.Recipe == recipe.ID)
+                node.ValueRef.ItemsRequested += quantity;
+            else
+                component.Queue.AddLast(new LatheRecipeBatch(recipe.ID, 0, quantity));
 
             return true;
         }
@@ -195,8 +207,11 @@ namespace Content.Server.Lathe
             if (component.CurrentRecipe != null || component.Queue.Count <= 0 || !this.IsPowered(uid, EntityManager))
                 return false;
 
-            var recipeProto = component.Queue.Dequeue();
-            var recipe = _proto.Index(recipeProto);
+            var batch = component.Queue.First();
+            batch.ItemsPrinted++;
+            if (batch.ItemsPrinted >= batch.ItemsRequested || batch.ItemsPrinted < 0) // Rollover sanity check
+                component.Queue.RemoveFirst();
+            var recipe = _proto.Index(batch.Recipe);
 
             var time = _reagentSpeed.ApplySpeed(uid, recipe.CompleteTime) * component.TimeMultiplier;
 
@@ -271,8 +286,8 @@ namespace Content.Server.Lathe
                 return;
 
             var producing = component.CurrentRecipe;
-            if (producing == null && component.Queue.TryPeek(out var next))
-                producing = next;
+            if (producing == null && component.Queue.First is { } node)
+                producing = node.Value.Recipe;
 
             var state = new LatheUpdateState(GetAvailableRecipes(uid, component), component.Queue.ToArray(), producing);
             _uiSys.SetUiState(uid, LatheUiKey.Key, state);
@@ -349,12 +364,10 @@ namespace Content.Server.Lathe
         {
             if (!args.Powered)
             {
-                RemComp<LatheProducingComponent>(uid);
-                UpdateRunningAppearance(uid, false);
+                AbortProduction(uid);
             }
-            else if (component.CurrentRecipe != null)
+            else
             {
-                EnsureComp<LatheProducingComponent>(uid);
                 TryStartProducing(uid, component);
             }
         }
@@ -416,25 +429,46 @@ namespace Content.Server.Lathe
             return GetAvailableRecipes(uid, component).Contains(recipe.ID);
         }
 
+        public void AbortProduction(EntityUid uid, LatheComponent? component = null)
+        {
+            if (!Resolve(uid, ref component))
+                return;
+
+            if (component.CurrentRecipe != null)
+            {
+                if (component.Queue.Count > 0)
+                {
+                    // Batch abandoned while printing last item, need to create a one-item batch
+                    var batch = component.Queue.First();
+                    if (batch.Recipe != component.CurrentRecipe)
+                    {
+                        var newBatch = new LatheRecipeBatch(component.CurrentRecipe.Value, 0, 1);
+                        component.Queue.AddFirst(newBatch);
+                    }
+                    else if (batch.ItemsPrinted > 0)
+                    {
+                        batch.ItemsPrinted--;
+                    }
+                }
+
+                component.CurrentRecipe = null;
+            }
+            RemCompDeferred<LatheProducingComponent>(uid);
+            UpdateUserInterfaceState(uid, component);
+            UpdateRunningAppearance(uid, false);
+        }
+
         #region UI Messages
 
         private void OnLatheQueueRecipeMessage(EntityUid uid, LatheComponent component, LatheQueueRecipeMessage args)
         {
             if (_proto.TryIndex(args.ID, out LatheRecipePrototype? recipe))
             {
-                var count = 0;
-                for (var i = 0; i < args.Quantity; i++)
-                {
-                    if (TryAddToQueue(uid, recipe, component))
-                        count++;
-                    else
-                        break;
-                }
-                if (count > 0)
+                if (TryAddToQueue(uid, recipe, args.Quantity, component))
                 {
                     _adminLogger.Add(LogType.Action,
                         LogImpact.Low,
-                        $"{ToPrettyString(args.Actor):player} queued {count} {GetRecipeName(recipe)} at {ToPrettyString(uid):lathe}");
+                        $"{ToPrettyString(args.Actor):player} queued {args.Quantity} {GetRecipeName(recipe)} at {ToPrettyString(uid):lathe}");
                 }
             }
             TryStartProducing(uid, component);
@@ -445,6 +479,92 @@ namespace Content.Server.Lathe
         {
             UpdateUserInterfaceState(uid, component);
         }
+
+        /// <summary>
+        /// Removes a batch from the batch queue by index.
+        /// If the index given does not exist or is outside of the bounds of the lathe's batch queue, nothing happens.
+        /// </summary>
+        /// <param name="uid">The lathe whose queue is being altered.</param>
+        /// <param name="component"></param>
+        /// <param name="args"></param>
+        public void OnLatheDeleteRequestMessage(EntityUid uid, LatheComponent component, ref LatheDeleteRequestMessage args)
+        {
+            if (args.Index < 0 || args.Index >= component.Queue.Count)
+                return;
+
+            var node = component.Queue.First;
+            for (int i = 0; i < args.Index; i++)
+                node = node?.Next;
+
+            if (node == null) // Shouldn't happen with checks above.
+                return;
+
+            var batch = node.Value;
+            _adminLogger.Add(LogType.Action,
+                LogImpact.Low,
+                $"{ToPrettyString(args.Actor):player} deleted a lathe job for ({batch.ItemsPrinted}/{batch.ItemsRequested}) {GetRecipeName(batch.Recipe)} at {ToPrettyString(uid):lathe}");
+
+            component.Queue.Remove(node);
+            UpdateUserInterfaceState(uid, component);
+        }
+
+        public void OnLatheMoveRequestMessage(EntityUid uid, LatheComponent component, ref LatheMoveRequestMessage args)
+        {
+            if (args.Change == 0 || args.Index < 0 || args.Index >= component.Queue.Count)
+                return;
+
+            // New index must be within the bounds of the batch.
+            var newIndex = args.Index + args.Change;
+            if (newIndex < 0 || newIndex >= component.Queue.Count)
+                return;
+
+            var node = component.Queue.First;
+            for (int i = 0; i < args.Index; i++)
+                node = node?.Next;
+
+            if (node == null) // Something went wrong.
+                return;
+
+            if (args.Change > 0)
+            {
+                var newRelativeNode = node.Next;
+                for (int i = 1; i < args.Change; i++) // 1-indexed: starting from Next
+                    newRelativeNode = newRelativeNode?.Next;
+
+                if (newRelativeNode == null) // Something went wrong.
+                    return;
+
+                component.Queue.Remove(node);
+                component.Queue.AddAfter(newRelativeNode, node);
+            }
+            else
+            {
+                var newRelativeNode = node.Previous;
+                for (int i = 1; i < -args.Change; i++) // 1-indexed: starting from Previous
+                    newRelativeNode = newRelativeNode?.Previous;
+
+                if (newRelativeNode == null) // Something went wrong.
+                    return;
+
+                component.Queue.Remove(node);
+                component.Queue.AddBefore(newRelativeNode, node);
+            }
+
+            UpdateUserInterfaceState(uid, component);
+        }
+
+        public void OnLatheAbortFabricationMessage(EntityUid uid, LatheComponent component, ref LatheAbortFabricationMessage args)
+        {
+            if (component.CurrentRecipe == null)
+                return;
+
+            _adminLogger.Add(LogType.Action,
+                LogImpact.Low,
+                $"{ToPrettyString(args.Actor):player} aborted printing {GetRecipeName(component.CurrentRecipe.Value)} at {ToPrettyString(uid):lathe}");
+
+            component.CurrentRecipe = null;
+            FinishProducing(uid, component);
+        }
         #endregion
     }
 }
index 8b701ff64eaa121a86d031d3cf6e1a458df8bcbf..7bd776451454fc6f3c3e438d865856ab530506c0 100644 (file)
@@ -26,10 +26,14 @@ namespace Content.Shared.Lathe
         // Otherwise the material arbitrage test and/or LatheSystem.GetAllBaseRecipes needs to be updated
 
         /// <summary>
-        /// The lathe's construction queue
+        /// The lathe's construction queue.
         /// </summary>
+        /// <remarks>
+        /// This is a LinkedList to allow for constant time insertion/deletion (vs a List), and more efficient
+        /// moves (vs a Queue).
+        /// </remarks>
         [DataField]
-        public Queue<ProtoId<LatheRecipePrototype>> Queue = new();
+        public LinkedList<LatheRecipeBatch> Queue = new();
 
         /// <summary>
         /// The sound that plays when the lathe is producing an item, if any
@@ -97,6 +101,21 @@ namespace Content.Shared.Lathe
         }
     }
 
+    [Serializable]
+    public sealed partial class LatheRecipeBatch
+    {
+        public ProtoId<LatheRecipePrototype> Recipe;
+        public int ItemsPrinted;
+        public int ItemsRequested;
+
+        public LatheRecipeBatch(ProtoId<LatheRecipePrototype> recipe, int itemsPrinted, int itemsRequested)
+        {
+            Recipe = recipe;
+            ItemsPrinted = itemsPrinted;
+            ItemsRequested = itemsRequested;
+        }
+    }
+
     /// <summary>
     /// Event raised on a lathe when it starts producing a recipe.
     /// </summary>
index 1c1c6440f118ea83d8aabd39b62223b66522487e..fe72eed367eefff5890aae6b40be2c63d6113266 100644 (file)
@@ -10,11 +10,11 @@ public sealed class LatheUpdateState : BoundUserInterfaceState
 {
     public List<ProtoId<LatheRecipePrototype>> Recipes;
 
-    public ProtoId<LatheRecipePrototype>[] Queue;
+    public LatheRecipeBatch[] Queue;
 
     public ProtoId<LatheRecipePrototype>? CurrentlyProducing;
 
-    public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, ProtoId<LatheRecipePrototype>[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
+    public LatheUpdateState(List<ProtoId<LatheRecipePrototype>> recipes, LatheRecipeBatch[] queue, ProtoId<LatheRecipePrototype>? currentlyProducing = null)
     {
         Recipes = recipes;
         Queue = queue;
@@ -46,6 +46,33 @@ public sealed class LatheQueueRecipeMessage : BoundUserInterfaceMessage
     }
 }
 
+/// <summary>
+///     Sent to the server to remove a batch from the queue.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class LatheDeleteRequestMessage(int index) : BoundUserInterfaceMessage
+{
+    public int Index = index;
+}
+
+/// <summary>
+///     Sent to the server to move the position of a batch in the queue.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class LatheMoveRequestMessage(int index, int change) : BoundUserInterfaceMessage
+{
+    public int Index = index;
+    public int Change = change;
+}
+
+/// <summary>
+///     Sent to the server to stop producing the current item.
+/// </summary>
+[Serializable, NetSerializable]
+public sealed class LatheAbortFabricationMessage() : BoundUserInterfaceMessage
+{
+}
+
 [NetSerializable, Serializable]
 public enum LatheUiKey
 {
diff --git a/Content.Shared/Lathe/PrototypeIdLinkedListSerializer.cs b/Content.Shared/Lathe/PrototypeIdLinkedListSerializer.cs
new file mode 100644 (file)
index 0000000..1c616ff
--- /dev/null
@@ -0,0 +1,81 @@
+using Robust.Shared.Serialization;
+using Robust.Shared.Serialization.Manager;
+using Robust.Shared.Serialization.Markdown;
+using Robust.Shared.Serialization.Markdown.Sequence;
+using Robust.Shared.Serialization.Markdown.Validation;
+using Robust.Shared.Serialization.TypeSerializers.Interfaces;
+
+namespace Content.Shared.Lathe;
+
+/// <summary>
+/// Handles reading, writing, and validation for linked lists of prototypes.
+/// </summary>
+/// <typeparam name="T">The type of prototype this linked list represents</typeparam>
+/// <remarks>
+/// This is in the Content.Shared.Lathe namespace as there are no other LinkedList ProtoId instances.
+/// </remarks>
+[TypeSerializer]
+public sealed class LinkedListSerializer<T> : ITypeSerializer<LinkedList<T>, SequenceDataNode>, ITypeCopier<LinkedList<T>> where T : class
+{
+    public ValidationNode Validate(ISerializationManager serializationManager, SequenceDataNode node,
+        IDependencyCollection dependencies, ISerializationContext? context = null)
+    {
+        var list = new List<ValidationNode>();
+
+        foreach (var elem in node.Sequence)
+        {
+            list.Add(serializationManager.ValidateNode<T>(elem, context));
+        }
+
+        return new ValidatedSequenceNode(list);
+    }
+
+    public DataNode Write(ISerializationManager serializationManager, LinkedList<T> value,
+        IDependencyCollection dependencies,
+        bool alwaysWrite = false,
+        ISerializationContext? context = null)
+    {
+        var sequence = new SequenceDataNode();
+
+        foreach (var elem in value)
+        {
+            sequence.Add(serializationManager.WriteValue(elem, alwaysWrite, context));
+        }
+
+        return sequence;
+    }
+
+    LinkedList<T> ITypeReader<LinkedList<T>, SequenceDataNode>.Read(ISerializationManager serializationManager,
+        SequenceDataNode node,
+        IDependencyCollection dependencies,
+        SerializationHookContext hookCtx,
+        ISerializationContext? context, ISerializationManager.InstantiationDelegate<LinkedList<T>>? instanceProvider)
+    {
+        var list = instanceProvider != null ? instanceProvider() : new LinkedList<T>();
+
+        foreach (var dataNode in node.Sequence)
+        {
+            list.AddLast(serializationManager.Read<T>(dataNode, hookCtx, context));
+        }
+
+        return list;
+    }
+
+    public void CopyTo(
+        ISerializationManager serializationManager,
+        LinkedList<T> source,
+        ref LinkedList<T> target,
+        IDependencyCollection dependencies,
+        SerializationHookContext hookCtx,
+        ISerializationContext? context = null)
+    {
+        target.Clear();
+        using var enumerator = source.GetEnumerator();
+
+        while (enumerator.MoveNext())
+        {
+            var current = enumerator.Current;
+            target.AddLast(current);
+        }
+    }
+}
index 524d83fd8480fc8f672b9f7f9eb78372ae60cd46..5942f4bf6c812ad4bcd8055738e22632675eb566 100644 (file)
@@ -22,6 +22,7 @@ public abstract class SharedLatheSystem : EntitySystem
     [Dependency] private readonly EmagSystem _emag = default!;
 
     public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
+    public const int MaxItemsPerRequest = 10_000;
 
     public override void Initialize()
     {
@@ -86,6 +87,8 @@ public abstract class SharedLatheSystem : EntitySystem
             return false;
         if (!HasRecipe(uid, recipe, component))
             return false;
+        if (amount <= 0)
+            return false;
 
         foreach (var (material, needed) in recipe.Materials)
         {
index 076a70447cbf131093f9199960a5132bb9169958..c04c09516266d154cec8fd7f341dc13a1dcbea35 100644 (file)
@@ -29,3 +29,9 @@ lathe-menu-silo-linked-message = Silo Linked
 lathe-menu-fabricating-message = Fabricating...
 lathe-menu-materials-title = Materials
 lathe-menu-queue-title = Build Queue
+lathe-menu-delete-fabricating-tooltip = Cancel printing the current item.
+lathe-menu-delete-item-tooltip = Cancel printing this batch.
+lathe-menu-move-up-tooltip = Move this batch ahead in the queue.
+lathe-menu-move-down-tooltip = Move this batch back in the queue.
+lathe-menu-item-single = {$index}. {$name}
+lathe-menu-item-batch = {$index}. {$name} ({$printed}/{$total})