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; using Content.Shared.Containers.ItemSlots; using Robust.Shared.Containers; using Content.Shared.Stacks; // Frontier 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!; [Dependency] protected readonly ItemSlotsSystem ItemSlots = default!; // Frontier public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnVendingGetState); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnUnemagged); // Frontier SubscribeLocalEvent(OnEntityInserted); // Frontier SubscribeLocalEvent(OnEntityRemoved); // Frontier 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, CashSlotBalance = component.CashSlotBalance, // Frontier }; } 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); // Frontier } protected virtual void OnMapInit(EntityUid uid, VendingMachineComponent component, MapInitEvent args) { RestockInventoryFromPrototype(uid, component, component.InitialStockQuality); // Frontier: create the cash slot if this entity has one if (component.CashSlot != null && component.CashSlotName != null) ItemSlots.AddItemSlot(uid, component.CashSlotName, component.CashSlot); // End Frontier } 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 bool TryEjectVendorItem(EntityUid uid, InventoryType type, string itemId, bool throwItem, EntityUid? user = null, VendingMachineComponent? vendComponent = null) // Frontier: void 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); } // Frontier: custom vending check public abstract void AuthorizedVend(EntityUid uid, EntityUid sender, InventoryType type, string itemId, VendingMachineComponent component); // End Frontier: custom vending check 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; } // Frontier: demag private void OnUnemagged(EntityUid uid, VendingMachineComponent component, ref GotUnEmaggedEvent args) { if (!_emag.CompareFlag(args.Type, EmagType.Interaction)) return; if (!_emag.CheckFlag(uid, EmagType.Interaction)) return; // Always demag if emagged. args.Handled = true; } // End Frontier /// /// 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); } // New Frontiers - Unlimited vending - support items with unlimited vending stock. // This code is licensed under AGPLv3. See AGPLv3.txt if (inventory.TryGetValue(id, out var entry)) { // Frontier: Max value is reserved for unlimited items, this should not be restocked. if (entry.Amount == uint.MaxValue) continue; // 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)); // End of modified code } } } // Frontier: cash slot handlers private void OnEntityInserted(Entity ent, ref EntInsertedIntoContainerMessage args) { if (ent.Comp.CashSlotName != null && ent.Comp.CurrencyStackType != null && ItemSlots.TryGetSlot(ent, ent.Comp.CashSlotName, out var slot) && TryComp(slot?.ContainerSlot?.ContainedEntity, out var stack) && stack.StackTypeId == ent.Comp.CurrencyStackType) { ent.Comp.CashSlotBalance = stack.Count; } else { ent.Comp.CashSlotBalance = 0; } Dirty(ent, ent.Comp); UpdateUI((ent.Owner, ent.Comp)); // nullable type, must be reconstructed } private void OnEntityRemoved(Entity ent, ref EntRemovedFromContainerMessage args) { ent.Comp.CashSlotBalance = 0; Dirty(ent, ent.Comp); UpdateUI((ent.Owner, ent.Comp)); // nullable type, must be reconstructed } // End Frontier: cash slot handlers }