using Content.Server.Cargo.Systems; using Content.Server.Popups; 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.Emag.Systems; using Content.Shared.Throwing; using Content.Shared.VendingMachines; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Player; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.VendingMachines { public sealed class VendingMachineSystem : SharedVendingMachineSystem { [Dependency] private readonly IComponentFactory _factory = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly AppearanceSystem _appearanceSystem = default!; [Dependency] private readonly AudioSystem _audioSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = 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); } private void OnVendingPrice(EntityUid uid, VendingMachineComponent component, ref PriceCalculationEvent args) { var price = 0.0; foreach (var (id, entry) in component.Inventory) { 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, _factory); } args.Price += price; } protected override void OnComponentInit(EntityUid uid, SharedVendingMachineComponent sharedComponent, ComponentInit args) { base.OnComponentInit(uid, sharedComponent, args); var component = (VendingMachineComponent) sharedComponent; if (HasComp(component.Owner)) { 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(component); } private void UpdateVendingMachineInterfaceState(VendingMachineComponent component) { var state = new VendingMachineInterfaceState(GetAllInventory(component.Owner, component)); _userInterfaceSystem.TrySetUiState(component.Owner, 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, 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, GotEmaggedEvent args) { if (component.Emagged || component.EmaggedInventory.Count == 0 ) return; component.Emagged = true; args.Handled = true; } 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); } /// /// 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; _audioSystem.Play(vendComponent.SoundDeny, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, 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) || sender == null) return false; if (TryComp(vendComponent.Owner, out var accessReader)) { if (!_accessReader.IsAllowed(sender.Value, accessReader) && !vendComponent.Emagged) { _popupSystem.PopupEntity(Loc.GetString("vending-machine-component-try-eject-access-denied"), uid, Filter.Pvs(uid)); Deny(uid, vendComponent); return false; } } return true; } /// /// 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(itemId, type, vendComponent); if (entry == null) { _popupSystem.PopupEntity(Loc.GetString("vending-machine-component-try-eject-invalid-item"), uid, Filter.Pvs(uid)); Deny(uid, vendComponent); return; } if (entry.Amount <= 0) { _popupSystem.PopupEntity(Loc.GetString("vending-machine-component-try-eject-out-of-stock"), uid, Filter.Pvs(uid)); Deny(uid, vendComponent); return; } if (string.IsNullOrEmpty(entry.ID)) return; if (!TryComp(vendComponent.Owner, out var transformComp)) return; // Start Ejecting, and prevent users from ordering while anim playing vendComponent.Ejecting = true; vendComponent.NextItemToEject = entry.ID; vendComponent.ThrowNextItem = throwItem; entry.Amount--; UpdateVendingMachineInterfaceState(vendComponent); TryUpdateVisualState(uid, vendComponent); _audioSystem.Play(vendComponent.SoundVend, Filter.Pvs(vendComponent.Owner), vendComponent.Owner, AudioParams.Default.WithVolume(-2f)); } /// /// 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; } if (TryComp(vendComponent.Owner, out var appearance)) { _appearanceSystem.SetData(uid, VendingMachineVisuals.VisualState, finalState, appearance); } } /// /// 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(item.ID, item.Type, vendComponent); if (entry != null) entry.Amount--; EjectItem(vendComponent, forceEject); } else TryEjectVendorItem(uid, item.Type, item.ID, throwItem, vendComponent); } private void EjectItem(VendingMachineComponent vendComponent, bool forceEject = false) { // No need to update the visual state because we never changed it during a forced eject if (!forceEject) TryUpdateVisualState(vendComponent.Owner, vendComponent); if (string.IsNullOrEmpty(vendComponent.NextItemToEject)) { vendComponent.ThrowNextItem = false; return; } var ent = EntityManager.SpawnEntity(vendComponent.NextItemToEject, Transform(vendComponent.Owner).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 void DenyItem(VendingMachineComponent vendComponent) { TryUpdateVisualState(vendComponent.Owner, vendComponent); } private VendingMachineInventoryEntry? GetEntry(string entryId, InventoryType type, VendingMachineComponent component) { if (type == InventoryType.Emagged && component.Emagged) 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); foreach (var comp in EntityQuery()) { if (comp.Ejecting) { comp.EjectAccumulator += frameTime; if (comp.EjectAccumulator >= comp.EjectDelay) { comp.EjectAccumulator = 0f; comp.Ejecting = false; EjectItem(comp); } } if (comp.Denying) { comp.DenyAccumulator += frameTime; if (comp.DenyAccumulator >= comp.DenyDelay) { comp.DenyAccumulator = 0f; comp.Denying = false; DenyItem(comp); } } if (comp.DispenseOnHitCoolingDown) { comp.DispenseOnHitAccumulator += frameTime; if (comp.DispenseOnHitAccumulator >= comp.DispenseOnHitCooldown) { comp.DispenseOnHitAccumulator = 0f; comp.DispenseOnHitCoolingDown = false; } } } } } }