#nullable enable using System; using System.Linq; using System.Threading.Tasks; using Content.Server.Chemistry.Components; using Content.Server.Hands.Components; using Content.Server.Items; using Content.Server.Power.Components; using Content.Server.UserInterface; using Content.Shared.Chemistry.Solution; using Content.Shared.Interaction; using Content.Shared.Kitchen.Components; using Content.Shared.Notification; using Content.Shared.Notification.Managers; using Content.Shared.Random.Helpers; using Content.Shared.Tag; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Localization; using Robust.Shared.Player; using Robust.Shared.Serialization.Manager.Attributes; using Robust.Shared.ViewVariables; namespace Content.Server.Kitchen.Components { /// /// The combo reagent grinder/juicer. The reason why grinding and juicing are seperate is simple, /// think of grinding as a utility to break an object down into its reagents. Think of juicing as /// 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". /// [RegisterComponent] [ComponentReference(typeof(IActivate))] public class ReagentGrinderComponent : SharedReagentGrinderComponent, IActivate, IInteractUsing { private AudioSystem _audioSystem = default!; [ViewVariables] private ContainerSlot _beakerContainer = default!; /// /// Can be null since we won't always have a beaker in the grinder. /// [ViewVariables] private SolutionContainerComponent? _heldBeaker = default!; /// /// Contains the things that are going to be ground or juiced. /// [ViewVariables] private Container _chamber = default!; [ViewVariables] private bool ChamberEmpty => _chamber.ContainedEntities.Count <= 0; [ViewVariables] private bool HasBeaker => _beakerContainer.ContainedEntity != null; [ViewVariables] private BoundUserInterface? UserInterface => Owner.GetUIOrNull(ReagentGrinderUiKey.Key); private bool Powered => !Owner.TryGetComponent(out PowerReceiverComponent? receiver) || receiver.Powered; /// /// Should the BoundUI be told to update? /// private bool _uiDirty = true; /// /// Is the machine actively doing something and can't be used right now? /// private bool _busy = false; //YAML serialization vars [ViewVariables(VVAccess.ReadWrite)] [DataField("chamberCapacity")] private int _storageCap = 16; [ViewVariables(VVAccess.ReadWrite)] [DataField("workTime")] private int _workTime = 3500; //3.5 seconds, completely arbitrary for now. protected override void Initialize() { base.Initialize(); //A slot for the beaker where the grounds/juices will go. _beakerContainer = ContainerHelpers.EnsureContainer(Owner, $"{Name}-reagentContainerContainer"); //A container for the things that WILL be ground/juiced. Useful for ejecting them instead of deleting them from the hands of the user. _chamber = ContainerHelpers.EnsureContainer(Owner, $"{Name}-entityContainerContainer"); if (UserInterface != null) { UserInterface.OnReceiveMessage += UserInterfaceOnReceiveMessage; } _audioSystem = EntitySystem.Get(); } public override void HandleMessage(ComponentMessage message, IComponent? component) { base.HandleMessage(message, component); switch (message) { case PowerChangedMessage powerChanged: OnPowerStateChanged(powerChanged); break; } } protected override void OnRemove() { base.OnRemove(); if (UserInterface != null) { UserInterface.OnReceiveMessage -= UserInterfaceOnReceiveMessage; } } private void UserInterfaceOnReceiveMessage(ServerBoundUserInterfaceMessage message) { if(_busy) { return; } switch(message.Message) { case ReagentGrinderGrindStartMessage msg: if (!Powered) break; ClickSound(); DoWork(message.Session.AttachedEntity!, GrinderProgram.Grind); break; case ReagentGrinderJuiceStartMessage msg: if (!Powered) break; ClickSound(); DoWork(message.Session.AttachedEntity!, GrinderProgram.Juice); break; case ReagentGrinderEjectChamberAllMessage msg: if(!ChamberEmpty) { ClickSound(); for (var i = _chamber.ContainedEntities.Count - 1; i >= 0; i--) { EjectSolid(_chamber.ContainedEntities.ElementAt(i).Uid); } } break; case ReagentGrinderEjectChamberContentMessage msg: if (!ChamberEmpty) { EjectSolid(msg.EntityID); ClickSound(); _uiDirty = true; } break; case ReagentGrinderEjectBeakerMessage msg: ClickSound(); EjectBeaker(message.Session.AttachedEntity); //EjectBeaker will dirty the UI for us, we don't have to do it explicitly here. break; } } private void OnPowerStateChanged(PowerChangedMessage e) { _uiDirty = true; } private void ClickSound() { SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/machine_switch.ogg", Owner, AudioParams.Default.WithVolume(-2f)); } private void SetAppearance() { if (Owner.TryGetComponent(out AppearanceComponent? appearance)) { appearance.SetData(ReagentGrinderVisualState.BeakerAttached, HasBeaker); } } public void OnUpdate() { if(_uiDirty) { UpdateInterface(); _uiDirty = false; } } // This doesn't check for UI dirtiness so handle that when calling this. private void UpdateInterface() { bool canJuice = false; bool canGrind = false; if (HasBeaker) { foreach (var entity in _chamber.ContainedEntities) { if (!canJuice && entity.HasComponent()) canJuice = true; if (!canGrind && entity.HasTag("Grindable")) canGrind = true; if (canJuice && canGrind) break; } } UserInterface?.SetState(new ReagentGrinderInterfaceState ( _busy, HasBeaker, Powered, canJuice, canGrind, _chamber.ContainedEntities.Select(item => item.Uid).ToArray(), //Remember the beaker can be null! _heldBeaker?.Solution.Contents.ToArray() )); _uiDirty = false; } private void EjectSolid(EntityUid entityID) { if (_busy) return; if (Owner.EntityManager.TryGetEntity(entityID, out var entity)) { _chamber.Remove(entity); //Give the ejected entity a tiny bit of offset so each one is apparent in case of a big stack, //but (hopefully) not enough to clip it through a solid (wall). entity.RandomOffset(0.4f); } _uiDirty = true; } /// /// Tries to eject whatever is in the beaker slot. Puts the item in the user's hands or failing that on top /// of the grinder. /// private void EjectBeaker(IEntity? user) { if (!HasBeaker || _heldBeaker == null || _busy) return; var beaker = _beakerContainer.ContainedEntity; if(beaker is null) return; _beakerContainer.Remove(beaker); if (user == null || !user.TryGetComponent(out var hands) || !_heldBeaker.Owner.TryGetComponent(out var item)) return; hands.PutInHandOrDrop(item); _heldBeaker = null; _uiDirty = true; SetAppearance(); } void IActivate.Activate(ActivateEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out ActorComponent? actor)) { return; } _uiDirty = true; UserInterface?.Toggle(actor.PlayerSession); } async Task IInteractUsing.InteractUsing(InteractUsingEventArgs eventArgs) { if (!eventArgs.User.TryGetComponent(out IHandsComponent? hands)) { Owner.PopupMessage(eventArgs.User, Loc.GetString("You have no hands.")); return true; } IEntity heldEnt = eventArgs.Using; //First, check if user is trying to insert a beaker. //No promise it will be a beaker right now, but whatever. //Maybe this should whitelist "beaker" in the prototype id of heldEnt? if(heldEnt.TryGetComponent(out SolutionContainerComponent? beaker) && beaker.Capabilities.HasFlag(SolutionContainerCaps.FitsInDispenser)) { _beakerContainer.Insert(heldEnt); _heldBeaker = beaker; _uiDirty = true; //We are done, return. Insert the beaker and exit! SetAppearance(); ClickSound(); return true; } //Next, see if the user is trying to insert something they want to be ground/juiced. if(!heldEnt.HasTag("Grindable") && !heldEnt.TryGetComponent(out JuiceableComponent? juice)) { //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 false; } //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 (_chamber.ContainedEntities.Count >= _storageCap) { return false; } if (!_chamber.Insert(heldEnt)) return false; _uiDirty = true; return true; } /// /// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker. /// /// true for wanting to juice, false for wanting to grind. private async void DoWork(IEntity user, GrinderProgram program) { //Have power, are we busy, chamber has anything to grind, a beaker for the grounds to go? if(!Powered || _busy || ChamberEmpty || !HasBeaker || _heldBeaker == null) { return; } _busy = true; UserInterface?.SendMessage(new ReagentGrinderWorkStartedMessage(program)); switch (program) { case GrinderProgram.Grind: SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/blender.ogg", Owner, AudioParams.Default); //Get each item inside the chamber and get the reagents it contains. Transfer those reagents to the beaker, given we have one in. Owner.SpawnTimer(_workTime, (Action) (() => { foreach (var item in _chamber.ContainedEntities.ToList()) { if (!item.HasTag("Grindable")) continue; if (!item.TryGetComponent(out var solution)) continue; if (_heldBeaker.CurrentVolume + solution.CurrentVolume > _heldBeaker.MaxVolume) continue; _heldBeaker.TryAddSolution(solution.Solution); solution.RemoveAllSolution(); item.Delete(); } _busy = false; _uiDirty = true; UserInterface?.SendMessage(new ReagentGrinderWorkCompleteMessage()); })); break; case GrinderProgram.Juice: SoundSystem.Play(Filter.Pvs(Owner), "/Audio/Machines/juicer.ogg", Owner, AudioParams.Default); Owner.SpawnTimer(_workTime, (Action) (() => { foreach (var item in _chamber.ContainedEntities.ToList()) { if (!item.TryGetComponent(out var juiceMe)) continue; if (_heldBeaker.CurrentVolume + juiceMe.JuiceResultSolution.TotalVolume > _heldBeaker.MaxVolume) continue; _heldBeaker.TryAddSolution(juiceMe.JuiceResultSolution); item.Delete(); } UserInterface?.SendMessage(new ReagentGrinderWorkCompleteMessage()); _busy = false; _uiDirty = true; })); break; } } } }