using System.Linq; using Content.Server.Store.Components; using Content.Shared.FixedPoint; using Content.Shared.Implants.Components; using Content.Shared.Interaction; using Content.Shared.Popups; using Content.Shared.Stacks; using Content.Shared.Store.Components; using Content.Shared.Store.Events; using Content.Shared.UserInterface; using Robust.Shared.Prototypes; using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Store.Systems; /// /// Manages general interactions with a store and different entities, /// getting listings for stores, and interfacing with the store UI. /// public sealed partial class StoreSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly SharedPopupSystem _popup = default!; [Dependency] private readonly IGameTiming _timing = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnStoreOpenAttempt); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(BeforeActivatableUiOpen); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnStartup); SubscribeLocalEvent(OnShutdown); SubscribeLocalEvent(OnImplantActivate); SubscribeLocalEvent(OnIntrinsicStoreAction); InitializeUi(); InitializeCommand(); InitializeRefund(); } private void OnMapInit(EntityUid uid, StoreComponent component, MapInitEvent args) { RefreshAllListings(component); component.StartingMap = Transform(uid).MapUid; } private void OnStartup(EntityUid uid, StoreComponent component, ComponentStartup args) { // for traitors, because the StoreComponent for the PDA can be added at any time. if (MetaData(uid).EntityLifeStage == EntityLifeStage.MapInitialized) { RefreshAllListings(component); } var ev = new StoreAddedEvent(); RaiseLocalEvent(uid, ref ev, true); } private void OnShutdown(EntityUid uid, StoreComponent component, ComponentShutdown args) { var ev = new StoreRemovedEvent(); RaiseLocalEvent(uid, ref ev, true); } private void OnStoreOpenAttempt(EntityUid uid, StoreComponent component, ActivatableUIOpenAttemptEvent args) { if (!component.OwnerOnly) return; if (!_mind.TryGetMind(args.User, out var mind, out _)) return; component.AccountOwner ??= mind; DebugTools.Assert(component.AccountOwner != null); if (component.AccountOwner == mind) return; _popup.PopupEntity(Loc.GetString("store-not-account-owner", ("store", uid)), uid, args.User); args.Cancel(); } private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) { if (args.Handled || !args.CanReach) return; if (!TryComp(args.Target, out var store)) return; var ev = new CurrencyInsertAttemptEvent(args.User, args.Target.Value, args.Used, store); RaiseLocalEvent(args.Target.Value, ev); if (ev.Cancelled) return; if (!TryAddCurrency((uid, component), (args.Target.Value, store))) return; args.Handled = true; var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", args.Target)); _popup.PopupEntity(msg, args.Target.Value, args.User); } private void OnImplantActivate(EntityUid uid, StoreComponent component, OpenUplinkImplantEvent args) { ToggleUi(args.Performer, uid, component); } /// /// Gets the value from an entity's currency component. /// Scales with stacks. /// /// /// If this result is intended to be used with , /// consider using instead to ensure that the currency is consumed in the process. /// /// /// /// The value of the currency public Dictionary GetCurrencyValue(EntityUid uid, CurrencyComponent component) { var amount = EntityManager.GetComponentOrNull(uid)?.Count ?? 1; return component.Price.ToDictionary(v => v.Key, p => p.Value * amount); } /// /// Tries to add a currency to a store's balance. Note that if successful, this will consume the currency in the process. /// public bool TryAddCurrency(Entity currency, Entity store) { if (!Resolve(currency.Owner, ref currency.Comp)) return false; if (!Resolve(store.Owner, ref store.Comp)) return false; var value = currency.Comp.Price; if (TryComp(currency.Owner, out StackComponent? stack) && stack.Count != 1) { value = currency.Comp.Price .ToDictionary(v => v.Key, p => p.Value * stack.Count); } if (!TryAddCurrency(value, store, store.Comp)) return false; // Avoid having the currency accidentally be re-used. E.g., if multiple clients try to use the currency in the // same tick currency.Comp.Price.Clear(); if (stack != null) _stack.SetCount((currency.Owner, stack), 0); QueueDel(currency); return true; } /// /// Tries to add a currency to a store's balance /// /// The value to add to the store /// /// The store to add it to /// Whether or not the currency was succesfully added public bool TryAddCurrency(Dictionary currency, EntityUid uid, StoreComponent? store = null) { if (!Resolve(uid, ref store)) return false; //verify these before values are modified foreach (var type in currency) { if (!store.CurrencyWhitelist.Contains(type.Key)) return false; } foreach (var type in currency) { if (!store.Balance.TryAdd(type.Key, type.Value)) store.Balance[type.Key] += type.Value; } UpdateUserInterface(null, uid, store); return true; } private void OnIntrinsicStoreAction(Entity ent, ref IntrinsicStoreActionEvent args) { ToggleUi(args.Performer, ent.Owner, ent.Comp); } } public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs { public readonly EntityUid User; public readonly EntityUid Target; public readonly EntityUid Used; public readonly StoreComponent Store; public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store) { User = user; Target = target; Used = used; Store = store; } }