diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml index 493d1ded795e88..59761f9dbdebbd 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml @@ -158,6 +158,10 @@ + + + + diff --git a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs index fe0994a4bf84e1..2efff20b452ae3 100644 --- a/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs +++ b/Content.Client/Preferences/UI/HumanoidProfileEditor.xaml.cs @@ -73,6 +73,8 @@ public sealed partial class HumanoidProfileEditor : Control private SingleMarkingPicker _facialHairPicker => CFacialHairPicker; private EyeColorPicker _eyesPicker => CEyeColorPicker; + private CheckBox _teleportAfkToCryoStorage => CTeleportAfkToCryoStorage; + private TabContainer _tabContainer => CTabContainer; private BoxContainer _jobList => CJobList; private BoxContainer _antagList => CAntagList; @@ -513,6 +515,14 @@ public HumanoidProfileEditor(IClientPreferencesManager preferencesManager, IProt }; _previewSpriteSideControl.AddChild(_previewSpriteSide); #endregion Dummy + + #region TeleportAfkToCryoStorage + + _tabContainer.SetTabTitle(5, Loc.GetString("humanoid-profile-edtior-afkPreferences-tab")); + _teleportAfkToCryoStorage.Pressed = Profile?.TeleportAfkToCryoStorage ?? true; + _teleportAfkToCryoStorage.OnToggled += args => SetTeleportAfkToCryoStorage(args.Pressed); + + #endregion TeleportAfkToCryoStorage #endregion Left @@ -845,6 +855,12 @@ private void SetBackpack(BackpackPreference newBackpack) IsDirty = true; } + private void SetTeleportAfkToCryoStorage(bool newTeleportAfkToCryoStorage) + { + Profile = Profile?.WithTeleportAfkToCryoStorage(newTeleportAfkToCryoStorage); + IsDirty = true; + } + public void Save() { IsDirty = false; diff --git a/Content.Server/SS220/CryopodSSD/AfkSSDSystem.cs b/Content.Server/SS220/CryopodSSD/AfkSSDSystem.cs new file mode 100644 index 00000000000000..2c35010fb09772 --- /dev/null +++ b/Content.Server/SS220/CryopodSSD/AfkSSDSystem.cs @@ -0,0 +1,101 @@ +// © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt + +using System.Linq; +using Content.Server.Afk; +using Content.Server.GameTicking; +using Content.Server.Mind.Components; +using Content.Server.Preferences.Managers; +using Content.Shared.Body.Components; +using Content.Shared.CCVar; +using Content.Shared.Preferences; +using Robust.Server.Player; +using Robust.Shared.Configuration; +using Robust.Shared.Enums; +using Robust.Shared.Network; +using Robust.Shared.Timing; +using Robust.Shared.Utility; + +namespace Content.Server.SS220.CryopodSSD; + +public sealed class AfkSSDSystem : EntitySystem +{ + [Dependency] private readonly CryopodSSDSystem _cryopodSSDSystem = default!; + [Dependency] private readonly IServerPreferencesManager _preferencesManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + [Dependency] private readonly IConfigurationManager _cfg = default!; + + private float _afkTeleportTocryo; + + private readonly Dictionary<(EntityUid, NetUserId), (TimeSpan, bool)> _entityEnteredSSDTimes = new(); + + public override void Initialize() + { + base.Initialize(); + + _cfg.OnValueChanged(CCVars.AfkTeleportToCryo, SetAfkTeleportToCryo, true); + _playerManager.PlayerStatusChanged += OnPlayerChange; + } + + private void SetAfkTeleportToCryo(float value) + => _afkTeleportTocryo = value; + + public override void Shutdown() + { + base.Shutdown(); + + _cfg.UnsubValueChanged(CCVars.AfkTeleportToCryo, SetAfkTeleportToCryo); + _playerManager.PlayerStatusChanged -= OnPlayerChange; + } + + public override void Update(float frameTime) + { + base.Update(frameTime); + foreach (var pair in _entityEnteredSSDTimes.Where(uid => HasComp(uid.Key.Item1))) + { + if (pair.Value.Item2 && IsTeleportAfkToCryoTime(pair.Value.Item1) + && _cryopodSSDSystem.TeleportEntityToCryoStorageWithDelay(pair.Key.Item1)) + { + _entityEnteredSSDTimes.Remove(pair.Key); + } + } + } + + private bool IsTeleportAfkToCryoTime(TimeSpan time) + { + var timeOut = TimeSpan.FromSeconds(_afkTeleportTocryo); + return _gameTiming.CurTime - time > timeOut; + } + + private void OnPlayerChange(object? sender, SessionStatusEventArgs e) + { + + switch (e.NewStatus) + { + case SessionStatus.Disconnected: + if (e.Session.AttachedEntity is null + || !HasComp(e.Session.AttachedEntity) + || !HasComp(e.Session.AttachedEntity)) + { + break; + } + + if (!_preferencesManager.TryGetCachedPreferences(e.Session.UserId, out var preferences) + || preferences.SelectedCharacter is not HumanoidCharacterProfile humanoidPreferences) + { + break; + } + _entityEnteredSSDTimes[(e.Session.AttachedEntity.Value, e.Session.UserId)] + = (_gameTiming.CurTime, humanoidPreferences.TeleportAfkToCryoStorage); + break; + case SessionStatus.Connected: + if (_entityEnteredSSDTimes + .TryFirstOrNull(item => item.Key.Item2 == e.Session.UserId, out var item)) + { + _entityEnteredSSDTimes.Remove(item.Value.Key); + } + + break; + } + } +} \ No newline at end of file diff --git a/Content.Server/SS220/CryopodSSD/CryopodSSDSystem.cs b/Content.Server/SS220/CryopodSSD/CryopodSSDSystem.cs index 4f1d78589b1c65..e33f0949331a8e 100644 --- a/Content.Server/SS220/CryopodSSD/CryopodSSDSystem.cs +++ b/Content.Server/SS220/CryopodSSD/CryopodSSDSystem.cs @@ -1,8 +1,11 @@ // © SS220, An EULA/CLA with a hosting restriction, full text: https://raw.githubusercontent.com/SerbiaStrong-220/space-station-14/master/CLA.txt using Content.Server.Mind.Components; +using Content.Server.Station.Systems; +using Content.Shared.Audio; using Content.Shared.CCVar; using Content.Shared.DoAfter; using Content.Shared.DragDrop; +using Content.Shared.Mobs.Systems; using Content.Shared.SS220.CryopodSSD; using Content.Shared.Verbs; using Robust.Shared.Configuration; @@ -20,6 +23,9 @@ public sealed class CryopodSSDSystem : SharedCryopodSSDSystem { [Dependency] private readonly SSDStorageConsoleSystem _SSDStorageConsoleSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; + [Dependency] private readonly MobStateSystem _mobStateSystem = default!; + [Dependency] private readonly SharedAudioSystem _audioSystem = default!; + [Dependency] private readonly StationSystem _stationSystem = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; private ISawmill _sawmill = default!; @@ -35,10 +41,11 @@ public override void Initialize() _cfg.OnValueChanged(CCVars.AutoTransferToCryoDelay, SetAutoTransferDelay, true); SubscribeLocalEvent(OnComponentInit); - + SubscribeLocalEvent>(AddAlternativeVerbs); SubscribeLocalEvent(OnCryopodSSDLeaveAction); + SubscribeLocalEvent(OnTeleportFinished); SubscribeLocalEvent(OnDragFinished); SubscribeLocalEvent(HandleDragDropOn); } @@ -62,6 +69,13 @@ public override void Update(float frameTime) TransferToCryoStorage(uid, cryopodSSDComp); } } + + public override void Shutdown() + { + base.Shutdown(); + + _cfg.UnsubValueChanged(CCVars.AutoTransferToCryoDelay, SetAutoTransferDelay); + } /// /// Ejects body from cryopod @@ -84,6 +98,73 @@ public override void Update(float frameTime) base.EjectBody(uid, cryopodSsdComponent); return contained; } + + + /// + /// Tries to teleport target inside cryopod, if any available + /// + /// Target to teleport in first matching cryopod + /// true if player successfully transferred to cryo storage, otherwise returns false + public bool TeleportEntityToCryoStorageWithDelay(EntityUid target) + { + var station = _stationSystem.GetOwningStation(target); + + if (station is null) + { + return false; + } + + foreach (var comp in EntityQuery()) + { + if (comp.BodyContainer.ContainedEntity == target) + { + return true; + } + } + + var cryopodSSDComponents = EntityQueryEnumerator(); + + while (cryopodSSDComponents + .MoveNext(out var cryopodSSDUid, out var cryopodSSDComp)) + { + if (cryopodSSDComp.BodyContainer.ContainedEntity is null + && _stationSystem.GetOwningStation(cryopodSSDUid) == station) + { + var portal = Spawn("CryoStoragePortal", Transform(target).Coordinates); + + if (TryComp(portal, out var ambientSoundComponent)) + { + _audioSystem.PlayPvs(ambientSoundComponent.Sound, portal); + } + + var doAfterArgs = new DoAfterArgs(target, cryopodSSDComp.EntryDelay, new TeleportToCryoFinished(portal), cryopodSSDUid) + { + BreakOnDamage = false, + BreakOnTargetMove = false, + BreakOnUserMove = true, + NeedHand = false, + }; + + _doAfterSystem.TryStartDoAfter(doAfterArgs); + return true; + } + } + + return false; + } + + private void OnTeleportFinished(EntityUid uid, CryopodSSDComponent component, TeleportToCryoFinished args) + { + InsertBody(uid, args.User, component); + TransferToCryoStorage(uid, component); + + if (TryComp(args.PortalId, out var ambientSoundComponent)) + { + _audioSystem.PlayPvs(ambientSoundComponent.Sound, args.PortalId); + } + + EntityManager.DeleteEntity(args.PortalId); + } private void SetAutoTransferDelay(float value) => _autoTransferDelay = value; @@ -96,7 +177,13 @@ private void HandleDragDropOn(EntityUid uid, CryopodSSDComponent cryopodSsdCompo if (!TryComp(args.Dragged, out MindComponent? mind) || !mind.HasMind) { - _sawmill.Error($"{ToPrettyString(args.User)} tries to put non-playable entity into SSD cryopod {ToPrettyString(args.Dragged)}"); + _sawmill.Info($"{ToPrettyString(args.User)} tries to put non-playable entity into SSD cryopod {ToPrettyString(args.Dragged)}"); + return; + } + + if (_mobStateSystem.IsDead(args.Dragged)) + { + _sawmill.Log(LogLevel.Warning,$"{ToPrettyString(args.User)} tries to put dead entity(passing client check) {ToPrettyString(args.Dragged)} into SSD cryopod, potentially client exploit"); return; } diff --git a/Content.Shared/CCVar/CCVars.cs b/Content.Shared/CCVar/CCVars.cs index 138c7afd642e42..e66959bd4ce8f7 100644 --- a/Content.Shared/CCVar/CCVars.cs +++ b/Content.Shared/CCVar/CCVars.cs @@ -1341,6 +1341,9 @@ public static readonly CVarDef public static readonly CVarDef AfkTimeKick = CVarDef.Create("afk.time_kick", 600f, CVar.SERVERONLY); + public static readonly CVarDef AfkTeleportToCryo = + CVarDef.Create("afk.teleport_to_cryo", 1800f, CVar.SERVERONLY); + /* * IC */ diff --git a/Content.Shared/Preferences/HumanoidCharacterProfile.cs b/Content.Shared/Preferences/HumanoidCharacterProfile.cs index e450407630dd01..b40310f605cb55 100644 --- a/Content.Shared/Preferences/HumanoidCharacterProfile.cs +++ b/Content.Shared/Preferences/HumanoidCharacterProfile.cs @@ -46,7 +46,8 @@ private HumanoidCharacterProfile( Dictionary jobPriorities, PreferenceUnavailableMode preferenceUnavailable, List antagPreferences, - List traitPreferences) + List traitPreferences, + bool teleportAfkToCryoStorage = true) { Name = name; FlavorText = flavortext; @@ -62,6 +63,7 @@ private HumanoidCharacterProfile( PreferenceUnavailable = preferenceUnavailable; _antagPreferences = antagPreferences; _traitPreferences = traitPreferences; + TeleportAfkToCryoStorage = teleportAfkToCryoStorage; } /// Copy constructor but with overridable references (to prevent useless copies) @@ -71,7 +73,7 @@ private HumanoidCharacterProfile( List antagPreferences, List traitPreferences) : this(other.Name, other.FlavorText, other.Species, other.Voice, other.Age, other.Sex, other.Gender, other.Appearance, other.Clothing, other.Backpack, - jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences) + jobPriorities, other.PreferenceUnavailable, antagPreferences, traitPreferences, other.TeleportAfkToCryoStorage) { } @@ -224,6 +226,7 @@ public static HumanoidCharacterProfile RandomWithSpecies(string species = Shared public IReadOnlyList AntagPreferences => _antagPreferences; public IReadOnlyList TraitPreferences => _traitPreferences; public PreferenceUnavailableMode PreferenceUnavailable { get; private set; } + public bool TeleportAfkToCryoStorage { get; private set; } = true; public HumanoidCharacterProfile WithName(string name) { @@ -346,6 +349,9 @@ public HumanoidCharacterProfile WithTraitPreference(string traitId, bool pref) return new(this, _jobPriorities, _antagPreferences, list); } + public HumanoidCharacterProfile WithTeleportAfkToCryoStorage(bool teleportAfkToCryoStorage) + => new(this) { TeleportAfkToCryoStorage = teleportAfkToCryoStorage }; + public string Summary => Loc.GetString( "humanoid-character-profile-summary", diff --git a/Content.Shared/SS220/CryopodSSD/SharedCryopodSSDSystem.cs b/Content.Shared/SS220/CryopodSSD/SharedCryopodSSDSystem.cs index 1525a5834dadff..005802893d2988 100644 --- a/Content.Shared/SS220/CryopodSSD/SharedCryopodSSDSystem.cs +++ b/Content.Shared/SS220/CryopodSSD/SharedCryopodSSDSystem.cs @@ -54,7 +54,7 @@ public bool InsertBody(EntityUid uid, EntityUid target, CryopodSSDComponent cryo return false; var xform = Transform(target); - cryopodSsdComponent.BodyContainer.Insert(target, transform: xform); + cryopodSsdComponent.BodyContainer.Insert(target, transform: xform, force: true); if (_prototypeManager.TryIndex("CryopodSSDLeave", out var leaveAction)) { @@ -118,7 +118,7 @@ private void OnCryopodSSDCanDropTarget(EntityUid uid, CryopodSSDComponent compon if (args.Handled) return; - args.CanDrop = HasComp(args.Dragged); + args.CanDrop = HasComp(args.Dragged) && _mobStateSystem.IsAlive(args.Dragged); args.Handled = true; } @@ -168,6 +168,17 @@ protected void AddAlternativeVerbs(EntityUid uid, CryopodSSDComponent cryopodSSD public sealed class CryopodSSDDragFinished : SimpleDoAfterEvent { } + + [Serializable, NetSerializable] + public sealed class TeleportToCryoFinished : SimpleDoAfterEvent + { + public EntityUid PortalId { get; private set; } + + public TeleportToCryoFinished(EntityUid portalId) + { + PortalId = portalId; + } + } } } diff --git a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl index ccd86ee30733b2..9e4ddfcaec5745 100644 --- a/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl +++ b/Resources/Locale/ru-RU/preferences/ui/humanoid-profile-editor.ftl @@ -39,3 +39,5 @@ humanoid-profile-editor-job-priority-never-button = Никогда humanoid-profile-editor-naming-rules-warning = Внимание: Оскорбительные или странные имена и описания могут повлечь за собой беседу с администрацией. Прочитайте \[Правила\]. humanoid-profile-editor-markings-tab = Черты внешности humanoid-profile-editor-flavortext-tab = Описание +humanoid-profile-edtior-afkPreferences-tab = ССД +humanoid-profile-edtior-afkPreferences-teleport-to-cryo-storage = Телепортация в крио хранилище при длительном ССД \ No newline at end of file diff --git a/Resources/Prototypes/SS220/CryopodSSD/CryopodSSD.yml b/Resources/Prototypes/SS220/CryopodSSD/CryopodSSD.yml index efac119da8c887..83d36ad6c50efe 100644 --- a/Resources/Prototypes/SS220/CryopodSSD/CryopodSSD.yml +++ b/Resources/Prototypes/SS220/CryopodSSD/CryopodSSD.yml @@ -32,6 +32,8 @@ type: CryopodSSDBoundUserInterface - type: SSDStorageConsole whitelist: + components: + - Stamp tags: - HighRiskItem - type: ContainerContainer @@ -64,7 +66,6 @@ sprite: SS220/Structures/Machines/cryopodSSD.rsi state: icon - type: CryopodSSD - autoTransferToCryoDelay: 900 - type: SSDStorageConsole isItReallyCryopod: true - type: ContainerContainer @@ -72,4 +73,21 @@ storagebase: !type:Container pod-body: !type:ContainerSlot - showEnts: false \ No newline at end of file + showEnts: false + +- type: entity + id: CryoStoragePortal + noSpawn: true + name: портал в крио + components: + - type: Sprite + sprite: /Textures/Effects/portal.rsi + state: portal-blue + - type: PointLight + netsync: false + color: SkyBlue + radius: 3 + energy: 3 + - type: AmbientSound + enabled: false + sound: /Audio/Effects/teleport_departure.ogg