using System.Linq; using Content.Server.Cargo.Systems; using Content.Server.Power.Components; using Content.Server.Power.EntitySystems; using Content.Server.UserInterface; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Actions; using Content.Shared.Actions.ActionTypes; using Content.Shared.Damage; using Content.Shared.Destructible; using Content.Shared.DoAfter; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Popups; using Content.Shared.Throwing; using Content.Shared.VendingMachines; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.VendingMachines { public sealed class VendingMachineSystem : SharedVendingMachineSystem { [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly SharedActionsSystem _action = default!; [Dependency] private readonly PricingSystem _pricing = default!; [Dependency] private readonly ThrowingSystem _throwingSystem = default!; [Dependency] private readonly UserInterfaceSystem _userInterfaceSystem = default!; private ISawmill _sawmill = default!; public override void Initialize() { base.Initialize(); _sawmill = Logger.GetSawmill("vending"); SubscribeLocalEvent(OnPowerChanged); SubscribeLocalEvent(OnBreak); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnDamage); SubscribeLocalEvent(OnVendingPrice); SubscribeLocalEvent(OnActivatableUIOpenAttempt); SubscribeLocalEvent(OnBoundUIOpened); SubscribeLocalEvent(OnInventoryEjectMessage); SubscribeLocalEvent(OnSelfDispense); SubscribeLocalEvent(OnDoAfter); SubscribeLocalEvent(OnPriceCalculation); } private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args) { var price = 0.0; foreach (var entry in component.Inventory.Values) { if (!PrototypeManager.TryIndex(entry.ID, out var proto)) { _sawmill.Error($"Unable to find entity prototype {entry.ID} on {ToPrettyString(uid)} vending."); continue; } price += entry.Amount * _pricing.GetEstimatedPrice(proto); } args.Price += price; } protected override void OnComponentInit(EntityUid uid, VendingMachineComponent component, ComponentInit args) { base.OnComponentInit(uid, component, args); if (HasComp(uid)) { TryUpdateVisualState(uid, component); } if (component.Action != null) { var action = new InstantAction(PrototypeManager.Index(component.Action)); _action.AddAction(uid, action, uid); } } private void OnActivatableUIOpenAttempt(EntityUid uid, VendingMachineComponent component, ActivatableUIOpenAttemptEvent args) { if (component.Broken) args.Cancel(); } private void OnBoundUIOpened(EntityUid uid, VendingMachineComponent component, BoundUIOpenedEvent args) { UpdateVendingMachineInterfaceState(uid, component); } private void UpdateVendingMachineInterfaceState(EntityUid uid, VendingMachineComponent component) { var state = new VendingMachineInterfaceState(GetAllInventory(uid, component)); _userInterfaceSystem.TrySetUiState(uid, VendingMachineUiKey.Key, state); } private void OnInventoryEjectMessage(EntityUid uid, VendingMachineComponent component, VendingMachineEjectMessage args) { if (!this.IsPowered(uid, EntityManager)) return; if (args.Session.AttachedEntity is not { Valid: true } entity || Deleted(entity)) return; AuthorizedVend(uid, entity, args.Type, args.ID, component); } private void OnPowerChanged(EntityUid uid, VendingMachineComponent component, ref PowerChangedEvent args) { TryUpdateVisualState(uid, component); } private void OnBreak(EntityUid uid, VendingMachineComponent vendComponent, BreakageEventArgs eventArgs) { vendComponent.Broken = true; TryUpdateVisualState(uid, vendComponent); } private void OnEmagged(EntityUid uid, VendingMachineComponent component, ref GotEmaggedEvent args) { // only emag if there are emag-only items args.Handled = component.EmaggedInventory.Count > 0; } private void OnDamage(EntityUid uid, VendingMachineComponent component, DamageChangedEvent args) { if (component.Broken || component.DispenseOnHitCoolingDown || component.DispenseOnHitChance == null || args.DamageDelta == null) return; if (args.DamageIncreased && args.DamageDelta.Total >= component.DispenseOnHitThreshold && _random.Prob(component.DispenseOnHitChance.Value)) { if (component.DispenseOnHitCooldown > 0f) component.DispenseOnHitCoolingDown = true; EjectRandom(uid, throwItem: true, forceEject: true, component); } } private void OnSelfDispense(EntityUid uid, VendingMachineComponent component, VendingMachineSelfDispenseEvent args) { if (args.Handled) return; args.Handled = true; EjectRandom(uid, throwItem: true, forceEject: false, component); } private void OnDoAfter(EntityUid uid, VendingMachineComponent component, DoAfterEvent args) { if (args.Handled || args.Cancelled || args.Args.Used == null) return; if (!TryComp(args.Args.Used, out var restockComponent)) { _sawmill.Error($"{ToPrettyString(args.Args.User)} tried to restock {ToPrettyString(uid)} with {ToPrettyString(args.Args.Used.Value)} which did not have a VendingMachineRestockComponent."); return; } TryRestockInventory(uid, component); Popup.PopupEntity(Loc.GetString("vending-machine-restock-done", ("this", args.Args.Used), ("user", args.Args.User), ("target", uid)), args.Args.User, PopupType.Medium); Audio.PlayPvs(restockComponent.SoundRestockDone, uid, AudioParams.Default.WithVolume(-2f).WithVariation(0.2f)); Del(args.Args.Used.Value); args.Handled = true; } /// /// Sets the property of the vending machine. /// public void SetShooting(EntityUid uid, bool canShoot, VendingMachineComponent? component = null) { if (!Resolve(uid, ref component)) return; component.CanShoot = canShoot; } public void Deny(EntityUid uid, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; if (vendComponent.Denying) return; vendComponent.Denying = true; Audio.PlayPvs(vendComponent.SoundDeny, uid, AudioParams.Default.WithVolume(-2f)); TryUpdateVisualState(uid, vendComponent); } /// /// 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, accessReader) || HasComp(uid)) return true; Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid); Deny(uid, vendComponent); return false; } /// /// 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, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; if (vendComponent.Ejecting || vendComponent.Broken || !this.IsPowered(uid, EntityManager)) { return; } var entry = GetEntry(uid, itemId, type, vendComponent); if (entry == null) { Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid); Deny(uid, vendComponent); return; } if (entry.Amount <= 0) { Popup.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid); Deny(uid, vendComponent); return; } if (string.IsNullOrEmpty(entry.ID)) return; // Start Ejecting, and prevent users from ordering while anim playing vendComponent.Ejecting = true; vendComponent.NextItemToEject = entry.ID; vendComponent.ThrowNextItem = throwItem; entry.Amount--; UpdateVendingMachineInterfaceState(uid, vendComponent); TryUpdateVisualState(uid, vendComponent); Audio.PlayPvs(vendComponent.SoundVend, uid); } /// /// 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, component); } } /// /// Tries to update the visuals of the component based on its current state. /// public void TryUpdateVisualState(EntityUid uid, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; var finalState = VendingMachineVisualState.Normal; if (vendComponent.Broken) { finalState = VendingMachineVisualState.Broken; } else if (vendComponent.Ejecting) { finalState = VendingMachineVisualState.Eject; } else if (vendComponent.Denying) { finalState = VendingMachineVisualState.Deny; } else if (!this.IsPowered(uid, EntityManager)) { finalState = VendingMachineVisualState.Off; } _appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState); } /// /// Ejects a random item from the available stock. Will do nothing if the vending machine is empty. /// /// /// Whether to throw the item in a random direction after dispensing it. /// Whether to skip the regular ejection checks and immediately dispense the item without animation. /// public void EjectRandom(EntityUid uid, bool throwItem, bool forceEject = false, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; var availableItems = GetAvailableInventory(uid, vendComponent); if (availableItems.Count <= 0) return; var item = _random.Pick(availableItems); if (forceEject) { vendComponent.NextItemToEject = item.ID; vendComponent.ThrowNextItem = throwItem; var entry = GetEntry(uid, item.ID, item.Type, vendComponent); if (entry != null) entry.Amount--; EjectItem(uid, vendComponent, forceEject); } else { TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent); } } private void EjectItem(EntityUid uid, VendingMachineComponent? vendComponent = null, bool forceEject = false) { if (!Resolve(uid, ref vendComponent)) return; // No need to update the visual state because we never changed it during a forced eject if (!forceEject) TryUpdateVisualState(uid, vendComponent); if (string.IsNullOrEmpty(vendComponent.NextItemToEject)) { vendComponent.ThrowNextItem = false; return; } var ent = Spawn(vendComponent.NextItemToEject, Transform(uid).Coordinates); if (vendComponent.ThrowNextItem) { var range = vendComponent.NonLimitedEjectRange; var direction = new Vector2(_random.NextFloat(-range, range), _random.NextFloat(-range, range)); _throwingSystem.TryThrow(ent, direction, vendComponent.NonLimitedEjectForce); } vendComponent.NextItemToEject = null; vendComponent.ThrowNextItem = false; } private 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); } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { if (comp.Ejecting) { comp.EjectAccumulator += frameTime; if (comp.EjectAccumulator >= comp.EjectDelay) { comp.EjectAccumulator = 0f; comp.Ejecting = false; EjectItem(uid, comp); } } if (comp.Denying) { comp.DenyAccumulator += frameTime; if (comp.DenyAccumulator >= comp.DenyDelay) { comp.DenyAccumulator = 0f; comp.Denying = false; TryUpdateVisualState(uid, comp); } } if (comp.DispenseOnHitCoolingDown) { comp.DispenseOnHitAccumulator += frameTime; if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown) { comp.DispenseOnHitAccumulator = 0f; comp.DispenseOnHitCoolingDown = false; } } } } public void TryRestockInventory(EntityUid uid, VendingMachineComponent? vendComponent = null) { if (!Resolve(uid, ref vendComponent)) return; RestockInventoryFromPrototype(uid, vendComponent); UpdateVendingMachineInterfaceState(uid, vendComponent); TryUpdateVisualState(uid, vendComponent); } private void OnPriceCalculation(EntityUid uid, VendingMachineRestockComponent component, ref PriceCalculationEvent args) { List priceSets = new(); // Find the most expensive inventory and use that as the highest price. foreach (var vendingInventory in component.CanRestock) { double total = 0; if (PrototypeManager.TryIndex(vendingInventory, out VendingMachineInventoryPrototype? inventoryPrototype)) { foreach (var (item, amount) in inventoryPrototype.StartingInventory) { if (PrototypeManager.TryIndex(item, out EntityPrototype? entity)) total += _pricing.GetEstimatedPrice(entity) * amount; } } priceSets.Add(total); } args.Price += priceSets.Max(); } } }