using Content.Shared.Emag.Components; using Robust.Shared.Prototypes; using System.Linq; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Advertise.Components; using Content.Shared.Advertise.Systems; using Content.Shared.DoAfter; using Content.Shared.Emag.Systems; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Power.EntitySystems; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.GameStates; using Robust.Shared.Network; using Robust.Shared.Random; using Robust.Shared.Timing; namespace Content.Shared.VendingMachines; public abstract partial class SharedVendingMachineSystem : EntitySystem { [Dependency] protected readonly IGameTiming Timing = default!; [Dependency] protected readonly IPrototypeManager PrototypeManager = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!; [Dependency] protected readonly SharedAudioSystem Audio = default!; [Dependency] private readonly SharedDoAfterSystem _doAfter = default!; [Dependency] protected readonly SharedPointLightSystem Light = default!; [Dependency] private readonly SharedPowerReceiverSystem _receiver = default!; [Dependency] protected readonly SharedPopupSystem Popup = default!; [Dependency] private readonly SharedSpeakOnUIClosedSystem _speakOn = default!; [Dependency] protected readonly SharedUserInterfaceSystem UISystem = default!; [Dependency] protected readonly IRobustRandom Randomizer = default!; [Dependency] private readonly EmagSystem _emag = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnVendingGetState); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnAfterInteract); Subs.BuiEvents(VendingMachineUiKey.Key, subs => { subs.Event(OnInventoryEjectMessage); }); } private void OnVendingGetState(Entity entity, ref ComponentGetState args) { var component = entity.Comp; var inventory = new Dictionary(); var emaggedInventory = new Dictionary(); var contrabandInventory = new Dictionary(); foreach (var weh in component.Inventory) { inventory[weh.Key] = new(weh.Value); } foreach (var weh in component.EmaggedInventory) { emaggedInventory[weh.Key] = new(weh.Value); } foreach (var weh in component.ContrabandInventory) { contrabandInventory[weh.Key] = new(weh.Value); } args.State = new VendingMachineComponentState() { Inventory = inventory, EmaggedInventory = emaggedInventory, ContrabandInventory = contrabandInventory, Contraband = component.Contraband, EjectEnd = component.EjectEnd, DenyEnd = component.DenyEnd, DispenseOnHitEnd = component.DispenseOnHitEnd, }; } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); var curTime = Timing.CurTime; while (query.MoveNext(out var uid, out var comp)) { if (comp.Ejecting) { if (curTime > comp.EjectEnd) { comp.EjectEnd = null; Dirty(uid, comp); EjectItem(uid, comp); UpdateUI((uid, comp)); } } if (comp.Denying) { if (curTime > comp.DenyEnd) { comp.DenyEnd = null; Dirty(uid, comp); TryUpdateVisualState((uid, comp)); } } if (comp.DispenseOnHitCoolingDown) { if (curTime > comp.DispenseOnHitEnd) { comp.DispenseOnHitEnd = null; Dirty(uid, comp); } } } } private void OnInventoryEjectMessage(Entity entity, ref VendingMachineEjectMessage args) { if (!_receiver.IsPowered(entity.Owner) || Deleted(entity)) return; if (args.Actor is not { Valid: true } actor) return; AuthorizedVend(entity.Owner, actor, args.Type, args.ID, entity.Comp); } protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args) { RestockInventoryFromPrototype(uid, component, component.InitialStockQuality); } protected virtual void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { } /// /// Checks if the user is authorized to use this vending machine /// /// /// Entity trying to use the vending machine /// public bool IsAuthorized(EntityUid uid, EntityUid sender, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return false; if (!TryComp(uid, out var accessReader)) return true; if (_accessReader.IsAllowed(sender, uid, accessReader) || HasComp(uid)) return true; Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, sender); Deny((uid, vendComponent), sender); return false; } protected VendingMachineInventoryEntry? GetEntry(EntityUid uid, string entryId, InventoryType type, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return null; if (type == InventoryType.Emagged && HasComp(uid)) return component.EmaggedInventory.GetValueOrDefault(entryId); if (type == InventoryType.Contraband && component.Contraband) return component.ContrabandInventory.GetValueOrDefault(entryId); return component.Inventory.GetValueOrDefault(entryId); } /// /// Tries to eject the provided item. Will do nothing if the vending machine is incapable of ejecting, already ejecting /// or the item doesn't exist in its inventory. /// /// /// The type of inventory the item is from /// The prototype ID of the item /// Whether the item should be thrown in a random direction after ejection /// public void TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; if (vendComponent.Ejecting || vendComponent.Broken || !_receiver.IsPowered(uid)) { return; } var entry = GetEntry(uid, itemId, type, vendComponent); if (string.IsNullOrEmpty(entry?.ID)) { Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid); Deny((uid, vendComponent)); return; } if (entry.Amount <= 0) { Popup.PopupClient(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid); Deny((uid, vendComponent)); return; } // Start Ejecting, and prevent users from ordering while anim playing vendComponent.EjectEnd = Timing.CurTime + vendComponent.EjectDelay; vendComponent.NextItemToEject = entry.ID; vendComponent.ThrowNextItem = throwItem; if (TryComp(uid, out SpeakOnUIClosedComponent? speakComponent)) _speakOn.TrySetFlag((uid, speakComponent)); entry.Amount--; Dirty(uid, vendComponent); UpdateUI((uid, vendComponent)); TryUpdateVisualState((uid, vendComponent)); Audio.PlayPredicted(vendComponent.SoundVend, uid, user); } public void Deny(Entity entity, EntityUid? user = null) { if (!Resolve(entity.Owner, ref entity.Comp)) return; if (entity.Comp.Denying) return; entity.Comp.DenyEnd = Timing.CurTime + entity.Comp.DenyDelay; Audio.PlayPredicted(entity.Comp.SoundDeny, entity.Owner, user, AudioParams.Default.WithVolume(-2f)); TryUpdateVisualState(entity); Dirty(entity); } protected virtual void UpdateUI(Entity entity) { } /// /// Tries to update the visuals of the component based on its current state. /// public void TryUpdateVisualState(Entity entity) { if (!Resolve(entity.Owner, ref entity.Comp)) return; var finalState = VendingMachineVisualState.Normal; if (entity.Comp.Broken) { finalState = VendingMachineVisualState.Broken; } else if (entity.Comp.Ejecting) { finalState = VendingMachineVisualState.Eject; } else if (entity.Comp.Denying) { finalState = VendingMachineVisualState.Deny; } else if (!_receiver.IsPowered(entity.Owner)) { finalState = VendingMachineVisualState.Off; } // TODO: You know this should really live on the client with netsync off because client knows the state. if (Light.TryGetLight(entity.Owner, out var pointlight)) { var lightEnabled = finalState != VendingMachineVisualState.Broken && finalState != VendingMachineVisualState.Off; Light.SetEnabled(entity.Owner, lightEnabled, pointlight); } _appearanceSystem.SetData(entity.Owner, VendingMachineVisuals.VisualState, finalState); } /// /// Checks whether the user is authorized to use the vending machine, then ejects the provided item if true /// /// /// Entity that is trying to use the vending machine /// The type of inventory the item is from /// The prototype ID of the item /// public void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component) { if (IsAuthorized(uid, sender, component)) { TryEjectVendorItem(uid, type, itemId, component.CanShoot, sender, component); } } public void RestockInventoryFromPrototype(EntityUid uid, VendingMachineComponent? component = null, float restockQuality = 1f) { if (!Resolve(uid, ref component)) { return; } if (!PrototypeManager.TryIndex(component.PackPrototypeId, out VendingMachineInventoryPrototype? packPrototype)) return; AddInventoryFromPrototype(uid, packPrototype.StartingInventory, InventoryType.Regular, component, restockQuality); AddInventoryFromPrototype(uid, packPrototype.EmaggedInventory, InventoryType.Emagged, component, restockQuality); AddInventoryFromPrototype(uid, packPrototype.ContrabandInventory, InventoryType.Contraband, component, restockQuality); Dirty(uid, component); } private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) return; if (_emag.CheckFlag(uid, EmagType.Interaction)) return; // only emag if there are emag-only items args.Handled = component.EmaggedInventory.Count > 0; } /// /// Returns all of the vending machine's inventory. Only includes emagged and contraband inventories if /// with the EmagType.Interaction flag exists and is true /// are true respectively. /// /// /// /// public List GetAllInventory(EntityUid uid, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return new(); var inventory = new List(component.Inventory.Values); if (_emag.CheckFlag(uid, EmagType.Interaction)) inventory.AddRange(component.EmaggedInventory.Values); if (component.Contraband) inventory.AddRange(component.ContrabandInventory.Values); return inventory; } public List GetAvailableInventory(EntityUid uid, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return new(); return GetAllInventory(uid, component).Where(_ => _.Amount > 0).ToList(); } private void AddInventoryFromPrototype(EntityUid uid, Dictionary? entries, InventoryType type, VendingMachineComponent? component = null, float restockQuality = 1.0f) { if (!Resolve(uid, ref component) || entries == null) { return; } Dictionary inventory; switch (type) { case InventoryType.Regular: inventory = component.Inventory; break; case InventoryType.Emagged: inventory = component.EmaggedInventory; break; case InventoryType.Contraband: inventory = component.ContrabandInventory; break; default: return; } foreach (var (id, amount) in entries) { if (PrototypeManager.HasIndex(id)) { var restock = amount; var chanceOfMissingStock = 1 - restockQuality; var result = Randomizer.NextFloat(0, 1); if (result < chanceOfMissingStock) { restock = (uint) Math.Floor(amount * result / chanceOfMissingStock); } if (inventory.TryGetValue(id, out var entry)) // Prevent a machine's stock from going over three times // the prototype's normal amount. This is an arbitrary // number and meant to be a convenience for someone // restocking a machine who doesn't want to force vend out // all the items just to restock one empty slot without // losing the rest of the restock. entry.Amount = Math.Min(entry.Amount + amount, 3 * restock); else inventory.Add(id, new VendingMachineInventoryEntry(type, id, restock)); } } } }