{
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)
<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">
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();
};
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)
/// 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++;
}
--- /dev/null
+<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>
--- /dev/null
+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);
+ };
+ }
+}
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);
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;
}
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;
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);
{
if (!args.Powered)
{
- RemComp<LatheProducingComponent>(uid);
- UpdateRunningAppearance(uid, false);
+ AbortProduction(uid);
}
- else if (component.CurrentRecipe != null)
+ else
{
- EnsureComp<LatheProducingComponent>(uid);
TryStartProducing(uid, component);
}
}
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);
{
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
}
}
// 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
}
}
+ [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>
{
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;
}
}
+/// <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
{
--- /dev/null
+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);
+ }
+ }
+}
[Dependency] private readonly EmagSystem _emag = default!;
public readonly Dictionary<string, List<LatheRecipePrototype>> InverseRecipes = new();
+ public const int MaxItemsPerRequest = 10_000;
public override void Initialize()
{
return false;
if (!HasRecipe(uid, recipe, component))
return false;
+ if (amount <= 0)
+ return false;
foreach (var (material, needed) in recipe.Materials)
{
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})