using Content.Server.Kitchen.Components; using Content.Server.Power.Components; 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.Construction.Completions; using Content.Server.Jittering; using Content.Shared.Jittering; using Content.Shared.Power; namespace Content.Server.Kitchen.EntitySystems { [UsedImplicitly] internal sealed class ReagentGrinderSystem : EntitySystem { [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!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnActiveGrinderStart); SubscribeLocalEvent(OnActiveGrinderRemove); SubscribeLocalEvent((uid, _, _) => UpdateUiState(uid)); SubscribeLocalEvent((EntityUid uid, ReagentGrinderComponent _, ref PowerChangedEvent _) => UpdateUiState(uid)); SubscribeLocalEvent(OnInteractUsing); SubscribeLocalEvent(OnContainerModified); SubscribeLocalEvent(OnContainerModified); SubscribeLocalEvent(OnEntRemoveAttempt); SubscribeLocalEvent(OnToggleAutoModeMessage); SubscribeLocalEvent(OnStartMessage); SubscribeLocalEvent(OnEjectChamberAllMessage); SubscribeLocalEvent(OnEjectChamberContentMessage); } private void OnToggleAutoModeMessage(Entity entity, ref ReagentGrinderToggleAutoModeMessage message) { entity.Comp.AutoMode = (GrinderAutoMode) (((byte) entity.Comp.AutoMode + 1) % Enum.GetValues(typeof(GrinderAutoMode)).Length); UpdateUiState(entity); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); 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(uid); var inputContainer = _containerSystem.EnsureContainer(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 = active.Program switch { GrinderProgram.Grind => GetGrindSolution(item), GrinderProgram.Juice => CompOrNull(item)?.JuiceSolution, _ => null, }; if (solution is null) continue; if (TryComp(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.SetCount(item, stack.Count - 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 ent, ref ComponentStartup args) { _jitter.AddJitter(ent, -10, 100); } private void OnActiveGrinderRemove(Entity ent, ref ComponentRemove args) { RemComp(ent); } private void OnEntRemoveAttempt(Entity entity, ref ContainerIsRemovingAttemptEvent args) { if (HasComp(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(uid) && this.IsPowered(uid, EntityManager)) { var program = reagentGrinder.AutoMode == GrinderAutoMode.Grind ? GrinderProgram.Grind : GrinderProgram.Juice; DoWork(uid, reagentGrinder, program); } } private void OnInteractUsing(Entity entity, ref InteractUsingEvent args) { var heldEnt = args.Used; var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); if (!HasComp(heldEnt)) { if (!HasComp(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(uid, SharedReagentGrinder.InputContainerId); var outputContainer = _itemSlotsSystem.GetItemOrNull(uid, SharedReagentGrinder.BeakerSlotId); Solution? containerSolution = null; var isBusy = HasComp(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(CanGrind); canJuice = inputContainer.ContainedEntities.All(CanJuice); } 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 entity, ref ReagentGrinderStartMessage message) { if (!this.IsPowered(entity.Owner, EntityManager) || HasComp(entity)) return; DoWork(entity.Owner, entity.Comp, message.Program); } private void OnEjectChamberAllMessage(Entity entity, ref ReagentGrinderEjectChamberAllMessage message) { var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); if (HasComp(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 entity, ref ReagentGrinderEjectChamberContentMessage message) { if (HasComp(entity)) return; var inputContainer = _containerSystem.EnsureContainer(entity.Owner, SharedReagentGrinder.InputContainerId); var ent = GetEntity(message.EntityId); if (_containerSystem.Remove(ent, inputContainer)) { _randomHelper.RandomOffset(ent, 0.4f); ClickSound(entity); UpdateUiState(entity); } } /// /// The wzhzhzh of the grinder. Processes the contents of the grinder and puts the output in the beaker. /// /// The grinder itself /// /// Which program, such as grind or juice private void DoWork(EntityUid uid, ReagentGrinderComponent reagentGrinder, GrinderProgram program) { var inputContainer = _containerSystem.EnsureContainer(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(outputContainer)) return; SoundSpecifier? sound; switch (program) { case GrinderProgram.Grind when inputContainer.ContainedEntities.All(CanGrind): sound = reagentGrinder.GrindSound; break; case GrinderProgram.Juice when inputContainer.ContainedEntities.All(CanJuice): sound = reagentGrinder.JuiceSound; break; default: return; } var active = AddComp(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 reagentGrinder) { _audioSystem.PlayPvs(reagentGrinder.Comp.ClickSound, reagentGrinder.Owner, AudioParams.Default.WithVolume(-2f)); } private Solution? GetGrindSolution(EntityUid uid) { if (TryComp(uid, out var extractable) && extractable.GrindableSolution is not null && _solutionContainersSystem.TryGetSolution(uid, extractable.GrindableSolution, out _, out var solution)) { return solution; } else return null; } private bool CanGrind(EntityUid uid) { var solutionName = CompOrNull(uid)?.GrindableSolution; return solutionName is not null && _solutionContainersSystem.TryGetSolution(uid, solutionName, out _, out _); } private bool CanJuice(EntityUid uid) { return CompOrNull(uid)?.JuiceSolution is not null; } } }