using Content.Server.Cargo.Components; using Content.Server.Chat.Systems; using Content.Server.Station.Components; using Content.Shared._DV.Traitor; using Content.Shared.Bed.Sleep; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.Database; using Content.Shared.StatusEffect; using Content.Shared.Storage.EntitySystems; using Robust.Shared.Audio; using Robust.Shared.Map; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Cargo.Systems; /// /// Handles purchasing ransomed entities from a cargo request console. /// public sealed partial class CargoSystem { [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly SharedEntityStorageSystem _entityStorage = default!; [Dependency] private readonly StatusEffectsSystem _statusEffects = default!; /// /// The crate to put ransomed entities in when purchasing them. /// public static readonly EntProtoId RansomCrate = "CrateSyndicate"; /// /// Status effect for . /// public static readonly ProtoId StatusEffectKey = "ForcedSleep"; /// /// Sound to play for the ransom victim when being "trafficked" to the ATS. /// public static readonly SoundSpecifier HypoSound = new SoundPathSpecifier("/Audio/Items/hypospray.ogg"); /// /// How long to be slept for. /// public static readonly TimeSpan SleepyTime = TimeSpan.FromSeconds(10); private void InitializeRansom() { Subs.BuiEvents(CargoConsoleUiKey.Orders, subs => { subs.Event(OnPurchaseMessage); }); } private void OnPurchaseMessage(Entity ent, ref RansomPurchaseMessage args) { var user = args.Actor; if (!_accessReaderSystem.IsAllowed(user, ent)) { ConsolePopup(user, Loc.GetString("cargo-console-order-not-allowed")); PlayDenySound(ent, ent); return; } // malf client or they somehow got gibbed in jail if (GetEntity(args.Entity) is not { Valid: true } uid || // got released already !TryComp(uid, out var ransom) || // not on a station _station.GetOwningStation(uid) is not {} station || !TryComp(station, out var bank) || !TryComp(station, out var stationData)) { ConsolePopup(user, Loc.GetString("cargo-console-station-not-found")); PlayDenySound(ent, ent); return; } var cost = ransom.Ransom; var balance = bank.Accounts[bank.PrimaryAccount]; if (cost > balance) { ConsolePopup(user, Loc.GetString("cargo-console-insufficient-funds", ("cost", cost))); PlayDenySound(ent, ent); return; } // paid the ransom, time to bring em home if (TryReturnEntity(uid, stationData) is not {} trade) { ConsolePopup(user, Loc.GetString("cargo-console-unfulfilled")); PlayDenySound(ent, ent); return; } _audio.PlayPvs(ApproveSound, ent); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):user} paid the ransom of ${cost} for {ToPrettyString(uid)} with balance at {balance}"); UpdateBankAccount((station, bank), -cost, CreateAccountDistribution((station, bank))); // announce it so everyone knows var msg = Loc.GetString("syndicate-ransom-return-announcement", ("station", trade)); var sender = Loc.GetString("syndicate-ransom-return-announcement-sender"); var sound = new SoundPathSpecifier("/Audio/Misc/notice1.ogg"); var color = Color.Red; _chat.DispatchGlobalAnnouncement(msg, sender, playSound: true, sound, color); } // like TryFulfillOrder but for ransoms private EntityUid? TryReturnEntity(EntityUid uid, StationDataComponent station) { _listEnts.Clear(); GetTradeStations(station, ref _listEnts); // Try to fulfill from any station where possible, if the pad is not occupied. foreach (var trade in _listEnts) { var tradePads = GetCargoPallets(trade, BuySellType.Buy); _random.Shuffle(tradePads); var freePads = GetFreeCargoPallets(trade, tradePads); if (freePads.Count == 0) continue; // sleepy time _audio.PlayPvs(HypoSound, uid); _statusEffects.TryAddStatusEffect(uid, StatusEffectKey, SleepyTime, refresh: false); var pad = _random.Pick(freePads); var coordinates = new EntityCoordinates(trade, pad.Transform.LocalPosition); var crate = Spawn(RansomCrate, coordinates); if (!_entityStorage.Insert(uid, crate)) _transformSystem.DropNextTo(uid, crate); // just teleport directly if it somehow fails return trade; } return null; } }