using Content.Server.Chemistry.Components; using Content.Server.Chemistry.Containers.EntitySystems; 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!; [Dependency] private readonly IEntityManager _entityManager = default!; [ValidatePrototypeId] private const string PillPrototypeId = "Pill"; [ValidatePrototypeId] private const string PillCanisterPrototypeId = "PillCanister"; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(SubscribeUpdateUiState); SubscribeLocalEvent(OnSetModeMessage); SubscribeLocalEvent(OnSetPillTypeMessage); SubscribeLocalEvent(OnReagentButtonMessage); SubscribeLocalEvent(OnCreatePillsMessage); SubscribeLocalEvent(OnOutputToBottleMessage); SubscribeLocalEvent(OnSortMethodUpdated); SubscribeLocalEvent(OnTransferringAmountUpdated); SubscribeLocalEvent(OnAmountsUpdated); } private void OnAmountsUpdated(Entity ent, ref ChemMasterAmountsUpdated args) => ent.Comp.Amounts = args.Amounts; 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; if (!_solutionContainerSystem.TryGetSolution(owner, SharedChemMaster.PillBufferSolutionName, out _, out var pillBufferSolution)) return; var container = _itemSlotsSystem.GetItemOrNull(owner, SharedChemMaster.InputSlotName); var bufferReagents = bufferSolution.Contents; var bufferCurrentVolume = bufferSolution.Volume; var pillBufferReagents = pillBufferSolution.Contents; var pillBufferCurrentVolume = pillBufferSolution.Volume; var state = new ChemMasterBoundUserInterfaceState( chemMaster.Mode, BuildInputContainerInfo(container), bufferReagents, pillBufferReagents, bufferCurrentVolume, pillBufferCurrentVolume, chemMaster.PillType, chemMaster.PillDosageLimit, updateLabel, chemMaster.SortMethod, chemMaster.TransferringAmount, chemMaster.Amounts); _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 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) { switch (chemMaster.Comp.Mode) { case ChemMasterMode.Transfer: TransferReagents(chemMaster, message.ReagentId, message.Amount, message.FromBuffer, message.IsOutput); break; case ChemMasterMode.Discard: DiscardReagents(chemMaster, message.ReagentId, message.Amount, message.FromBuffer, message.IsOutput); break; default: // Invalid mode. return; } ClickSound(chemMaster); } private void TransferReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer, bool isOutput) { 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) || !_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.PillBufferSolutionName, out _, out var pillBufferSolution)) return; if (fromBuffer) // Buffer to container { amount = FixedPoint2.Min(amount, containerSolution.AvailableVolume); var solution = isOutput ? pillBufferSolution : bufferSolution; amount = solution.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); var solution = isOutput ? pillBufferSolution : bufferSolution; solution.AddReagent(id, amount); } UpdateUiState(chemMaster, updateLabel: true); } private void DiscardReagents(Entity chemMaster, ReagentId id, FixedPoint2 amount, bool fromBuffer, bool isOutput) { if (fromBuffer) { if (_solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.BufferSolutionName, out _, out var bufferSolution) && _solutionContainerSystem.TryGetSolution(chemMaster.Owner, SharedChemMaster.PillSolutionName, out _, out var pillBufferSolution)) { var solution = isOutput ? pillBufferSolution : bufferSolution; solution.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 == null) { var canister = _entityManager.SpawnEntity(PillCanisterPrototypeId, Transform(chemMaster.Owner).Coordinates); _itemSlotsSystem.TryInsert(chemMaster.Owner, SharedChemMaster.OutputSlotName, canister, null); maybeContainer = canister; } 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; // quick fix until we get a proper one from EE if (message.Number > 50) 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); var hasItemSolution = _solutionContainerSystem.EnsureSolutionEntity( (item, null), SharedChemMaster.PillSolutionName, out var itemSolution, message.Dosage); if (!hasItemSolution || itemSolution is null) continue; _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 == null) { var canister = _entityManager.SpawnEntity(PillCanisterPrototypeId, Transform(chemMaster.Owner).Coordinates); _itemSlotsSystem.TryInsert(chemMaster.Owner, SharedChemMaster.OutputSlotName, canister, null); maybeContainer = canister; } 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.PillBufferSolutionName, 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 static ContainerInfo BuildContainerInfo(string name, Solution solution) => new(name, solution.Volume, solution.MaxVolume) { Reagents = solution.Contents }; private void OnSortMethodUpdated(EntityUid uid, ChemMasterComponent chemMaster, ChemMasterSortMethodUpdated args) { chemMaster.SortMethod = args.SortMethod; UpdateUiState((uid, chemMaster)); } private void OnTransferringAmountUpdated(EntityUid uid, ChemMasterComponent chemMaster, ChemMasterTransferringAmountUpdated args) { chemMaster.TransferringAmount = args.TransferringAmount; ClickSound((uid, chemMaster)); UpdateUiState((uid, chemMaster)); } } }