mirror of
https://github.com/DeltaV-Station/Delta-v.git
synced 2026-09-30 08:07:28 +00:00
433 lines
18 KiB
C#
433 lines
18 KiB
C#
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.
|
|
/// </summary>
|
|
/// <param name="ent">The entity which we check for solutions.</param>
|
|
/// <param name="program">The grinder program.</param>
|
|
/// <returns>The solution received, or null if none.</returns>
|
|
public Solution? GetGrinderSolution(Entity<ExtractableComponent?> ent, GrinderProgram program)
|
|
{
|
|
if (!Resolve(ent, ref ent.Comp, false))
|
|
return null;
|
|
|
|
switch (program)
|
|
{
|
|
case GrinderProgram.Grind:
|
|
if (_solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolutionName, out _, out var solution))
|
|
{
|
|
return solution;
|
|
}
|
|
break;
|
|
case GrinderProgram.Juice:
|
|
return ent.Comp.JuiceSolution;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the entity can be ground using a ReagentGrinder.
|
|
/// </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.GrindableSolutionName == null)
|
|
return false;
|
|
|
|
return _solutionContainersSystem.TryGetSolution(ent.Owner, ent.Comp.GrindableSolutionName, out _, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks whether the entity can be juiced using a ReagentGrinder.
|
|
/// </summary>
|
|
/// <param name="ent">The entity to check.</param>
|
|
/// <returns>True if it can be juiced, otherwise false.</returns>
|
|
public bool CanJuice(Entity<ExtractableComponent?> ent)
|
|
{
|
|
if (!Resolve(ent, ref ent.Comp, false))
|
|
return false;
|
|
|
|
return ent.Comp.JuiceSolution is not null;
|
|
}
|
|
|
|
|
|
// Begin Imp Changes - DeltaV - Updated for reagent grinder moving to shared
|
|
private Solution? TryGrindSolution(
|
|
EntityUid uid,
|
|
Solution? solution,
|
|
Entity<ReagentGrinderComponent> grinder,
|
|
IReadOnlyList<EntityUid> contents)
|
|
{
|
|
var ev = new GrindAttemptEvent(grinder, contents);
|
|
RaiseLocalEvent(uid, ev);
|
|
|
|
if (ev.Cancelled)
|
|
return null;
|
|
|
|
return solution;
|
|
}
|
|
|
|
public sealed partial class GrindAttemptEvent : CancellableEntityEventArgs
|
|
{
|
|
public Entity<ReagentGrinderComponent> Grinder;
|
|
public IReadOnlyList<EntityUid> Reagents;
|
|
|
|
public GrindAttemptEvent(Entity<ReagentGrinderComponent> grinder, IReadOnlyList<EntityUid> reagents)
|
|
{
|
|
Grinder = grinder;
|
|
Reagents = reagents;
|
|
}
|
|
}
|
|
// End Imp Changes - DeltaV - Updated for reagent grinder moving to shared
|
|
}
|