using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Shared.FixedPoint; using Content.Shared.Implants; using Content.Shared.Interaction; using Content.Shared.Mind; using Content.Shared.Popups; using Content.Shared.Stacks; using Content.Shared.Store.Components; using Content.Shared.Store.Events; using Robust.Shared.Prototypes; namespace Content.Shared.Store; /// /// Manages general interactions with a store and different entities, /// getting listings for stores, and interfacing with the store UI. /// public abstract partial class SharedStoreSystem : EntitySystem { [Dependency] protected IPrototypeManager Proto = default!; [Dependency] protected SharedMindSystem Mind = default!; [Dependency] protected SharedPopupSystem Popup = default!; [Dependency] protected SharedStackSystem Stack = default!; [Dependency] protected SharedUserInterfaceSystem UI = default!; [Dependency] protected EntityQuery StoreQuery = default!; [Dependency] protected EntityQuery RemoteStoreQuery = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnAfterInteract); SubscribeLocalEvent(OnGetStore); SubscribeLocalEvent>((x, ref y) => { var ev = y.Args; OnGetStore(x, ref ev); y.Args = ev; }); SubscribeLocalEvent>(OnImplantInsertAttempt); SubscribeLocalEvent(OnIntrinsicStoreAction); } private void OnGetStore(Entity entity, ref GetStoreEvent args) { if (args.Handled) return; if (!StoreQuery.TryComp(entity.Comp.Store, out var store)) return; args.Store = (entity.Comp.Store.Value, store); } private void OnImplantInsertAttempt(Entity implant, ref ImplantRelayEvent args) { var ev = args.Args; // Only allow insertion if the person implanted is doing the action. if (ev.User == ev.Target) ev.TargetOverride = implant; else ev.Cancel(); args.Args = ev; } private void OnAfterInteract(EntityUid uid, CurrencyComponent component, AfterInteractEvent args) { if (args.Handled || !args.CanReach || args.Target is not { } target) return; if (!TryGetStore(target, out var store)) return; var ev = new CurrencyInsertAttemptEvent(args.User, target, args.Used, store.Value.Comp); RaiseLocalEvent(target, ev); if (ev.Cancelled) return; if (!TryAddCurrency((uid, component), (store.Value, store.Value.Comp))) return; args.Handled = true; var msg = Loc.GetString("store-currency-inserted", ("used", args.Used), ("target", ev.TargetOverride ?? target)); Popup.PopupEntity(msg, target, args.User); } /// /// Attempts to find a store connected to this entity. /// First checking for a on this entity, /// then checking for a to find a remotely connected store. /// /// Entity we're checking for an attached store on /// Store entity we're returning. /// True if a store was found. public bool TryGetStore(EntityUid entity, [NotNullWhen(true)] out Entity? store) { store = GetStore(entity); return store != null; } /// /// Attempts to find a store connected to this entity. /// First checking for a on this entity, /// then checking for a to find a remotely connected store. /// /// Entity we're checking for an attached store on /// The store entity and component if found. public Entity? GetStore(EntityUid entity) { if (StoreQuery.TryComp(entity, out var storeComp)) return (entity, storeComp); var ev = new GetStoreEvent(); RaiseLocalEvent(entity, ref ev); return ev.Store; } /// /// Attempts to find a remote store connected to this entity. /// Checking for a with an attached store entity. /// /// Entity we're checking for an attached store on /// The store entity and component if found. public Entity? GetRemoteStore(Entity entity) { if (RemoteStoreQuery.Resolve(entity, ref entity.Comp) && entity.Comp.Store != null && StoreQuery.TryComp(entity.Comp.Store, out var storeComp)) return (entity.Comp.Store.Value, storeComp); return null; } public void SetRemoteStore(Entity entity, EntityUid? store) { if (!RemoteStoreQuery.Resolve(entity, ref entity.Comp)) return; entity.Comp.Store = store; } /// /// 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); } } [ByRefEvent] public record struct GetStoreEvent { public readonly bool Handled => Store != null; public Entity? Store; } public sealed class CurrencyInsertAttemptEvent : CancellableEntityEventArgs { public readonly EntityUid User; public readonly EntityUid Target; public readonly EntityUid Used; public readonly StoreComponent Store; // An optional override for the "Target" of this interaction, used to change the name that pops up! public EntityUid? TargetOverride; public CurrencyInsertAttemptEvent(EntityUid user, EntityUid target, EntityUid used, StoreComponent store) { User = user; Target = target; Used = used; Store = store; } }