using Content.Server.Chemistry.Components; using Content.Server.Labels; using Content.Server.Popups; using Content.Server.Storage.EntitySystems; using Content.Shared.Administration.Logs; using Content.Shared.Chemistry; using Content.Shared.Chemistry.Components; using Content.Shared.Chemistry.EntitySystems; using Content.Shared.Chemistry.Reagent; using Content.Shared.Containers.ItemSlots; using Content.Shared.Database; using Content.Shared.FixedPoint; using Content.Shared.Storage; using JetBrains.Annotations; using Robust.Server.Audio; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Containers; using Robust.Shared.Prototypes; using System.Diagnostics.CodeAnalysis; using System.Linq; namespace Content.Server.Chemistry.EntitySystems { /// /// Contains all the server-side logic for ChemMasters. /// /// [UsedImplicitly] public sealed class ChemMasterSystem : EntitySystem { [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly AudioSystem _audioSystem = default!; [Dependency] private readonly SharedSolutionContainerSystem _solutionContainerSystem = default!; [Dependency] private readonly ItemSlotsSystem _itemSlotsSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; [Dependency] private readonly StorageSystem _storageSystem = default!; [Dependency] private readonly LabelSystem _labelSystem = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [ValidatePrototypeId] private const string PillPrototypeId = "Pill"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(OnSetModeMessage); SubscribeLocalEvent(OnCycleSortingTypeMessage); SubscribeLocalEvent(OnSetPillTypeMessage); SubscribeLocalEvent(OnReagentButtonMessage); SubscribeLocalEvent(OnCreatePillsMessage); SubscribeLocalEvent(OnOutputToBottleMessage); } private void SubscribeUpdateUiState(Entity ent, ref T ev) { UpdateUiState(ent); } private void UpdateUiState(Entity ent, bool updateLabel = false) { var (owner, chemMaster) = ent; if (!_solutionContainerSystem.TryGetSolution(owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution)) return; var inputContainer = _itemSlotsSystem.GetItemOrNull(owner, SharedChemMaster.InputSlotName); var outputContainer = _itemSlotsSystem.GetItemOrNull(owner, SharedChemMaster.OutputSlotName); var bufferReagents = bufferSolution.Contents; var bufferCurrentVolume = bufferSolution.Volume; var state = new ChemMasterBoundUserInterfaceState( chemMaster.Mode, chemMaster.SortingType, BuildInputContainerInfo(inputContainer), BuildOutputContainerInfo(outputContainer), bufferReagents, bufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel); _userInterfaceSystem.SetUiState(owner, ChemMasterUiKey.Key, state); } private void OnSetModeMessage(Entity chemMaster, ref ChemMasterSetModeMessage message) { // Ensure the mode is valid, either Transfer or Discard. if (!Enum.IsDefined(typeof(ChemMasterMode), message.ChemMasterMode)) return; chemMaster.Comp.Mode = message.ChemMasterMode; UpdateUiState(chemMaster); ClickSound(chemMaster); } private void OnCycleSortingTypeMessage(Entity chemMaster, ref ChemMasterSortingTypeCycleMessage message) { chemMaster.Comp.SortingType++; if (chemMaster.Comp.SortingType > ChemMasterSortingType.Latest) chemMaster.Comp.SortingType = ChemMasterSortingType.None; UpdateUiState(chemMaster); ClickSound(chemMaster); } private void OnSetPillTypeMessage(Entity chemMaster, ref ChemMasterSetPillTypeMessage message) { // Ensure valid pill type. There are 20 pills selectable, 0-19. if (message.PillType > SharedChemMaster.PillTypes - 1) return; chemMaster.Comp.PillType = message.PillType; UpdateUiState(chemMaster); ClickSound(chemMaster); } private void OnReagentButtonMessage(Entity chemMaster, ref ChemMasterReagentAmountButtonMessage message) { // Ensure the amount corresponds to one of the reagent amount buttons. if (!Enum.IsDefined(typeof(ChemMasterReagentAmount), message.Amount)) return; switch (chemMaster.Comp.Mode) { case ChemMasterMode.Transfer: TransferReagents(chemMaster, message.ReagentId, message.Amount.GetFixedPoint(), message.FromBuffer); break; case ChemMasterMode.Discard: DiscardReagents(chemMaster, message.ReagentId, message.Amount.GetFixedPoint(), message.FromBuffer); break; default: // Invalid mode. return; } ClickSound(chemMaster); } private void TransferReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer) { var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName); if (container is null || !_solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSoln, out var containerSolution) || !_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution)) { return; } if (fromBuffer) // Buffer to container { amount = FixedPoint2.Min(amount, containerSolution.AvailableVolume); amount = bufferSolution.RemoveReagent(id, amount, preserveOrder: true); _solutionContainerSystem.TryAddReagent(containerSoln.Value, id, amount, out var _); } else // Container to buffer { amount = FixedPoint2.Min(amount, containerSolution.GetReagentQuantity(id)); _solutionContainerSystem.RemoveReagent(containerSoln.Value, id, amount); bufferSolution.AddReagent(id, amount); } UpdateUiState(chemMaster, updateLabel: true); } private void DiscardReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer) { if (fromBuffer) { if (_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution)) bufferSolution.RemoveReagent(id, amount, preserveOrder: true); else return; } else { var container = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.InputSlotName); if (container is not null && _solutionContainerSystem.TryGetFitsInDispenser(container.Value, out var containerSolution, out _)) { _solutionContainerSystem.RemoveReagent(containerSolution.Value, id, amount); } else return; } UpdateUiState(chemMaster, updateLabel: fromBuffer); } private void OnCreatePillsMessage(Entity chemMaster, ref ChemMasterCreatePillsMessage message) { var user = message.Actor; var maybeContainer = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.OutputSlotName); if (maybeContainer is not { Valid: true } container || !TryComp(container, out StorageComponent? storage)) { return; // output can't fit pills } // Ensure the number is valid. if (message.Number == 0 || !_storageSystem.HasSpace((container, storage))) return; // Ensure the amount is valid. if (message.Dosage == 0 || message.Dosage > chemMaster.Comp.PillDosageLimit) return; // Ensure label length is within the character limit. if (message.Label.Length > SharedChemMaster.LabelMaxLength) return; var needed = message.Dosage * message.Number; if (!WithdrawFromBuffer(chemMaster, needed, user, out var withdrawal)) return; _labelSystem.Label(container, message.Label); for (var i = 0; i < message.Number; i++) { var item = Spawn(PillPrototypeId, Transform(container).Coordinates); _storageSystem.Insert(container, item, out _, user: user, storage); _labelSystem.Label(item, message.Label); _solutionContainerSystem.EnsureSolutionEntity(item, SharedChemMaster.PillSolutionName,out var itemSolution ,message.Dosage); if (!itemSolution.HasValue) return; _solutionContainerSystem.TryAddSolution(itemSolution.Value, withdrawal.SplitSolution(message.Dosage)); var pill = EnsureComp(item); pill.PillType = chemMaster.Comp.PillType; Dirty(item, pill); // Log pill creation by a user _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} printed {ToPrettyString(item):pill} {SharedSolutionContainerSystem.ToPrettyString(itemSolution.Value.Comp.Solution)}"); } UpdateUiState(chemMaster); ClickSound(chemMaster); } private void OnOutputToBottleMessage(Entity chemMaster, ref ChemMasterOutputToBottleMessage message) { var user = message.Actor; var maybeContainer = _itemSlotsSystem.GetItemOrNull(chemMaster, SharedChemMaster.OutputSlotName); if (maybeContainer is not { Valid: true } container || !_solutionContainerSystem.TryGetSolution(container, SharedChemMaster.BottleSolutionName, out var soln, out var solution)) { return; // output can't fit reagents } // Ensure the amount is valid. if (message.Dosage == 0 || message.Dosage > solution.AvailableVolume) return; // Ensure label length is within the character limit. if (message.Label.Length > SharedChemMaster.LabelMaxLength) return; if (!WithdrawFromBuffer(chemMaster, message.Dosage, user, out var withdrawal)) return; _labelSystem.Label(container, message.Label); _solutionContainerSystem.TryAddSolution(soln.Value, withdrawal); // Log bottle creation by a user _adminLogger.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):user} bottled {ToPrettyString(container):bottle} {SharedSolutionContainerSystem.ToPrettyString(solution)}"); UpdateUiState(chemMaster); ClickSound(chemMaster); } private bool WithdrawFromBuffer( Entity chemMaster, FixedPoint2 neededVolume, EntityUid? user, [NotNullWhen(returnValue: true)] out Solution? outputSolution) { outputSolution = null; if (!_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var solution)) { return false; } if (solution.Volume == 0) { if (user.HasValue) _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-empty-text"), user.Value); return false; } // ReSharper disable once InvertIf if (neededVolume > solution.Volume) { if (user.HasValue) _popupSystem.PopupCursor(Loc.GetString("chem-master-window-buffer-low-text"), user.Value); return false; } outputSolution = solution.SplitSolution(neededVolume); return true; } private void ClickSound(Entity chemMaster) { _audioSystem.PlayPvs(chemMaster.Comp.ClickSound, chemMaster, AudioParams.Default.WithVolume(-2f)); } private ContainerInfo? BuildInputContainerInfo(EntityUid? container) { if (container is not { Valid: true }) return null; if (!TryComp(container, out FitsInDispenserComponent? fits) || !_solutionContainerSystem.TryGetSolution(container.Value, fits.Solution, out _, out var solution)) { return null; } return BuildContainerInfo(Name(container.Value), solution); } private ContainerInfo? BuildOutputContainerInfo(EntityUid? container) { if (container is not { Valid: true }) return null; var name = Name(container.Value); { if (_solutionContainerSystem.TryGetSolution( container.Value, SharedChemMaster.BottleSolutionName, out _, out var solution)) { return BuildContainerInfo(name, solution); } } if (!TryComp(container, out StorageComponent? storage)) return null; var pills = storage.Container.ContainedEntities.Select((Func) (pill => { _solutionContainerSystem.TryGetSolution(pill, SharedChemMaster.PillSolutionName, out _, out var solution); var quantity = solution?.Volume ?? FixedPoint2.Zero; return (Name(pill), quantity); })).ToList(); return new ContainerInfo(name, _storageSystem.GetCumulativeItemAreas((container.Value, storage)), storage.Grid.GetArea()) { Entities = pills }; } private static ContainerInfo BuildContainerInfo(string name, Solution solution) { return new ContainerInfo(name, solution.Volume, solution.MaxVolume) { Reagents = solution.Contents }; } } }