Predict reagent grinder (#42815)

* prediction!!!

* fixes

* fixes and add debug logs

* fix prediction and cleanup

* raise event on client + comp access

* reapply power state changes

* Update Content.Client/Kitchen/EntitySystems/ReagentGrinderSystem.cs

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>

* Update Content.Shared/Kitchen/Components/ReagentGrinderComponent.cs

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>

* comments and prefilled grinder protos

* t

---------

Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
Co-authored-by: ScarKy0 <scarky0@onet.eu>
This commit is contained in:
slarticodefast 2026-03-15 20:15:36 +01:00 • committed by GitHub
parent 651b70f13a
commit 2736c458c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 697 additions and 640 deletions

View file

@ -116,7 +116,7 @@ public sealed class ChemistryGuideDataSystem : SharedChemistryGuideDataSystem
}
if (extractableComponent.GrindableSolution is { } grindableSolutionId &&
if (extractableComponent.GrindableSolutionName is { } grindableSolutionId &&
entProto.TryGetComponent<SolutionContainerManagerComponent>(out var manager, EntityManager.ComponentFactory) &&
_solutionContainer.TryGetSolution(manager, grindableSolutionId, out var grindableSolution))
{

View file

@ -1,7 +1,28 @@
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using JetBrains.Annotations;
namespace Content.Client.Kitchen.EntitySystems;
[UsedImplicitly]
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem
{
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ReagentGrinderComponent, AfterAutoHandleStateEvent>(OnAfterAutoHandleState);
}
private void OnAfterAutoHandleState(Entity<ReagentGrinderComponent> ent, ref AfterAutoHandleStateEvent args)
{
UpdateUi(ent);
}
public override void UpdateUi(EntityUid uid)
{
if (_userInterface.TryGetOpenUi(uid, ReagentGrinderUiKey.Key, out var bui))
bui.Update();
}
}

View file

@ -5,7 +5,7 @@
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" VerticalAlignment="Top" Margin="8" MinWidth="100">
<Label Text="{Loc grinder-menu-auto-label}" HorizontalAlignment="Center"/>
<Button Name="AutoModeButton" Text="{Loc grinder-menu-auto-button}" TextAlign="Center" MinSize="64 48" Margin="0 0 0 16" />
<Button Name="AutoModeButton" Text="{Loc grinder-menu-auto-button-off}" TextAlign="Center" MinSize="64 48" Margin="0 0 0 16" />
<Label Text="{Loc grinder-menu-manual-label}" HorizontalAlignment="Center"/>
<Button Name="GrindButton" Text="{Loc grinder-menu-grind-button}" TextAlign="Center" MinSize="64 48" Margin="0 0 0 16" />
<Button Name="JuiceButton" Text="{Loc grinder-menu-juice-button}" TextAlign="Center" MinSize="64 48" />

View file

@ -1,136 +1,127 @@
using System.Linq;
using Content.Client.UserInterface.Controls;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Reagent;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Power.EntitySystems;
using Robust.Client.AutoGenerated;
using Robust.Client.GameObjects;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
using Robust.Shared.Prototypes;
namespace Content.Client.Kitchen.UI
namespace Content.Client.Kitchen.UI;
[GenerateTypedNameReferences]
public sealed partial class GrinderMenu : FancyWindow
{
[GenerateTypedNameReferences]
public sealed partial class GrinderMenu : FancyWindow
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
private readonly ItemSlotsSystem _slots = default!;
private readonly SharedPowerReceiverSystem _power = default!;
private readonly SharedReagentGrinderSystem _grinder = default!;
private readonly SharedSolutionContainerSystem _solutionContainer = default!;
private readonly Dictionary<int, EntityUid> _chamberVisualContents = new();
private EntityUid _owner;
public event Action? OnToggleAuto;
public event Action? OnGrind;
public event Action? OnJuice;
public event Action? OnEjectAll;
public event Action? OnEjectBeaker;
public event Action<EntityUid>? OnEjectChamber;
public GrinderMenu()
{
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
_slots = _entityManager.System<ItemSlotsSystem>();
_power = _entityManager.System<SharedPowerReceiverSystem>();
_grinder = _entityManager.System<SharedReagentGrinderSystem>();
_solutionContainer = _entityManager.System<SharedSolutionContainerSystem>();
private readonly Dictionary<int, EntityUid> _chamberVisualContents = new();
AutoModeButton.OnPressed += _ => OnToggleAuto?.Invoke();
GrindButton.OnPressed += _ => OnGrind?.Invoke();
JuiceButton.OnPressed += _ => OnJuice?.Invoke();
ChamberContentBox.EjectButton.OnPressed += _ => OnEjectAll?.Invoke();
BeakerContentBox.EjectButton.OnPressed += _ => OnEjectBeaker?.Invoke();
ChamberContentBox.BoxContents.OnItemSelected += OnChamberBoxContentsItemSelected;
BeakerContentBox.BoxContents.SelectMode = ItemList.ItemListSelectMode.None;
}
public event Action? OnToggleAuto;
public event Action? OnGrind;
public event Action? OnJuice;
public event Action? OnEjectAll;
public event Action? OnEjectBeaker;
public event Action<EntityUid>? OnEjectChamber;
private void OnChamberBoxContentsItemSelected(ItemList.ItemListSelectedEventArgs args)
{
OnEjectChamber?.Invoke(_chamberVisualContents[args.ItemIndex]);
}
public GrinderMenu()
public void SetEntity(EntityUid owner)
{
_owner = owner;
UpdateUi();
}
public void UpdateUi()
{
if (!_entityManager.TryGetComponent<ReagentGrinderComponent>(_owner, out var grinderComp))
return;
var active = _grinder.IsActive((_owner, grinderComp));
var beaker = _slots.GetItemOrNull(_owner, ReagentGrinderComponent.BeakerSlotId);
var powered = _power.IsPowered(_owner);
var hasInput = grinderComp.InputContainer.ContainedEntities.Any();
var canGrind = hasInput && grinderComp.InputContainer.ContainedEntities.All(x => _grinder.CanGrind(x));
var canJuice = hasInput && grinderComp.InputContainer.ContainedEntities.All(x => _grinder.CanJuice(x));
BeakerContentBox.EjectButton.Disabled = active || !beaker.HasValue;
ChamberContentBox.EjectButton.Disabled = active || !hasInput;
GrindButton.Disabled = active || !canGrind || !powered || !beaker.HasValue;
JuiceButton.Disabled = active || !canJuice || !powered || !beaker.HasValue;
GrindButton.Modulate = grinderComp.Program == GrinderProgram.Grind ? Color.Green : Color.White;
JuiceButton.Modulate = grinderComp.Program == GrinderProgram.Juice ? Color.Green : Color.White;
AutoModeButton.Text = grinderComp.AutoMode switch
{
RobustXamlLoader.Load(this);
IoCManager.InjectDependencies(this);
AutoModeButton.OnPressed += _ => OnToggleAuto?.Invoke();
GrindButton.OnPressed += _ => OnGrind?.Invoke();
JuiceButton.OnPressed += _ => OnJuice?.Invoke();
ChamberContentBox.EjectButton.OnPressed += _ => OnEjectAll?.Invoke();
BeakerContentBox.EjectButton.OnPressed += _ => OnEjectBeaker?.Invoke();
ChamberContentBox.BoxContents.OnItemSelected += OnChamberBoxContentsItemSelected;
BeakerContentBox.BoxContents.SelectMode = ItemList.ItemListSelectMode.None;
GrinderAutoMode.Grind => Loc.GetString("grinder-menu-grind-button"),
GrinderAutoMode.Juice => Loc.GetString("grinder-menu-juice-button"),
_ => Loc.GetString("grinder-menu-auto-button-off"),
};
//Refresh chamber contents
_chamberVisualContents.Clear();
ChamberContentBox.BoxContents.Clear();
foreach (var entity in grinderComp.InputContainer.ContainedEntities)
{
// TODO: Replace with SpriteView
var texture = _entityManager.GetComponent<SpriteComponent>(entity).Icon?.Default;
var solidItem = ChamberContentBox.BoxContents.AddItem(_entityManager.GetComponent<MetaDataComponent>(entity).EntityName, texture);
var solidIndex = ChamberContentBox.BoxContents.IndexOf(solidItem);
_chamberVisualContents.Add(solidIndex, entity);
}
private void OnChamberBoxContentsItemSelected(ItemList.ItemListSelectedEventArgs args)
//Refresh beaker contents
BeakerContentBox.BoxContents.Clear();
//if no beaker is attached use this guard to prevent hitting a null reference.
if (!beaker.HasValue || !_solutionContainer.TryGetFitsInDispenser(beaker.Value, out _, out var reagents))
return;
//Looks like we have a beaker attached.
if (!reagents.Any())
{
OnEjectChamber?.Invoke(_chamberVisualContents[args.ItemIndex]);
BeakerContentBox.BoxContents.AddItem(Loc.GetString("grinder-menu-beaker-content-box-is-empty"));
}
public void UpdateState(ReagentGrinderInterfaceState state)
else
{
BeakerContentBox.EjectButton.Disabled = !state.HasBeakerIn;
ChamberContentBox.EjectButton.Disabled = state.ChamberContents.Length <= 0;
GrindButton.Disabled = !state.CanGrind || !state.Powered;
JuiceButton.Disabled = !state.CanJuice || !state.Powered;
switch (state.AutoMode)
foreach (var (reagent, quantity) in reagents)
{
case GrinderAutoMode.Grind:
AutoModeButton.Text = Loc.GetString("grinder-menu-grind-button");
break;
case GrinderAutoMode.Juice:
AutoModeButton.Text = Loc.GetString("grinder-menu-juice-button");
break;
default:
AutoModeButton.Text = Loc.GetString("grinder-menu-auto-button-off");
break;
}
// TODO move this to a component state and ensure the net ids.
RefreshContentsDisplay(state.ReagentQuantities, _entityManager.GetEntityArray(state.ChamberContents), state.HasBeakerIn);
}
public void HandleMessage(BoundUserInterfaceMessage message)
{
switch (message)
{
case ReagentGrinderWorkStartedMessage workStarted:
GrindButton.Disabled = true;
GrindButton.Modulate = workStarted.GrinderProgram == GrinderProgram.Grind ? Color.Green : Color.White;
JuiceButton.Disabled = true;
JuiceButton.Modulate = workStarted.GrinderProgram == GrinderProgram.Juice ? Color.Green : Color.White;
BeakerContentBox.EjectButton.Disabled = true;
ChamberContentBox.EjectButton.Disabled = true;
break;
case ReagentGrinderWorkCompleteMessage:
GrindButton.Disabled = false;
JuiceButton.Disabled = false;
GrindButton.Modulate = Color.White;
JuiceButton.Modulate = Color.White;
BeakerContentBox.EjectButton.Disabled = false;
ChamberContentBox.EjectButton.Disabled = false;
break;
}
}
private void RefreshContentsDisplay(IList<ReagentQuantity>? reagents, IReadOnlyList<EntityUid> containedSolids, bool isBeakerAttached)
{
//Refresh chamber contents
_chamberVisualContents.Clear();
ChamberContentBox.BoxContents.Clear();
foreach (var entity in containedSolids)
{
if (!_entityManager.EntityExists(entity))
{
return;
}
var texture = _entityManager.GetComponent<SpriteComponent>(entity).Icon?.Default;
var solidItem = ChamberContentBox.BoxContents.AddItem(_entityManager.GetComponent<MetaDataComponent>(entity).EntityName, texture);
var solidIndex = ChamberContentBox.BoxContents.IndexOf(solidItem);
_chamberVisualContents.Add(solidIndex, entity);
}
//Refresh beaker contents
BeakerContentBox.BoxContents.Clear();
//if no beaker is attached use this guard to prevent hitting a null reference.
if (!isBeakerAttached || reagents == null)
{
return;
}
//Looks like we have a beaker attached.
if (reagents.Count <= 0)
{
BeakerContentBox.BoxContents.AddItem(Loc.GetString("grinder-menu-beaker-content-box-is-empty"));
}
else
{
foreach (var (reagent, quantity) in reagents)
{
var reagentName = _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto)
? Loc.GetString($"{quantity} {proto.LocalizedName}")
: "???";
BeakerContentBox.BoxContents.AddItem(reagentName);
}
var reagentName = _prototypeManager.TryIndex(reagent.Prototype, out ReagentPrototype? proto)
? $"{quantity} {proto.LocalizedName}"
: "???";
BeakerContentBox.BoxContents.AddItem(reagentName);
}
}
}

View file

@ -1,77 +1,63 @@
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Kitchen;
using Robust.Client.GameObjects;
using Content.Shared.Kitchen.Components;
using Robust.Client.UserInterface;
using Robust.Client.UserInterface.Controls;
using Robust.Shared.Prototypes;
namespace Content.Client.Kitchen.UI
namespace Content.Client.Kitchen.UI;
public sealed class ReagentGrinderBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
{
public sealed class ReagentGrinderBoundUserInterface : BoundUserInterface
[ViewVariables]
private GrinderMenu? _menu;
protected override void Open()
{
[ViewVariables]
private GrinderMenu? _menu;
base.Open();
public ReagentGrinderBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
{
}
_menu = this.CreateWindow<GrinderMenu>();
_menu.SetEntity(Owner);
_menu.OnToggleAuto += ToggleAutoMode;
_menu.OnGrind += StartGrinding;
_menu.OnJuice += StartJuicing;
_menu.OnEjectAll += EjectAll;
_menu.OnEjectBeaker += EjectBeaker;
_menu.OnEjectChamber += EjectChamberContent;
}
protected override void Open()
{
base.Open();
public override void Update()
{
base.Update();
_menu = this.CreateWindow<GrinderMenu>();
_menu.OnToggleAuto += ToggleAutoMode;
_menu.OnGrind += StartGrinding;
_menu.OnJuice += StartJuicing;
_menu.OnEjectAll += EjectAll;
_menu.OnEjectBeaker += EjectBeaker;
_menu.OnEjectChamber += EjectChamberContent;
}
_menu?.UpdateUi();
}
protected override void UpdateState(BoundUserInterfaceState state)
{
base.UpdateState(state);
if (state is not ReagentGrinderInterfaceState cState)
return;
public void ToggleAutoMode()
{
SendPredictedMessage(new ReagentGrinderToggleAutoModeMessage());
}
_menu?.UpdateState(cState);
}
public void StartGrinding()
{
SendPredictedMessage(new ReagentGrinderStartMessage(GrinderProgram.Grind));
}
protected override void ReceiveMessage(BoundUserInterfaceMessage message)
{
base.ReceiveMessage(message);
_menu?.HandleMessage(message);
}
public void StartJuicing()
{
SendPredictedMessage(new ReagentGrinderStartMessage(GrinderProgram.Juice));
}
public void ToggleAutoMode()
{
SendMessage(new ReagentGrinderToggleAutoModeMessage());
}
public void EjectAll()
{
SendPredictedMessage(new ReagentGrinderEjectChamberAllMessage());
}
public void StartGrinding()
{
SendMessage(new ReagentGrinderStartMessage(GrinderProgram.Grind));
}
public void EjectBeaker()
{
SendPredictedMessage(new ItemSlotButtonPressedEvent(ReagentGrinderComponent.BeakerSlotId));
}
public void StartJuicing()
{
SendMessage(new ReagentGrinderStartMessage(GrinderProgram.Juice));
}
public void EjectAll()
{
SendMessage(new ReagentGrinderEjectChamberAllMessage());
}
public void EjectBeaker()
{
SendMessage(new ItemSlotButtonPressedEvent(SharedReagentGrinder.BeakerSlotId));
}
public void EjectChamberContent(EntityUid uid)
{
SendMessage(new ReagentGrinderEjectChamberContentMessage(EntMan.GetNetEntity(uid)));
}
public void EjectChamberContent(EntityUid uid)
{
SendPredictedMessage(new ReagentGrinderEjectChamberContentMessage(EntMan.GetNetEntity(uid)));
}
}

View file

@ -1,322 +1,5 @@
using Content.Server.Power.EntitySystems;
using Content.Server.Stack;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Kitchen;
using Content.Shared.Kitchen.Components;
using Content.Shared.Popups;
using Content.Shared.Random;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Server.GameObjects;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Timing;
using System.Linq;
using Content.Server.Jittering;
using Content.Shared.Jittering;
using Content.Shared.Kitchen.EntitySystems;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
namespace Content.Server.Kitchen.EntitySystems
{
[UsedImplicitly]
internal sealed class ReagentGrinderSystem : SharedReagentGrinderSystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainersSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!;
[Dependency] private readonly StackSystem _stackSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDestructibleSystem _destructible = default!;
[Dependency] private readonly RandomHelperSystem _randomHelper = default!;
[Dependency] private readonly JitteringSystem _jitter = default!;
[Dependency] private readonly SharedPowerStateSystem _powerState = default!;
namespace Content.Server.Kitchen.EntitySystems;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<ActiveReagentGrinderComponent, ComponentStartup>(OnActiveGrinderStart);
SubscribeLocalEvent<ActiveReagentGrinderComponent, ComponentRemove>(OnActiveGrinderRemove);
SubscribeLocalEvent<ReagentGrinderComponent, ComponentStartup>((uid, _, _) => UpdateUiState(uid));
SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUiState(uid));
SubscribeLocalEvent<ReagentGrinderComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ReagentGrinderComponent, EntInsertedIntoContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ReagentGrinderComponent, EntRemovedFromContainerMessage>(OnContainerModified);
SubscribeLocalEvent<ReagentGrinderComponent, ContainerIsRemovingAttemptEvent>(OnEntRemoveAttempt);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderToggleAutoModeMessage>(OnToggleAutoModeMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderStartMessage>(OnStartMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderEjectChamberAllMessage>(OnEjectChamberAllMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderEjectChamberContentMessage>(OnEjectChamberContentMessage);
}
private void OnToggleAutoModeMessage(Entity<ReagentGrinderComponent> entity, ref ReagentGrinderToggleAutoModeMessage message)
{
entity.Comp.AutoMode = (GrinderAutoMode) (((byte) entity.Comp.AutoMode + 1) % Enum.GetValues(typeof(GrinderAutoMode)).Length);
Dirty(entity);
UpdateUiState(entity);
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var query = EntityQueryEnumerator<ActiveReagentGrinderComponent, ReagentGrinderComponent>();
while (query.MoveNext(out var uid, out var active, out var reagentGrinder))
{
if (active.EndTime > _timing.CurTime)
continue;
reagentGrinder.AudioStream = _audioSystem.Stop(reagentGrinder.AudioStream);
RemCompDeferred<ActiveReagentGrinderComponent>(uid);
var inputContainer = _containerSystem.EnsureContainer<Container>(uid, SharedReagentGrinder.InputContainerId);
var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId);
if (outputContainer is null || !_solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out var containerSoln, out var containerSolution))
continue;
foreach (var item in inputContainer.ContainedEntities.ToList())
{
var solution = GetGrinderSolution(item, active.Program);
if (solution is null)
continue;
if (TryComp<StackComponent>(item, out var stack))
{
var totalVolume = solution.Volume * stack.Count;
if (totalVolume <= 0)
continue;
// Maximum number of items we can process in the stack without going over AvailableVolume
// We add a small tolerance, because floats are inaccurate.
var fitsCount = (int) (stack.Count * FixedPoint2.Min(containerSolution.AvailableVolume / totalVolume + 0.01, 1));
if (fitsCount <= 0)
continue;
// Make a copy of the solution to scale
// Otherwise we'll actually change the volume of the remaining stack too
var scaledSolution = new Solution(solution);
scaledSolution.ScaleSolution(fitsCount);
solution = scaledSolution;
_stackSystem.ReduceCount((item, stack), fitsCount); // Setting to 0 will QueueDel
}
else
{
if (solution.Volume > containerSolution.AvailableVolume)
continue;
_destructible.DestroyEntity(item);
}
_solutionContainersSystem.TryAddSolution(containerSoln.Value, solution);
}
_userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key,
new ReagentGrinderWorkCompleteMessage());
UpdateUiState(uid);
}
}
private void OnActiveGrinderStart(Entity<ActiveReagentGrinderComponent> ent, ref ComponentStartup args)
{
_jitter.AddJitter(ent, -10, 100);
// Not all grinders need power.
_powerState.TrySetWorkingState(ent.Owner, true);
}
private void OnActiveGrinderRemove(Entity<ActiveReagentGrinderComponent> ent, ref ComponentRemove args)
{
RemComp<JitteringComponent>(ent);
_powerState.TrySetWorkingState(ent.Owner, false);
}
private void OnEntRemoveAttempt(Entity<ReagentGrinderComponent> entity, ref ContainerIsRemovingAttemptEvent args)
{
if (HasComp<ActiveReagentGrinderComponent>(entity))
args.Cancel();
}
private void OnContainerModified(EntityUid uid, ReagentGrinderComponent reagentGrinder, ContainerModifiedMessage args)
{
UpdateUiState(uid);
var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId);
_appearanceSystem.SetData(uid, ReagentGrinderVisualState.BeakerAttached, outputContainer.HasValue);
if (reagentGrinder.AutoMode != GrinderAutoMode.Off && !HasComp<ActiveReagentGrinderComponent>(uid) && this.IsPowered(uid, EntityManager))
{
var program = reagentGrinder.AutoMode == GrinderAutoMode.Grind ? GrinderProgram.Grind : GrinderProgram.Juice;
DoWork(uid, reagentGrinder, program);
}
}
private void OnInteractUsing(Entity<ReagentGrinderComponent> entity, ref InteractUsingEvent args)
{
var heldEnt = args.Used;
var inputContainer = _containerSystem.EnsureContainer<Container>(entity.Owner, SharedReagentGrinder.InputContainerId);
if (!HasComp<ExtractableComponent>(heldEnt))
{
if (!HasComp<FitsInDispenserComponent>(heldEnt))
{
// This is ugly but we can't use whitelistFailPopup because there are 2 containers with different whitelists.
_popupSystem.PopupEntity(Loc.GetString("reagent-grinder-component-cannot-put-entity-message"), entity.Owner, args.User);
}
// Entity did NOT pass the whitelist for grind/juice.
// Wouldn't want the clown grinding up the Captain's ID card now would you?
// Why am I asking you? You're biased.
return;
}
if (args.Handled)
return;
// Cap the chamber. Don't want someone putting in 500 entities and ejecting them all at once.
// Maybe I should have done that for the microwave too?
if (inputContainer.ContainedEntities.Count >= entity.Comp.StorageMaxEntities)
return;
if (!_containerSystem.Insert(heldEnt, inputContainer))
return;
args.Handled = true;
}
private void UpdateUiState(EntityUid uid)
{
ReagentGrinderComponent? grinderComp = null;
if (!Resolve(uid, ref grinderComp))
return;
var inputContainer = _containerSystem.EnsureContainer<Container>(uid, SharedReagentGrinder.InputContainerId);
var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId);
Solution? containerSolution = null;
var isBusy = HasComp<ActiveReagentGrinderComponent>(uid);
var canJuice = false;
var canGrind = false;
if (outputContainer is not null
&& _solutionContainersSystem.TryGetFitsInDispenser(outputContainer.Value, out _, out containerSolution)
&& inputContainer.ContainedEntities.Count > 0)
{
canGrind = inputContainer.ContainedEntities.All(x => CanGrind(x));
canJuice = inputContainer.ContainedEntities.All(x => CanJuice(x));
}
var state = new ReagentGrinderInterfaceState(
isBusy,
outputContainer.HasValue,
this.IsPowered(uid, EntityManager),
canJuice,
canGrind,
grinderComp.AutoMode,
GetNetEntityArray(inputContainer.ContainedEntities.ToArray()),
containerSolution?.Contents.ToArray()
);
_userInterfaceSystem.SetUiState(uid, ReagentGrinderUiKey.Key, state);
}
private void OnStartMessage(Entity<ReagentGrinderComponent> entity, ref ReagentGrinderStartMessage message)
{
if (!this.IsPowered(entity.Owner, EntityManager) || HasComp<ActiveReagentGrinderComponent>(entity))
return;
DoWork(entity.Owner, entity.Comp, message.Program);
}
private void OnEjectChamberAllMessage(Entity<ReagentGrinderComponent> entity, ref ReagentGrinderEjectChamberAllMessage message)
{
var inputContainer = _containerSystem.EnsureContainer<Container>(entity.Owner, SharedReagentGrinder.InputContainerId);
if (HasComp<ActiveReagentGrinderComponent>(entity) || inputContainer.ContainedEntities.Count <= 0)
return;
ClickSound(entity);
foreach (var toEject in inputContainer.ContainedEntities.ToList())
{
_containerSystem.Remove(toEject, inputContainer);
_randomHelper.RandomOffset(toEject, 0.4f);
}
UpdateUiState(entity);
}
private void OnEjectChamberContentMessage(Entity<ReagentGrinderComponent> entity, ref ReagentGrinderEjectChamberContentMessage message)
{
if (HasComp<ActiveReagentGrinderComponent>(entity))
return;
var inputContainer = _containerSystem.EnsureContainer<Container>(entity.Owner, SharedReagentGrinder.InputContainerId);
var ent = GetEntity(message.EntityId);
if (_containerSystem.Remove(ent, inputContainer))
{
_randomHelper.RandomOffset(ent, 0.4f);
ClickSound(entity);
UpdateUiState(entity);
}
}
/// <summary>
/// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker.
/// </summary>
/// <param name="uid">The grinder itself</param>
/// <param name="reagentGrinder"></param>
/// <param name="program">Which program, such as grind or juice</param>
private void DoWork(EntityUid uid, ReagentGrinderComponent reagentGrinder, GrinderProgram program)
{
var inputContainer = _containerSystem.EnsureContainer<Container>(uid, SharedReagentGrinder.InputContainerId);
var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId);
// Do we have anything to grind/juice and a container to put the reagents in?
if (inputContainer.ContainedEntities.Count <= 0 || !HasComp<FitsInDispenserComponent>(outputContainer))
return;
SoundSpecifier? sound;
switch (program)
{
case GrinderProgram.Grind when inputContainer.ContainedEntities.All(x => CanGrind(x)):
sound = reagentGrinder.GrindSound;
break;
case GrinderProgram.Juice when inputContainer.ContainedEntities.All(x => CanJuice(x)):
sound = reagentGrinder.JuiceSound;
break;
default:
return;
}
var active = AddComp<ActiveReagentGrinderComponent>(uid);
active.EndTime = _timing.CurTime + reagentGrinder.WorkTime * reagentGrinder.WorkTimeMultiplier;
active.Program = program;
reagentGrinder.AudioStream = _audioSystem.PlayPvs(sound, uid,
AudioParams.Default.WithPitchScale(1 / reagentGrinder.WorkTimeMultiplier))?.Entity; //slightly higher pitched
_userInterfaceSystem.ServerSendUiMessage(uid, ReagentGrinderUiKey.Key,
new ReagentGrinderWorkStartedMessage(program));
}
private void ClickSound(Entity<ReagentGrinderComponent> reagentGrinder)
{
_audioSystem.PlayPvs(reagentGrinder.Comp.ClickSound, reagentGrinder.Owner, AudioParams.Default.WithVolume(-2f));
}
}
}
public sealed class ReagentGrinderSystem : SharedReagentGrinderSystem;

View file

@ -41,7 +41,7 @@ public abstract partial class SharedSolutionContainerSystem
return false;
}
return TryGetSolution((entity.Owner, entity.Comp2), entity.Comp1.GrindableSolution, out soln, out solution);
return TryGetSolution((entity.Owner, entity.Comp2), entity.Comp1.GrindableSolutionName, out soln, out solution);
}
public bool TryGetDumpableSolution(Entity<DumpableSolutionComponent?, SolutionContainerManagerComponent?> entity, [NotNullWhen(true)] out Entity<SolutionComponent>? soln, [NotNullWhen(true)] out Solution? solution)

View file

@ -4,7 +4,7 @@ using Robust.Shared.Serialization;
namespace Content.Shared.Chemistry
{
/// <summary>
/// Send by the client when setting the transfer amount using the BUI.
/// Sent by the client when setting the transfer amount using the BUI.
/// </summary>
[Serializable, NetSerializable]
public sealed class TransferAmountSetValueMessage(FixedPoint2 value) : BoundUserInterfaceMessage

View file

@ -4,14 +4,22 @@ using Robust.Shared.GameStates;
namespace Content.Shared.Kitchen.Components;
/// <summary>
/// Tag component that denotes an entity as Extractable
/// This component allows an entity to be used in the reagent grinder to extract reagents.
/// <seealso cref="ReagentGrinderComponent"/>
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ExtractableComponent : Component
{
[DataField("juiceSolution")]
/// <summary>
/// The reagents to spawn when the grinder is set to juice mode.
/// This will delete the reagents in the GrindableSolution.
/// </summary>
[DataField]
public Solution? JuiceSolution;
[DataField("grindableSolutionName")]
public string? GrindableSolution;
/// <summary>
/// The reagents to transfer into the beaker when the grinder is set to grind mode.
/// </summary>
[DataField]
public string? GrindableSolutionName;
};

View file

@ -1,6 +1,8 @@
using Content.Shared.Kitchen.EntitySystems;
using Robust.Shared.Audio;
using Robust.Shared.Containers;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared.Kitchen.Components;
@ -10,44 +12,100 @@ namespace Content.Shared.Kitchen.Components;
/// converting something into its single juice form. E.g, grind an apple and get the nutriment and sugar
/// it contained, juice an apple and get "apple juice".
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState(raiseAfterAutoHandleState: true), AutoGenerateComponentPause]
[Access(typeof(SharedReagentGrinderSystem))]
public sealed partial class ReagentGrinderComponent : Component
{
/// <summary>
/// The container slot id for the beaker.
/// </summary>
public const string BeakerSlotId = "beakerSlot";
/// <summary>
/// The container id for the internal storage.
/// </summary>
public const string InputContainerId = "inputContainer";
/// <summary>
/// The cached container for the internal storage.
/// </summary>
[ViewVariables]
public Container InputContainer = default!;
/// <summary>
/// The amount of entities that fit into the container.
/// </summary>
[DataField, AutoNetworkedField]
public int StorageMaxEntities = 6;
/// <summary>
/// The time grinding or juicing takes.
/// Roughly matches the grind/juice sounds.
/// </summary>
[DataField]
public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5f);
/// <summary>
/// Multiplier for WorkTime, that pitches the audio accordingly.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan WorkTime = TimeSpan.FromSeconds(3.5); // Roughly matches the grind/juice sounds.
[DataField, AutoNetworkedField]
public float WorkTimeMultiplier = 1;
public float WorkTimeMultiplier = 1.0f;
/// <summary>
/// Sound played when pressing a button on the UI.
/// </summary>
[DataField]
public SoundSpecifier ClickSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg");
public SoundSpecifier ClickSound = new SoundPathSpecifier("/Audio/Machines/machine_switch.ogg", AudioParams.Default.WithVolume(-2f));
/// <summary>
/// Sound played when grinding.
/// </summary>
[DataField]
public SoundSpecifier GrindSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/blender.ogg");
public SoundSpecifier GrindSound = new SoundPathSpecifier("/Audio/Machines/blender.ogg");
/// <summary>
/// Sound played when juicing.
/// </summary>
[DataField]
public SoundSpecifier JuiceSound { get; set; } = new SoundPathSpecifier("/Audio/Machines/juicer.ogg");
public SoundSpecifier JuiceSound = new SoundPathSpecifier("/Audio/Machines/juicer.ogg");
/// <summary>
/// Grind automatically when inserting items?
/// </summary>
[DataField, AutoNetworkedField]
public GrinderAutoMode AutoMode = GrinderAutoMode.Off;
public EntityUid? AudioStream;
}
[RegisterComponent, NetworkedComponent]
[Access(typeof(SharedReagentGrinderSystem))]
public sealed partial class ActiveReagentGrinderComponent : Component
{
/// <summary>
/// Remaining time until the grinder finishes grinding/juicing.
/// The sound currently being played.
/// </summary>
[ViewVariables]
public TimeSpan EndTime;
[DataField]
public EntityUid? AudioStream;
[ViewVariables]
public GrinderProgram Program;
/// <summary>
/// The time the grinder will finish grinding/juicing.
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer))]
[AutoNetworkedField, AutoPausedField]
public TimeSpan? EndTime;
/// <summary>
/// The currently active program (if the grinder is working).
/// </summary>
[DataField, AutoNetworkedField]
public GrinderProgram? Program;
}
/// <summary>
/// Marker component for active reagent grinders used to improve the EntityQueryEnumerator performance in the update loop.
/// If you want to check if the grinder is currently active use <see cref="SharedReagentGrinderSystem.IsActive"/> instead,
/// because this component is being removed deferred, i.e. in the following game tick.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class ActiveReagentGrinderComponent : Component;
/// <summary>
/// Marker component added to beakers inserted into a reagent grinder.
/// Used to update the UI when the solution in the beaker changes.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class InsideReagentGrinderComponent : Component;

View file

@ -1,14 +1,346 @@
using System.Linq;
using Content.Shared.Chemistry.EntitySystems;
using Content.Shared.Chemistry.Components;
using Content.Shared.Containers.ItemSlots;
using Content.Shared.Destructible;
using Content.Shared.FixedPoint;
using Content.Shared.Interaction;
using Content.Shared.Jittering;
using Content.Shared.Kitchen.Components;
using Content.Shared.Popups;
using Content.Shared.Power;
using Content.Shared.Power.EntitySystems;
using Content.Shared.Stacks;
using JetBrains.Annotations;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Network;
using Robust.Shared.Timing;
namespace Content.Shared.Kitchen.EntitySystems;
[UsedImplicitly]
public abstract class SharedReagentGrinderSystem : EntitySystem
{
[Dependency] private readonly IGameTiming _timing = default!;
[Dependency] private readonly INetManager _net = default!;
[Dependency] private readonly SharedSolutionContainerSystem _solutionContainersSystem = default!;
[Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!;
[Dependency] private readonly SharedPopupSystem _popupSystem = default!;
[Dependency] private readonly SharedStackSystem _stackSystem = default!;
[Dependency] private readonly SharedAudioSystem _audioSystem = default!;
[Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
[Dependency] private readonly SharedContainerSystem _containerSystem = default!;
[Dependency] private readonly SharedDestructibleSystem _destructible = default!;
[Dependency] private readonly SharedJitteringSystem _jitter = default!;
[Dependency] private readonly SharedPowerReceiverSystem _power = default!;
[Dependency] private readonly SharedPowerStateSystem _powerState = default!;
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<InsideReagentGrinderComponent, SolutionContainerChangedEvent>(OnBeakerSolutionContainerChanged);
SubscribeLocalEvent<ReagentGrinderComponent, ComponentStartup>(OnGrinderStartup);
SubscribeLocalEvent<ReagentGrinderComponent, ContainerIsRemovingAttemptEvent>(OnEntRemovingAttempt);
SubscribeLocalEvent<ReagentGrinderComponent, EntRemovedFromContainerMessage>(OnEntRemoved);
SubscribeLocalEvent<ReagentGrinderComponent, EntInsertedIntoContainerMessage>(OnEntInserted);
SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUi(uid));
SubscribeLocalEvent<ReagentGrinderComponent, InteractUsingEvent>(OnInteractUsing);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderStartMessage>(OnStartMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderToggleAutoModeMessage>(OnToggleAutoModeMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderEjectChamberAllMessage>(OnEjectChamberAllMessage);
SubscribeLocalEvent<ReagentGrinderComponent, ReagentGrinderEjectChamberContentMessage>(OnEjectChamberContentMessage);
}
private void OnBeakerSolutionContainerChanged(Entity<InsideReagentGrinderComponent> ent, ref SolutionContainerChangedEvent args)
{
// Update the UI if the reagents inside the beaker are changed.
// This is needed in case the component state for the container is applied before that of the solution container
// or if the beaker somehow changes its contents on its own (for with example SolutionRegenerationComponent).
UpdateUi(Transform(ent).ParentUid);
}
private void OnGrinderStartup(Entity<ReagentGrinderComponent> ent, ref ComponentStartup args)
{
ent.Comp.InputContainer = _containerSystem.EnsureContainer<Container>(ent.Owner, ReagentGrinderComponent.InputContainerId);
}
private void OnEntRemovingAttempt(Entity<ReagentGrinderComponent> ent, ref ContainerIsRemovingAttemptEvent args)
{
// Allow server states to be applied without cancelling container changes.
if (_timing.ApplyingState)
return;
if (args.Container.ID != ReagentGrinderComponent.BeakerSlotId
&& args.Container.ID != ReagentGrinderComponent.InputContainerId)
return;
// Cannot remove items while the grinder is active.
if (IsActive(ent.AsNullable()))
args.Cancel();
}
public override void Update(float frameTime)
{
base.Update(frameTime);
var curTime = _timing.CurTime;
var query = EntityQueryEnumerator<ActiveReagentGrinderComponent, ReagentGrinderComponent>();
while (query.MoveNext(out var uid, out _, out var grinderComp))
{
if (grinderComp.EndTime == null || grinderComp.EndTime > curTime)
continue;
FinishGrinding((uid, grinderComp));
}
}
private void OnEntRemoved(EntityUid uid, ReagentGrinderComponent comp, EntRemovedFromContainerMessage args)
{
if (args.Container.ID != ReagentGrinderComponent.BeakerSlotId
&& args.Container.ID != ReagentGrinderComponent.InputContainerId)
return;
// Always update the UI on the client, both during prediction and when applying a game state.
UpdateUi(uid);
// The component changes from the code below are already part of the same game state being applied.
if (_timing.ApplyingState)
return;
if (args.Container.ID == ReagentGrinderComponent.BeakerSlotId) // Beaker removed.
{
RemComp<InsideReagentGrinderComponent>(args.Entity);
_appearanceSystem.SetData(uid, ReagentGrinderVisualState.BeakerAttached, false);
}
}
private void OnEntInserted(EntityUid uid, ReagentGrinderComponent comp, EntInsertedIntoContainerMessage args)
{
if (args.Container.ID != ReagentGrinderComponent.BeakerSlotId
&& args.Container.ID != ReagentGrinderComponent.InputContainerId)
return;
// Always update the UI on the client, both during prediction and when applying a game state.
UpdateUi(uid);
// The component changes from the code below are already part of the same game state being applied.
if (_timing.ApplyingState)
return;
if (args.Container.ID == ReagentGrinderComponent.BeakerSlotId) // Beaker inserted.
{
EnsureComp<InsideReagentGrinderComponent>(args.Entity);
_appearanceSystem.SetData(uid, ReagentGrinderVisualState.BeakerAttached, true);
}
// Start grinder when in auto mode.
if (comp.AutoMode != GrinderAutoMode.Off)
{
var program = comp.AutoMode == GrinderAutoMode.Grind ? GrinderProgram.Grind : GrinderProgram.Juice;
StartGrinder((uid, comp), program);
}
}
private void OnInteractUsing(Entity<ReagentGrinderComponent> ent, ref InteractUsingEvent args)
{
var heldEnt = args.Used;
if (!HasComp<ExtractableComponent>(heldEnt))
{
if (!HasComp<FitsInDispenserComponent>(heldEnt))
{
// This is ugly but we can't use whitelistFailPopup because there are 2 containers with different whitelists.
_popupSystem.PopupClient(Loc.GetString("reagent-grinder-component-cannot-put-entity-message"), ent.Owner, args.User);
}
// Entity did NOT pass the whitelist for grind/juice.
// Wouldn't want the clown grinding up the Captain's ID card now would you?
// Why am I asking you? You're biased.
return;
}
if (args.Handled)
return;
// Cap the chamber. Don't want someone putting in 500 entities and ejecting them all at once.
// Maybe I should have done that for the microwave too?
if (ent.Comp.InputContainer.ContainedEntities.Count >= ent.Comp.StorageMaxEntities)
return;
if (!_containerSystem.Insert(heldEnt, ent.Comp.InputContainer))
return;
args.Handled = true;
}
/// <summary>
/// Update the reagent grinder BUI for the client.
/// </summary>
public virtual void UpdateUi(EntityUid uid) { }
private void OnStartMessage(Entity<ReagentGrinderComponent> ent, ref ReagentGrinderStartMessage message)
{
StartGrinder(ent, message.Program);
}
private void OnToggleAutoModeMessage(Entity<ReagentGrinderComponent> ent, ref ReagentGrinderToggleAutoModeMessage message)
{
// Cycle through the enum values.
ent.Comp.AutoMode = (GrinderAutoMode)(((byte)ent.Comp.AutoMode + 1) % Enum.GetValues<GrinderAutoMode>().Length);
Dirty(ent);
UpdateUi(ent);
}
private void OnEjectChamberAllMessage(Entity<ReagentGrinderComponent> ent, ref ReagentGrinderEjectChamberAllMessage message)
{
if (IsActive(ent.AsNullable()) || ent.Comp.InputContainer.ContainedEntities.Count <= 0)
return;
_audioSystem.PlayPredicted(ent.Comp.ClickSound, ent.Owner, message.Actor);
_containerSystem.EmptyContainer(ent.Comp.InputContainer);
// UpdateUi is called in the resulting ContainerModifiedMessage.
}
private void OnEjectChamberContentMessage(Entity<ReagentGrinderComponent> ent, ref ReagentGrinderEjectChamberContentMessage message)
{
if (IsActive(ent.AsNullable()))
return;
if (!TryGetEntity(message.EntityId, out var toRemove))
return;
if (_containerSystem.Remove(toRemove.Value, ent.Comp.InputContainer))
{
_audioSystem.PlayPredicted(ent.Comp.ClickSound, ent.Owner, message.Actor);
}
// UpdateUi is called in the resulting ContainerModifiedMessage.
}
/// <summary>
/// The wzhzhzh of the grinder. Marks the grinder as active, but does not convert the items into reagents yet.
/// </summary>
private void StartGrinder(Entity<ReagentGrinderComponent> ent, GrinderProgram program)
{
if (IsActive(ent.AsNullable()))
return;
if (!_power.IsPowered(ent.Owner))
return;
var beaker = _itemSlotsSystem.GetItemOrNull(ent, ReagentGrinderComponent.BeakerSlotId);
// Do we have anything to grind/juice and a container to put the reagents in?
if (ent.Comp.InputContainer.ContainedEntities.Count <= 0 || !HasComp<FitsInDispenserComponent>(beaker))
return;
SoundSpecifier? sound;
switch (program)
{
case GrinderProgram.Grind when ent.Comp.InputContainer.ContainedEntities.All(x => CanGrind(x)):
sound = ent.Comp.GrindSound;
break;
case GrinderProgram.Juice when ent.Comp.InputContainer.ContainedEntities.All(x => CanJuice(x)):
sound = ent.Comp.JuiceSound;
break;
default:
return;
}
EnsureComp<ActiveReagentGrinderComponent>(ent);
_jitter.AddJitter(ent, -10, 100);
_powerState.TrySetWorkingState(ent.Owner, true); // Not all grinders need power.
ent.Comp.Program = program;
ent.Comp.EndTime = _timing.CurTime + ent.Comp.WorkTime * ent.Comp.WorkTimeMultiplier;
Dirty(ent);
UpdateUi(ent);
// Unpredicted because we don't have the user in the update loop
// TODO: Make the audio API sane https://github.com/space-wizards/RobustToolbox/issues/6436
if (_net.IsServer)
ent.Comp.AudioStream = _audioSystem.PlayPvs(sound, ent,
AudioParams.Default.WithPitchScale(1 / ent.Comp.WorkTimeMultiplier))?.Entity; //slightly higher pitched
}
/// <summary>
/// Converts items into reagents and marks the grinder as inactive.
/// </summary>
private void FinishGrinding(Entity<ReagentGrinderComponent> ent)
{
if (ent.Comp.Program is not { } program)
return; // Already finished.
ent.Comp.Program = null;
ent.Comp.AudioStream = _audioSystem.Stop(ent.Comp.AudioStream);
ent.Comp.EndTime = null; // It's important that we do this first or PredictedQueueDelete will fail to remove the entity from the container because the grinder is still active.
Dirty(ent);
// Remove deferred to avoid modifying the component we are currently enumerating over in the update loop.
RemCompDeferred<ActiveReagentGrinderComponent>(ent);
RemCompDeferred<JitteringComponent>(ent);
_powerState.TrySetWorkingState(ent.Owner, false);
var beaker = _itemSlotsSystem.GetItemOrNull(ent.Owner, ReagentGrinderComponent.BeakerSlotId);
if (beaker is null || !_solutionContainersSystem.TryGetFitsInDispenser(beaker.Value, out var beakerSolutionEntity, out var beakerSolution))
return;
// Convert items into reagents.
foreach (var item in ent.Comp.InputContainer.ContainedEntities.ToList())
{
var solution = GetGrinderSolution(item, program);
if (solution is null)
continue;
// Delete the item or reduce its stack size.
if (TryComp<StackComponent>(item, out var stack))
{
var totalVolume = solution.Volume * stack.Count;
if (totalVolume <= 0)
continue;
// Maximum number of items we can process in the stack without going over AvailableVolume
// We add a small tolerance, because floats are inaccurate.
var fitsCount = (int)(stack.Count * FixedPoint2.Min(beakerSolution.AvailableVolume / totalVolume + 0.01, 1));
if (fitsCount <= 0)
continue;
// Make a copy of the solution to scale
// Otherwise we'll actually change the volume of the remaining stack too
var scaledSolution = new Solution(solution);
scaledSolution.ScaleSolution(fitsCount);
solution = scaledSolution;
_stackSystem.SetCount((item, stack), stack.Count - fitsCount); // Setting to 0 will QueueDel
}
else
{
if (solution.Volume > beakerSolution.AvailableVolume)
continue;
_destructible.DestroyEntity(item);
}
_solutionContainersSystem.TryAddSolution(beakerSolutionEntity.Value, solution);
}
// UpdateUi is called when the entity in the grinder is deleted or the solution in the beaker is changed.
}
/// <summary>
/// Is the given grinder currently grinding/juicing?
/// </summary>
public bool IsActive(Entity<ReagentGrinderComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp))
return false;
// Don't use ActiveGrinderComponent for this because it is being removed deferred, meaning it will get updated at the end of the tick.
// ActiveReagentGrinderComponent is only for improving the EntityQueryEnumerator performance in the update loop.
return ent.Comp.EndTime != null;
}
/// <summary>
/// Gets the solutions from an entity using the specified Grinder program.
@ -24,7 +356,7 @@ public abstract class SharedReagentGrinderSystem : EntitySystem
switch (program)
{
case GrinderProgram.Grind:
if (_solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolution, out _, out var solution))
if (_solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolutionName, out _, out var solution))
{
return solution;
}
@ -41,15 +373,18 @@ public abstract class SharedReagentGrinderSystem : EntitySystem
/// </summary>
/// <param name="ent">The entity to check.</param>
/// <returns>True if it can be ground, otherwise false.</returns>
/// <remarks>
/// Will it blend? That is the question!
/// </remarks>
public bool CanGrind(Entity<ExtractableComponent?> ent)
{
if (!Resolve(ent, ref ent.Comp, false))
return false;
if (ent.Comp.GrindableSolution == null)
if (ent.Comp.GrindableSolutionName == null)
return false;
return _solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolution, out _, out _);
return _solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolutionName, out _, out _);
}
/// <summary>

View file

@ -1,116 +1,72 @@
using Content.Shared.Chemistry.Reagent;
using Robust.Shared.Serialization;
namespace Content.Shared.Kitchen
namespace Content.Shared.Kitchen;
/// <summary>
/// Sent by the client if they want to toggle the grinder's auto mode.
/// </summary>
[Serializable, NetSerializable]
public sealed class ReagentGrinderToggleAutoModeMessage() : BoundUserInterfaceMessage;
/// <summary>
/// Sent by the client if they want to start the grinder.
/// </summary>
[Serializable, NetSerializable]
public sealed class ReagentGrinderStartMessage(GrinderProgram program) : BoundUserInterfaceMessage
{
public sealed class SharedReagentGrinder
{
public static string BeakerSlotId = "beakerSlot";
public static string InputContainerId = "inputContainer";
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderToggleAutoModeMessage : BoundUserInterfaceMessage
{
public ReagentGrinderToggleAutoModeMessage() { }
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderStartMessage : BoundUserInterfaceMessage
{
public readonly GrinderProgram Program;
public ReagentGrinderStartMessage(GrinderProgram program)
{
Program = program;
}
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderEjectChamberAllMessage : BoundUserInterfaceMessage
{
public ReagentGrinderEjectChamberAllMessage()
{
}
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderEjectChamberContentMessage : BoundUserInterfaceMessage
{
public NetEntity EntityId;
public ReagentGrinderEjectChamberContentMessage(NetEntity entityId)
{
EntityId = entityId;
}
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderWorkStartedMessage : BoundUserInterfaceMessage
{
public GrinderProgram GrinderProgram;
public ReagentGrinderWorkStartedMessage(GrinderProgram grinderProgram)
{
GrinderProgram = grinderProgram;
}
}
[Serializable, NetSerializable]
public sealed class ReagentGrinderWorkCompleteMessage : BoundUserInterfaceMessage
{
public ReagentGrinderWorkCompleteMessage()
{
}
}
[Serializable, NetSerializable]
public enum ReagentGrinderVisualState : byte
{
BeakerAttached
}
[Serializable, NetSerializable]
public enum GrinderProgram : byte
{
Grind,
Juice
}
[NetSerializable, Serializable]
public enum ReagentGrinderUiKey : byte
{
Key
}
[Serializable, NetSerializable]
public enum GrinderAutoMode : byte
{
Off,
Grind,
Juice
}
[NetSerializable, Serializable]
public sealed class ReagentGrinderInterfaceState : BoundUserInterfaceState
{
public bool IsBusy;
public bool HasBeakerIn;
public bool Powered;
public bool CanJuice;
public bool CanGrind;
public NetEntity[] ChamberContents;
public ReagentQuantity[]? ReagentQuantities;
public GrinderAutoMode AutoMode;
public ReagentGrinderInterfaceState(bool isBusy, bool hasBeaker, bool powered, bool canJuice, bool canGrind, GrinderAutoMode autoMode, NetEntity[] chamberContents, ReagentQuantity[]? heldBeakerContents)
{
IsBusy = isBusy;
HasBeakerIn = hasBeaker;
Powered = powered;
CanJuice = canJuice;
CanGrind = canGrind;
AutoMode = autoMode;
ChamberContents = chamberContents;
ReagentQuantities = heldBeakerContents;
}
}
public GrinderProgram Program = program;
}
/// <summary>
/// Sent by the client if they want to eject all grindable entities within the grinder.
/// </summary>
[Serializable, NetSerializable]
public sealed class ReagentGrinderEjectChamberAllMessage() : BoundUserInterfaceMessage;
/// <summary>
/// Sent by the client if they want eject a single grindable entity within the grinder.
/// </summary>
[Serializable, NetSerializable]
public sealed class ReagentGrinderEjectChamberContentMessage(NetEntity entityId) : BoundUserInterfaceMessage
{
public NetEntity EntityId = entityId;
}
/// <summary>
/// Enum to be used for the grinder's appearance data.
/// </summary>
[Serializable, NetSerializable]
public enum ReagentGrinderVisualState : byte
{
BeakerAttached
}
/// <summary>
/// The mode the grinder will use when activated. Grinding and juicing the same prototype will yield different results.
/// </summary>
[Serializable, NetSerializable]
public enum GrinderProgram : byte
{
Grind,
Juice
}
/// <summary>
/// Key for the ReagentGrinderBoundUserInterface.
/// </summary>
[NetSerializable, Serializable]
public enum ReagentGrinderUiKey : byte
{
Key
}
/// <summary>
/// The setting of the grinder's auto mode.
/// </summary>
[NetSerializable, Serializable]
public enum GrinderAutoMode : byte
{
Off,
Grind,
Juice
}

View file

@ -97,7 +97,7 @@ public enum SmartFridgeUiKey : byte
}
/// <summary>
/// Send by the client when trying to dispense an item inside the fridge.
/// Sent by the client when trying to dispense an item inside the fridge.
/// </summary>
[Serializable, NetSerializable]
public sealed class SmartFridgeDispenseItemMessage(SmartFridgeEntry entry) : BoundUserInterfaceMessage
@ -106,7 +106,7 @@ public sealed class SmartFridgeDispenseItemMessage(SmartFridgeEntry entry) : Bou
}
/// <summary>
/// Send by the client when trying to remove an empty smart fridge entry from the list of items in the UI.
/// Sent by the client when trying to remove an empty smart fridge entry from the list of items in the UI.
/// </summary>
[Serializable, NetSerializable]
public sealed class SmartFridgeRemoveEntryMessage(SmartFridgeEntry entry) : BoundUserInterfaceMessage

View file

@ -3,7 +3,6 @@
parent: [ BaseMachinePowered, SmallConstructibleMachine ]
name: reagent grinder
description: From BlenderTech. Will It Blend? Let's find out!
suffix: grinder/juicer
components:
- type: ReagentGrinder
- type: ActivatableUI
@ -58,6 +57,26 @@
machine_board: !type:Container
machine_parts: !type:Container
- type: entity
id: KitchenReagentGrinderBeaker
parent: KitchenReagentGrinder
suffix: beaker
components:
- type: ContainerFill
containers:
beakerSlot:
- Beaker
- type: entity
id: KitchenReagentGrinderBeakerLarge
parent: KitchenReagentGrinder
suffix: large beaker
components:
- type: ContainerFill
containers:
beakerSlot:
- LargeBeaker
- type: entity
parent: Recycler #too different so different parent
id: ReagentGrinderIndustrial