]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Cryogenic Sleep Units (#24096)
authorNemanja <98561806+EmoGarbage404@users.noreply.github.com>
Mon, 15 Jan 2024 06:35:28 +0000 (01:35 -0500)
committerGitHub <noreply@github.com>
Mon, 15 Jan 2024 06:35:28 +0000 (23:35 -0700)
* Cryogenic sleep units

* pause map support

* no more body deletion

* Cryogenic Storage Units

* boowomp

* no more emag, no more dropping present people

38 files changed:
Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml.cs [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml.cs [new file with mode: 0644]
Content.Client/Bed/Cryostorage/CryostorageSystem.cs [new file with mode: 0644]
Content.Server/Bed/Cryostorage/CryostorageSystem.cs [new file with mode: 0644]
Content.Server/GameTicking/GameTicker.Spawning.cs
Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs [new file with mode: 0644]
Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs [new file with mode: 0644]
Content.Server/Station/Components/StationJobsComponent.cs
Content.Server/Station/Systems/StationJobsSystem.cs
Content.Shared/Access/Components/AccessReaderComponent.cs
Content.Shared/Access/Components/IdCardConsoleComponent.cs
Content.Shared/Access/Systems/AccessReaderSystem.cs
Content.Shared/Bed/Cryostorage/CryostorageComponent.cs [new file with mode: 0644]
Content.Shared/Bed/Cryostorage/CryostorageContainedComponent.cs [new file with mode: 0644]
Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs [new file with mode: 0644]
Content.Shared/CCVar/CCVars.cs
Content.Shared/Climbing/Systems/ClimbSystem.cs
Content.Shared/Containers/DragInsertContainerComponent.cs [new file with mode: 0644]
Content.Shared/Containers/DragInsertContainerSystem.cs [new file with mode: 0644]
Content.Shared/Containers/ExitContainerOnMoveComponent.cs [new file with mode: 0644]
Content.Shared/Containers/ExitContainerOnMoveSystem.cs [new file with mode: 0644]
Content.Shared/Inventory/InventorySystem.Slots.cs
Resources/Locale/en-US/containers/containers.ftl [new file with mode: 0644]
Resources/Locale/en-US/prototypes/access/accesses.ftl
Resources/Locale/en-US/round-end/cryostorage.ftl [new file with mode: 0644]
Resources/Prototypes/Access/command.yml
Resources/Prototypes/Access/misc.yml
Resources/Prototypes/Access/security.yml
Resources/Prototypes/Entities/Structures/cryopod.yml [new file with mode: 0644]
Resources/Textures/Structures/cryostorage.rsi/meta.json [new file with mode: 0644]
Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png [new file with mode: 0644]
Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png [new file with mode: 0644]
SpaceStation14.sln.DotSettings

diff --git a/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs b/Content.Client/Bed/Cryostorage/CryostorageBoundUserInterface.cs
new file mode 100644 (file)
index 0000000..ffab162
--- /dev/null
@@ -0,0 +1,56 @@
+using Content.Shared.Bed.Cryostorage;
+using JetBrains.Annotations;
+
+namespace Content.Client.Bed.Cryostorage;
+
+[UsedImplicitly]
+public sealed class CryostorageBoundUserInterface : BoundUserInterface
+{
+    [ViewVariables]
+    private CryostorageMenu? _menu;
+
+    public CryostorageBoundUserInterface(EntityUid owner, Enum uiKey) : base(owner, uiKey)
+    {
+    }
+
+    protected override void Open()
+    {
+        base.Open();
+
+        _menu = new();
+
+        _menu.OnClose += Close;
+
+        _menu.SlotRemoveButtonPressed += (ent, slot) =>
+        {
+            SendMessage(new CryostorageRemoveItemBuiMessage(ent, slot, CryostorageRemoveItemBuiMessage.RemovalType.Inventory));
+        };
+
+        _menu.HandRemoveButtonPressed += (ent, hand) =>
+        {
+            SendMessage(new CryostorageRemoveItemBuiMessage(ent, hand, CryostorageRemoveItemBuiMessage.RemovalType.Hand));
+        };
+
+        _menu.OpenCentered();
+    }
+
+    protected override void UpdateState(BoundUserInterfaceState state)
+    {
+        base.UpdateState(state);
+
+        switch (state)
+        {
+            case CryostorageBuiState msg:
+                _menu?.UpdateState(msg);
+                break;
+        }
+    }
+
+    protected override void Dispose(bool disposing)
+    {
+        base.Dispose(disposing);
+        if (!disposing)
+            return;
+        _menu?.Dispose();
+    }
+}
diff --git a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
new file mode 100644 (file)
index 0000000..176acbf
--- /dev/null
@@ -0,0 +1,21 @@
+<BoxContainer
+    xmlns="https://spacestation14.io"
+    xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+    xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:style="clr-namespace:Content.Client.Stylesheets"
+    xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+    Orientation="Vertical"
+    HorizontalExpand="True"
+    Margin="0 0 0 5">
+    <PanelContainer>
+        <PanelContainer.PanelOverride>
+            <graphics:StyleBoxFlat BackgroundColor="{xNamespace:Static style:StyleNano.ButtonColorDisabled}" />
+        </PanelContainer.PanelOverride>
+        <Collapsible Orientation="Vertical" Name="Collapsible">
+            <CollapsibleHeading Name="Heading" MinHeight="35"/>
+            <CollapsibleBody Name="Body">
+                <BoxContainer Name="ItemsContainer" Orientation="Vertical" HorizontalExpand="True"/>
+            </CollapsibleBody>
+        </Collapsible>
+    </PanelContainer>
+</BoxContainer>
diff --git a/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs b/Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml.cs
new file mode 100644 (file)
index 0000000..09e418f
--- /dev/null
@@ -0,0 +1,46 @@
+using Content.Shared.Bed.Cryostorage;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Bed.Cryostorage;
+
+[GenerateTypedNameReferences]
+public sealed partial class CryostorageEntryControl : BoxContainer
+{
+    public event Action<string>? SlotRemoveButtonPressed;
+    public event Action<string>? HandRemoveButtonPressed;
+
+    public NetEntity Entity;
+    public bool LastOpenState;
+
+    public CryostorageEntryControl(CryostorageContainedPlayerData data)
+    {
+        RobustXamlLoader.Load(this);
+        Entity = data.PlayerEnt;
+        Update(data);
+    }
+
+    public void Update(CryostorageContainedPlayerData data)
+    {
+        LastOpenState = Collapsible.BodyVisible;
+        Heading.Title = data.PlayerName;
+        Body.Visible = data.ItemSlots.Count != 0 && data.HeldItems.Count != 0;
+
+        ItemsContainer.Children.Clear();
+        foreach (var (name, itemName) in data.ItemSlots)
+        {
+            var control = new CryostorageSlotControl(name, itemName);
+            control.Button.OnPressed += _ => SlotRemoveButtonPressed?.Invoke(name);
+            ItemsContainer.AddChild(control);
+        }
+
+        foreach (var (name, held) in data.HeldItems)
+        {
+            var control = new CryostorageSlotControl(Loc.GetString("cryostorage-ui-filler-hand"), held);
+            control.Button.OnPressed += _ => HandRemoveButtonPressed?.Invoke(name);
+            ItemsContainer.AddChild(control);
+        }
+        Collapsible.BodyVisible = LastOpenState;
+    }
+}
diff --git a/Content.Client/Bed/Cryostorage/CryostorageMenu.xaml b/Content.Client/Bed/Cryostorage/CryostorageMenu.xaml
new file mode 100644 (file)
index 0000000..5360cdb
--- /dev/null
@@ -0,0 +1,33 @@
+<controls:FancyWindow
+    xmlns="https://spacestation14.io"
+    xmlns:graphics="clr-namespace:Robust.Client.Graphics;assembly=Robust.Client"
+    xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
+    xmlns:xNamespace="http://schemas.microsoft.com/winfx/2006/xaml"
+    xmlns:style="clr-namespace:Content.Client.Stylesheets"
+    Title="{Loc 'cryostorage-ui-window-title'}"
+    MinSize="350 350"
+    SetSize="450 400">
+    <BoxContainer
+        Orientation="Vertical"
+        VerticalExpand="True"
+        HorizontalExpand="True">
+        <PanelContainer
+            VerticalExpand="True"
+            HorizontalExpand="True"
+            Margin="15">
+            <PanelContainer.PanelOverride>
+                <graphics:StyleBoxFlat BackgroundColor="{xNamespace:Static style:StyleNano.PanelDark}" />
+            </PanelContainer.PanelOverride>
+            <ScrollContainer VerticalExpand="True" HorizontalExpand="True">
+                <Control>
+                    <Label Text="{Loc 'cryostorage-ui-label-no-bodies'}" Name="EmptyLabel" HorizontalAlignment="Center" VerticalAlignment="Center"/>
+                    <BoxContainer Name="EntriesContainer"
+                                  Orientation="Vertical"
+                                  Margin="10"
+                                  VerticalExpand="True"
+                                  HorizontalExpand="True"/>
+                </Control>
+            </ScrollContainer>
+        </PanelContainer>
+    </BoxContainer>
+</controls:FancyWindow>
diff --git a/Content.Client/Bed/Cryostorage/CryostorageMenu.xaml.cs b/Content.Client/Bed/Cryostorage/CryostorageMenu.xaml.cs
new file mode 100644 (file)
index 0000000..51f1561
--- /dev/null
@@ -0,0 +1,54 @@
+using System.Linq;
+using Content.Client.UserInterface.Controls;
+using Content.Shared.Bed.Cryostorage;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Collections;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Bed.Cryostorage;
+
+[GenerateTypedNameReferences]
+public sealed partial class CryostorageMenu : FancyWindow
+{
+    public event Action<NetEntity, string>? SlotRemoveButtonPressed;
+    public event Action<NetEntity, string>? HandRemoveButtonPressed;
+
+    public CryostorageMenu()
+    {
+        RobustXamlLoader.Load(this);
+    }
+
+    public void UpdateState(CryostorageBuiState state)
+    {
+        var data = state.PlayerData;
+        var nonexistentEntries = new ValueList<CryostorageContainedPlayerData>(data);
+
+        var children = new ValueList<Control>(EntriesContainer.Children);
+        foreach (var control in children)
+        {
+            if (control is not CryostorageEntryControl entryControl)
+                continue;
+
+            if (data.Where(p => p.PlayerEnt == entryControl.Entity).FirstOrNull() is not { } datum)
+            {
+                EntriesContainer.Children.Remove(entryControl);
+                continue;
+            }
+
+            nonexistentEntries.Remove(datum);
+            entryControl.Update(datum);
+        }
+
+        foreach (var player in nonexistentEntries)
+        {
+            var control = new CryostorageEntryControl(player);
+            control.SlotRemoveButtonPressed += a => SlotRemoveButtonPressed?.Invoke(player.PlayerEnt, a);
+            control.HandRemoveButtonPressed += a => HandRemoveButtonPressed?.Invoke(player.PlayerEnt, a);
+            EntriesContainer.Children.Add(control);
+        }
+
+        EmptyLabel.Visible = data.Count == 0;
+    }
+}
diff --git a/Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml b/Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml
new file mode 100644 (file)
index 0000000..b45e77c
--- /dev/null
@@ -0,0 +1,13 @@
+<BoxContainer
+    xmlns="https://spacestation14.io"
+    Orientation="Horizontal"
+    HorizontalExpand="True"
+    Margin="5">
+    <RichTextLabel Name="SlotLabel" HorizontalAlignment="Left"/>
+    <Control HorizontalExpand="True"/>
+    <BoxContainer Orientation="Horizontal"
+                  HorizontalAlignment="Right">
+        <Label Name="ItemLabel" Margin="0 0 5 0"/>
+        <Button Name="Button" Access="Public" Text="{Loc 'cryostorage-ui-button-remove'}"></Button>
+    </BoxContainer>
+</BoxContainer>
diff --git a/Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml.cs b/Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml.cs
new file mode 100644 (file)
index 0000000..629b958
--- /dev/null
@@ -0,0 +1,18 @@
+using Content.Client.Message;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Bed.Cryostorage;
+
+[GenerateTypedNameReferences]
+public sealed partial class CryostorageSlotControl : BoxContainer
+{
+    public CryostorageSlotControl(string name, string itemName)
+    {
+        RobustXamlLoader.Load(this);
+
+        SlotLabel.SetMarkup(Loc.GetString("cryostorage-ui-label-slot-name", ("slot", name)));
+        ItemLabel.Text = itemName;
+    }
+}
diff --git a/Content.Client/Bed/Cryostorage/CryostorageSystem.cs b/Content.Client/Bed/Cryostorage/CryostorageSystem.cs
new file mode 100644 (file)
index 0000000..882f433
--- /dev/null
@@ -0,0 +1,9 @@
+using Content.Shared.Bed.Cryostorage;
+
+namespace Content.Client.Bed.Cryostorage;
+
+/// <inheritdoc/>
+public sealed class CryostorageSystem : SharedCryostorageSystem
+{
+
+}
diff --git a/Content.Server/Bed/Cryostorage/CryostorageSystem.cs b/Content.Server/Bed/Cryostorage/CryostorageSystem.cs
new file mode 100644 (file)
index 0000000..799bb82
--- /dev/null
@@ -0,0 +1,309 @@
+using System.Linq;
+using Content.Server.Chat.Managers;
+using Content.Server.GameTicking;
+using Content.Server.Hands.Systems;
+using Content.Server.Inventory;
+using Content.Server.Popups;
+using Content.Server.Station.Components;
+using Content.Server.Station.Systems;
+using Content.Server.UserInterface;
+using Content.Shared.Access.Systems;
+using Content.Shared.Bed.Cryostorage;
+using Content.Shared.Chat;
+using Content.Shared.Climbing.Systems;
+using Content.Shared.Database;
+using Content.Shared.Hands.Components;
+using Content.Shared.Mind.Components;
+using Robust.Server.Audio;
+using Robust.Server.Containers;
+using Robust.Server.GameObjects;
+using Robust.Server.Player;
+using Robust.Shared.Containers;
+using Robust.Shared.Enums;
+using Robust.Shared.Network;
+using Robust.Shared.Player;
+
+namespace Content.Server.Bed.Cryostorage;
+
+/// <inheritdoc/>
+public sealed class CryostorageSystem : SharedCryostorageSystem
+{
+    [Dependency] private readonly IChatManager _chatManager = default!;
+    [Dependency] private readonly IPlayerManager _playerManager = default!;
+    [Dependency] private readonly AudioSystem _audio = default!;
+    [Dependency] private readonly AccessReaderSystem _accessReader = default!;
+    [Dependency] private readonly ClimbSystem _climb = default!;
+    [Dependency] private readonly ContainerSystem _container = default!;
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    [Dependency] private readonly HandsSystem _hands = default!;
+    [Dependency] private readonly ServerInventorySystem _inventory = default!;
+    [Dependency] private readonly PopupSystem _popup = default!;
+    [Dependency] private readonly StationSystem _station = default!;
+    [Dependency] private readonly StationJobsSystem _stationJobs = default!;
+    [Dependency] private readonly TransformSystem _transform = default!;
+    [Dependency] private readonly UserInterfaceSystem _ui = default!;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<CryostorageComponent, BeforeActivatableUIOpenEvent>(OnBeforeUIOpened);
+        SubscribeLocalEvent<CryostorageComponent, CryostorageRemoveItemBuiMessage>(OnRemoveItemBuiMessage);
+
+        SubscribeLocalEvent<CryostorageContainedComponent, PlayerSpawnCompleteEvent>(OnPlayerSpawned);
+        SubscribeLocalEvent<CryostorageContainedComponent, MindRemovedMessage>(OnMindRemoved);
+
+        _playerManager.PlayerStatusChanged += PlayerStatusChanged;
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+
+        _playerManager.PlayerStatusChanged -= PlayerStatusChanged;
+    }
+
+    private void OnBeforeUIOpened(Entity<CryostorageComponent> ent, ref BeforeActivatableUIOpenEvent args)
+    {
+        UpdateCryostorageUIState(ent);
+    }
+
+    private void OnRemoveItemBuiMessage(Entity<CryostorageComponent> ent, ref CryostorageRemoveItemBuiMessage args)
+    {
+        var comp = ent.Comp;
+        if (args.Session.AttachedEntity is not { } attachedEntity)
+            return;
+
+        var cryoContained = GetEntity(args.Entity);
+
+        if (!comp.StoredPlayers.Contains(cryoContained))
+            return;
+
+        if (!HasComp<HandsComponent>(attachedEntity))
+            return;
+
+        if (!_accessReader.IsAllowed(attachedEntity, ent))
+        {
+            _popup.PopupEntity(Loc.GetString("cryostorage-popup-access-denied"), attachedEntity, attachedEntity);
+            return;
+        }
+
+        EntityUid? entity = null;
+        if (args.Type == CryostorageRemoveItemBuiMessage.RemovalType.Hand)
+        {
+            if (_hands.TryGetHand(cryoContained, args.Key, out var hand))
+                entity = hand.HeldEntity;
+        }
+        else
+        {
+            if (_inventory.TryGetSlotContainer(cryoContained, args.Key, out var slot, out _))
+                entity = slot.ContainedEntity;
+        }
+
+        if (entity == null)
+            return;
+
+        AdminLog.Add(LogType.Action, LogImpact.High,
+            $"{ToPrettyString(attachedEntity):player} removed item {ToPrettyString(entity)} from cryostorage-contained player " +
+            $"{ToPrettyString(cryoContained):player}, stored in cryostorage {ToPrettyString(ent)}");
+        _container.TryRemoveFromContainer(entity.Value);
+        _transform.SetCoordinates(entity.Value, Transform(attachedEntity).Coordinates);
+        _hands.PickupOrDrop(attachedEntity, entity.Value);
+        UpdateCryostorageUIState(ent);
+    }
+
+    private void UpdateCryostorageUIState(Entity<CryostorageComponent> ent)
+    {
+        var state = new CryostorageBuiState(GetAllContainedData(ent).ToList());
+        _ui.TrySetUiState(ent, CryostorageUIKey.Key, state);
+    }
+
+    private void OnPlayerSpawned(Entity<CryostorageContainedComponent> ent, ref PlayerSpawnCompleteEvent args)
+    {
+        // if you spawned into cryostorage, we're not gonna round-remove you.
+        ent.Comp.GracePeriodEndTime = null;
+    }
+
+    private void OnMindRemoved(Entity<CryostorageContainedComponent> ent, ref MindRemovedMessage args)
+    {
+        var comp = ent.Comp;
+
+        if (!TryComp<CryostorageComponent>(comp.Cryostorage, out var cryostorageComponent))
+            return;
+
+        if (comp.GracePeriodEndTime != null)
+            comp.GracePeriodEndTime = Timing.CurTime + cryostorageComponent.NoMindGracePeriod;
+        comp.UserId = args.Mind.Comp.UserId;
+    }
+
+    private void PlayerStatusChanged(object? sender, SessionStatusEventArgs args)
+    {
+        if (args.Session.AttachedEntity is not { } entity)
+            return;
+
+        if (!TryComp<CryostorageContainedComponent>(entity, out var containedComponent))
+            return;
+
+        if (args.NewStatus is SessionStatus.Disconnected or SessionStatus.Zombie)
+        {
+            if (CryoSleepRejoiningEnabled)
+                containedComponent.StoredWhileDisconnected = true;
+
+            var delay = CompOrNull<CryostorageComponent>(containedComponent.Cryostorage)?.NoMindGracePeriod ?? TimeSpan.Zero;
+            containedComponent.GracePeriodEndTime = Timing.CurTime + delay;
+            containedComponent.UserId = args.Session.UserId;
+        }
+        else if (args.NewStatus == SessionStatus.InGame)
+        {
+            HandleCryostorageReconnection((entity, containedComponent));
+        }
+    }
+
+    public void HandleEnterCryostorage(Entity<CryostorageContainedComponent> ent, NetUserId? userId)
+    {
+        var comp = ent.Comp;
+        var cryostorageEnt = ent.Comp.Cryostorage;
+        if (!TryComp<CryostorageComponent>(cryostorageEnt, out var cryostorageComponent))
+            return;
+
+        // if we have a session, we use that to add back in all the job slots the player had.
+        if (userId != null)
+        {
+            foreach (var station in _station.GetStationsSet())
+            {
+                if (!TryComp<StationJobsComponent>(station, out var stationJobs))
+                    continue;
+
+                if (!_stationJobs.TryGetPlayerJobs(station, userId.Value, out var jobs, stationJobs))
+                    continue;
+
+                foreach (var job in jobs)
+                {
+                    _stationJobs.TryAdjustJobSlot(station, job, 1, clamp: true);
+                }
+
+                _stationJobs.TryRemovePlayerJobs(station, userId.Value, stationJobs);
+            }
+        }
+
+        _audio.PlayPvs(cryostorageComponent.RemoveSound, ent);
+
+        EnsurePausedMap();
+        if (PausedMap == null)
+        {
+            Log.Error("CryoSleep map was unexpectedly null");
+            return;
+        }
+
+        if (!comp.StoredWhileDisconnected &&
+            userId != null &&
+            Mind.TryGetMind(userId.Value, out var mind) &&
+            mind.Value.Comp.Session?.AttachedEntity == ent)
+        {
+            _gameTicker.OnGhostAttempt(mind.Value, false);
+        }
+        _transform.SetParent(ent, PausedMap.Value);
+        cryostorageComponent.StoredPlayers.Add(ent);
+        UpdateCryostorageUIState((cryostorageEnt.Value, cryostorageComponent));
+        AdminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(ent):player} was entered into cryostorage inside of {ToPrettyString(cryostorageEnt.Value)}");
+    }
+
+    private void HandleCryostorageReconnection(Entity<CryostorageContainedComponent> entity)
+    {
+        var (uid, comp) = entity;
+        if (!CryoSleepRejoiningEnabled || !comp.StoredWhileDisconnected)
+            return;
+
+        // how did you destroy these? they're indestructible.
+        if (comp.Cryostorage is not { } cryostorage ||
+            TerminatingOrDeleted(cryostorage) ||
+            !TryComp<CryostorageComponent>(comp.Cryostorage, out var cryostorageComponent))
+        {
+            QueueDel(entity);
+            return;
+        }
+
+        var cryoXform = Transform(cryostorage);
+        _transform.SetParent(uid, cryoXform.ParentUid);
+        _transform.SetCoordinates(uid, cryoXform.Coordinates);
+        if (!_container.TryGetContainer(cryostorage, cryostorageComponent.ContainerId, out var container) ||
+            !_container.Insert(uid, container, cryoXform))
+        {
+            _climb.ForciblySetClimbing(uid, cryostorage);
+        }
+
+        comp.GracePeriodEndTime = null;
+        comp.StoredWhileDisconnected = false;
+        cryostorageComponent.StoredPlayers.Remove(entity);
+        AdminLog.Add(LogType.Action, LogImpact.High, $"{ToPrettyString(entity):player} re-entered the game from cryostorage {ToPrettyString(cryostorage)}");
+        UpdateCryostorageUIState((cryostorage, cryostorageComponent));
+    }
+
+    protected override void OnInsertedContainer(Entity<CryostorageComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        var (uid, comp) = ent;
+        if (args.Container.ID != comp.ContainerId)
+            return;
+
+        base.OnInsertedContainer(ent, ref args);
+
+        var locKey = CryoSleepRejoiningEnabled
+            ? "cryostorage-insert-message-temp"
+            : "cryostorage-insert-message-permanent";
+
+        var msg = Loc.GetString(locKey, ("time", comp.GracePeriod.TotalMinutes));
+        if (TryComp<ActorComponent>(args.Entity, out var actor))
+            _chatManager.ChatMessageToOne(ChatChannel.Server, msg, msg, uid, false, actor.PlayerSession.Channel);
+    }
+
+    private IEnumerable<CryostorageContainedPlayerData> GetAllContainedData(Entity<CryostorageComponent> ent)
+    {
+        foreach (var contained in ent.Comp.StoredPlayers)
+        {
+            yield return GetContainedData(contained);
+        }
+    }
+
+    private CryostorageContainedPlayerData GetContainedData(EntityUid uid)
+    {
+        var data = new CryostorageContainedPlayerData();
+        data.PlayerName = Name(uid);
+        data.PlayerEnt = GetNetEntity(uid);
+
+        var enumerator = _inventory.GetSlotEnumerator(uid);
+        while (enumerator.NextItem(out var item, out var slotDef))
+        {
+            data.ItemSlots.Add(slotDef.Name, Name(item));
+        }
+
+        foreach (var hand in _hands.EnumerateHands(uid))
+        {
+            if (hand.HeldEntity == null)
+                continue;
+
+            data.HeldItems.Add(hand.Name, Name(hand.HeldEntity.Value));
+        }
+
+        return data;
+    }
+
+    public override void Update(float frameTime)
+    {
+        base.Update(frameTime);
+
+        var query = EntityQueryEnumerator<CryostorageContainedComponent>();
+        while (query.MoveNext(out var uid, out var containedComp))
+        {
+            if (containedComp.GracePeriodEndTime == null || containedComp.StoredWhileDisconnected)
+                continue;
+
+            if (Timing.CurTime < containedComp.GracePeriodEndTime)
+                continue;
+
+            Mind.TryGetMind(uid, out _, out var mindComp);
+            var id = mindComp?.UserId ?? containedComp.UserId;
+            HandleEnterCryostorage((uid, containedComp), id);
+        }
+    }
+}
index 1a86d9fef46122120a17859fee8e548b18e56c13..07c79747c0fc368bd02ef4e65d8017d7d0c610d6 100644 (file)
@@ -231,7 +231,7 @@ namespace Content.Server.GameTicking
                 EntityManager.AddComponent<OwOAccentComponent>(mob);
             }
 
-            _stationJobs.TryAssignJob(station, jobPrototype);
+            _stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
 
             if (lateJoin)
                 _adminLogger.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
diff --git a/Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs b/Content.Server/Spawners/Components/ContainerSpawnPointComponent.cs
new file mode 100644 (file)
index 0000000..5cd2ac3
--- /dev/null
@@ -0,0 +1,30 @@
+using Content.Server.Spawners.EntitySystems;
+
+namespace Content.Server.Spawners.Components;
+
+/// <summary>
+/// A spawn point that spawns a player into a target container rather than simply spawning them at a position.
+/// Occurs before regular spawn points but after arrivals.
+/// </summary>
+[RegisterComponent]
+[Access(typeof(ContainerSpawnPointSystem))]
+public sealed partial class ContainerSpawnPointComponent : Component
+{
+    /// <summary>
+    /// The ID of the container that this entity will spawn players into
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string ContainerId;
+
+    /// <summary>
+    /// An optional job specifier
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string? Job;
+
+    /// <summary>
+    /// The type of spawn point
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public SpawnPointType SpawnType = SpawnPointType.Unset;
+}
diff --git a/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs b/Content.Server/Spawners/EntitySystems/ContainerSpawnPointSystem.cs
new file mode 100644 (file)
index 0000000..65f1076
--- /dev/null
@@ -0,0 +1,85 @@
+using Content.Server.GameTicking;
+using Content.Server.Shuttles.Systems;
+using Content.Server.Spawners.Components;
+using Content.Server.Station.Systems;
+using Robust.Server.Containers;
+using Robust.Shared.Containers;
+using Robust.Shared.Random;
+
+namespace Content.Server.Spawners.EntitySystems;
+
+public sealed class ContainerSpawnPointSystem : EntitySystem
+{
+    [Dependency] private readonly GameTicker _gameTicker = default!;
+    [Dependency] private readonly IRobustRandom _random = default!;
+    [Dependency] private readonly ContainerSystem _container = default!;
+    [Dependency] private readonly StationSystem _station = default!;
+    [Dependency] private readonly StationSpawningSystem _stationSpawning = default!;
+
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<PlayerSpawningEvent>(OnSpawnPlayer, before: new[] { typeof(SpawnPointSystem), typeof(ArrivalsSystem) });
+    }
+
+    private void OnSpawnPlayer(PlayerSpawningEvent args)
+    {
+        if (args.SpawnResult != null)
+            return;
+
+        var query = EntityQueryEnumerator<ContainerSpawnPointComponent, ContainerManagerComponent, TransformComponent>();
+        var possibleContainers = new List<Entity<ContainerSpawnPointComponent, ContainerManagerComponent, TransformComponent>>();
+
+        while (query.MoveNext(out var uid, out var spawnPoint, out var container, out var xform))
+        {
+            if (args.Station != null && _station.GetOwningStation(uid, xform) != args.Station)
+                continue;
+
+            // If it's unset, then we allow it to be used for both roundstart and midround joins
+            if (spawnPoint.SpawnType == SpawnPointType.Unset)
+            {
+                // make sure we also check the job here for various reasons.
+                if (spawnPoint.Job == null || spawnPoint.Job == args.Job?.Prototype)
+                    possibleContainers.Add((uid, spawnPoint, container, xform));
+                continue;
+            }
+
+            if (_gameTicker.RunLevel == GameRunLevel.InRound && spawnPoint.SpawnType == SpawnPointType.LateJoin)
+            {
+                possibleContainers.Add((uid, spawnPoint, container, xform));
+            }
+
+            if (_gameTicker.RunLevel != GameRunLevel.InRound &&
+                spawnPoint.SpawnType == SpawnPointType.Job &&
+                (args.Job == null || spawnPoint.Job == args.Job.Prototype))
+            {
+                possibleContainers.Add((uid, spawnPoint, container, xform));
+            }
+        }
+
+        if (possibleContainers.Count == 0)
+            return;
+        // we just need some default coords so we can spawn the player entity.
+        var baseCoords = possibleContainers[0].Comp3.Coordinates;
+
+        args.SpawnResult = _stationSpawning.SpawnPlayerMob(
+            baseCoords,
+            args.Job,
+            args.HumanoidCharacterProfile,
+            args.Station);
+
+        _random.Shuffle(possibleContainers);
+        foreach (var (uid, spawnPoint, manager, xform) in possibleContainers)
+        {
+            if (!_container.TryGetContainer(uid, spawnPoint.ContainerId, out var container, manager))
+                continue;
+
+            if (!_container.Insert(args.SpawnResult.Value, container, containerXform: xform))
+                continue;
+
+            return;
+        }
+
+        Del(args.Station);
+        args.SpawnResult = null;
+    }
+}
index 677600df7e6887210ad8ab04fd2937d3995478c4..74399bf412db442a2e887b0f97b8a8349b3795b0 100644 (file)
@@ -1,6 +1,8 @@
 using Content.Server.Station.Systems;
 using Content.Shared.Roles;
 using JetBrains.Annotations;
+using Robust.Shared.Network;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
 using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
 
@@ -75,6 +77,13 @@ public sealed partial class StationJobsComponent : Component
     [DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
     public HashSet<string> OverflowJobs = new();
 
+    /// <summary>
+    /// A dictionary relating a NetUserId to the jobs they have on station.
+    /// An OOC way to track where job slots have gone.
+    /// </summary>
+    [DataField]
+    public Dictionary<NetUserId, List<ProtoId<JobPrototype>>> PlayerJobs = new();
+
     [DataField("availableJobs", required: true,
         customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
     public Dictionary<string, List<int?>> SetupAvailableJobs = default!;
index eeaace03b2cb5c53252a9bd3623179752c0de20b..c13df410a08e6daa6bef0be0e74a7846813c1fae 100644 (file)
@@ -9,7 +9,9 @@ using Content.Shared.Roles;
 using JetBrains.Annotations;
 using Robust.Server.Player;
 using Robust.Shared.Configuration;
+using Robust.Shared.Network;
 using Robust.Shared.Player;
+using Robust.Shared.Prototypes;
 using Robust.Shared.Random;
 
 namespace Content.Server.Station.Systems;
@@ -84,13 +86,14 @@ public sealed partial class StationJobsSystem : EntitySystem
 
     #region Public API
 
-    /// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,Content.Server.Station.Components.StationJobsComponent?)"/>
+    /// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,NetUserId,Content.Server.Station.Components.StationJobsComponent?)"/>
     /// <param name="station">Station to assign a job on.</param>
     /// <param name="job">Job to assign.</param>
+    /// <param name="netUserId">The net user ID of the player we're assigning this job to.</param>
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
-    public bool TryAssignJob(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null)
+    public bool TryAssignJob(EntityUid station, JobPrototype job, NetUserId netUserId, StationJobsComponent? stationJobs = null)
     {
-        return TryAssignJob(station, job.ID, stationJobs);
+        return TryAssignJob(station, job.ID, netUserId, stationJobs);
     }
 
     /// <summary>
@@ -98,12 +101,21 @@ public sealed partial class StationJobsSystem : EntitySystem
     /// </summary>
     /// <param name="station">Station to assign a job on.</param>
     /// <param name="jobPrototypeId">Job prototype ID to assign.</param>
+    /// <param name="netUserId">The net user ID of the player we're assigning this job to.</param>
     /// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
     /// <returns>Whether or not assignment was a success.</returns>
     /// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
-    public bool TryAssignJob(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null)
+    public bool TryAssignJob(EntityUid station, string jobPrototypeId, NetUserId netUserId, StationJobsComponent? stationJobs = null)
     {
-        return TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs);
+        if (!Resolve(station, ref stationJobs, false))
+            return false;
+
+        if (!TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs))
+            return false;
+
+        stationJobs.PlayerJobs.TryAdd(netUserId, new());
+        stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId);
+        return true;
     }
 
     /// <inheritdoc cref="TryAdjustJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
@@ -183,6 +195,28 @@ public sealed partial class StationJobsSystem : EntitySystem
         }
     }
 
+    public bool TryGetPlayerJobs(EntityUid station,
+        NetUserId userId,
+        [NotNullWhen(true)] out List<ProtoId<JobPrototype>>? jobs,
+        StationJobsComponent? jobsComponent = null)
+    {
+        jobs = null;
+        if (!Resolve(station, ref jobsComponent, false))
+            return false;
+
+        return jobsComponent.PlayerJobs.TryGetValue(userId, out jobs);
+    }
+
+    public bool TryRemovePlayerJobs(EntityUid station,
+        NetUserId userId,
+        StationJobsComponent? jobsComponent = null)
+    {
+        if (!Resolve(station, ref jobsComponent, false))
+            return false;
+
+        return jobsComponent.PlayerJobs.Remove(userId);
+    }
+
     /// <inheritdoc cref="TrySetJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
     /// <param name="station">Station to adjust the job slot on.</param>
     /// <param name="jobPrototype">Job prototype to adjust.</param>
index 5dd45b21c313590569ba5c2b01e1ad8812d29a35..3f6c9e1c052f1f0437eb5db80c0ec31c45457462 100644 (file)
@@ -63,6 +63,12 @@ public sealed partial class AccessReaderComponent : Component
     /// </summary>
     [DataField, ViewVariables(VVAccess.ReadWrite)]
     public int AccessLogLimit = 20;
+
+    /// <summary>
+    /// Whether or not emag interactions have an effect on this.
+    /// </summary>
+    [DataField]
+    public bool BreakOnEmag = true;
 }
 
 [DataDefinition, Serializable, NetSerializable]
index f630803446a24aacab2075fcf7d92d191ea2c86f..387ca8a01386b7b1c202adcca3cd41c0ee722063 100644 (file)
@@ -56,6 +56,7 @@ public sealed partial class IdCardConsoleComponent : Component
         "ChiefEngineer",
         "ChiefMedicalOfficer",
         "Command",
+        "Cryogenics",
         "Engineering",
         "External",
         "HeadOfPersonnel",
index c5bceb4899f552f2fdbe70aed35ee52e3af5efc0..812a8e048706f9369fd2dcdaa80647965b73275c 100644 (file)
@@ -76,6 +76,8 @@ public sealed class AccessReaderSystem : EntitySystem
 
     private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
     {
+        if (!reader.BreakOnEmag)
+            return;
         args.Handled = true;
         reader.Enabled = false;
         reader.AccessLog.Clear();
diff --git a/Content.Shared/Bed/Cryostorage/CryostorageComponent.cs b/Content.Shared/Bed/Cryostorage/CryostorageComponent.cs
new file mode 100644 (file)
index 0000000..c7aa00c
--- /dev/null
@@ -0,0 +1,110 @@
+using Robust.Shared.Audio;
+using Robust.Shared.GameStates;
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Bed.Cryostorage;
+
+/// <summary>
+/// This is used for a container which, when a player logs out while inside of,
+/// will delete their body and redistribute their items.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+public sealed partial class CryostorageComponent : Component
+{
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string ContainerId = "storage";
+
+    /// <summary>
+    /// How long a player can remain inside Cryostorage before automatically being taken care of, given that they have no mind.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan NoMindGracePeriod = TimeSpan.FromSeconds(30f);
+
+    /// <summary>
+    /// How long a player can remain inside Cryostorage before automatically being taken care of.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public TimeSpan GracePeriod = TimeSpan.FromMinutes(5f);
+
+    /// <summary>
+    /// A list of players who have actively entered cryostorage.
+    /// </summary>
+    [DataField]
+    public List<EntityUid> StoredPlayers = new();
+
+    /// <summary>
+    /// Sound that is played when a player is removed by a cryostorage.
+    /// </summary>
+    [DataField]
+    public SoundSpecifier? RemoveSound = new SoundPathSpecifier("/Audio/Effects/teleport_departure.ogg");
+}
+
+[Serializable, NetSerializable]
+public enum CryostorageVisuals : byte
+{
+    Full
+}
+
+[Serializable, NetSerializable]
+public record struct CryostorageContainedPlayerData()
+{
+    /// <summary>
+    /// The player's IC name
+    /// </summary>
+    public string PlayerName = string.Empty;
+
+    /// <summary>
+    /// The player's entity
+    /// </summary>
+    public NetEntity PlayerEnt = NetEntity.Invalid;
+
+    /// <summary>
+    /// A dictionary relating a slot definition name to the name of the item inside of it.
+    /// </summary>
+    public Dictionary<string, string> ItemSlots = new();
+
+    /// <summary>
+    /// A dictionary relating a hand ID to the hand name and the name of the item being held.
+    /// </summary>
+    public Dictionary<string, string> HeldItems = new();
+}
+
+[Serializable, NetSerializable]
+public sealed class CryostorageBuiState : BoundUserInterfaceState
+{
+    public List<CryostorageContainedPlayerData> PlayerData;
+
+    public CryostorageBuiState(List<CryostorageContainedPlayerData> playerData)
+    {
+        PlayerData = playerData;
+    }
+}
+
+[Serializable, NetSerializable]
+public sealed class CryostorageRemoveItemBuiMessage : BoundUserInterfaceMessage
+{
+    public NetEntity Entity;
+
+    public string Key;
+
+    public RemovalType Type;
+
+    public enum RemovalType : byte
+    {
+        Hand,
+        Inventory
+    }
+
+    public CryostorageRemoveItemBuiMessage(NetEntity entity, string key, RemovalType type)
+    {
+        Entity = entity;
+        Key = key;
+        Type = type;
+    }
+}
+
+[Serializable, NetSerializable]
+public enum CryostorageUIKey : byte
+{
+    Key
+}
diff --git a/Content.Shared/Bed/Cryostorage/CryostorageContainedComponent.cs b/Content.Shared/Bed/Cryostorage/CryostorageContainedComponent.cs
new file mode 100644 (file)
index 0000000..42a11aa
--- /dev/null
@@ -0,0 +1,34 @@
+using Robust.Shared.GameStates;
+using Robust.Shared.Network;
+using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
+
+namespace Content.Shared.Bed.Cryostorage;
+
+/// <summary>
+/// This is used to track an entity that is currently being held in Cryostorage.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[AutoGenerateComponentState]
+public sealed partial class CryostorageContainedComponent : Component
+{
+    /// <summary>
+    /// Whether or not this entity is being stored on another map or is just chilling in a container
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool StoredWhileDisconnected;
+
+    /// <summary>
+    /// The time at which the cryostorage grace period ends.
+    /// </summary>
+    [DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), ViewVariables(VVAccess.ReadWrite), AutoNetworkedField]
+    public TimeSpan? GracePeriodEndTime;
+
+    /// <summary>
+    /// The cryostorage this entity is 'stored' in.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public EntityUid? Cryostorage;
+
+    [DataField]
+    public NetUserId? UserId;
+}
diff --git a/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs b/Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs
new file mode 100644 (file)
index 0000000..e781433
--- /dev/null
@@ -0,0 +1,179 @@
+using Content.Shared.Administration.Logs;
+using Content.Shared.CCVar;
+using Content.Shared.DragDrop;
+using Content.Shared.GameTicking;
+using Content.Shared.Mind;
+using Content.Shared.Mind.Components;
+using Robust.Shared.Configuration;
+using Robust.Shared.Containers;
+using Robust.Shared.Map;
+using Robust.Shared.Timing;
+
+namespace Content.Shared.Bed.Cryostorage;
+
+/// <summary>
+/// This handles <see cref="CryostorageComponent"/>
+/// </summary>
+public abstract class SharedCryostorageSystem : EntitySystem
+{
+    [Dependency] protected readonly ISharedAdminLogManager AdminLog = default!;
+    [Dependency] private readonly IConfigurationManager _configuration = default!;
+    [Dependency] protected readonly IGameTiming Timing = default!;
+    [Dependency] private readonly IMapManager _mapManager = default!;
+    [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
+    [Dependency] protected readonly SharedMindSystem Mind = default!;
+
+    protected EntityUid? PausedMap { get; private set; }
+
+    protected bool CryoSleepRejoiningEnabled;
+
+    /// <inheritdoc/>
+    public override void Initialize()
+    {
+        SubscribeLocalEvent<CryostorageComponent, EntInsertedIntoContainerMessage>(OnInsertedContainer);
+        SubscribeLocalEvent<CryostorageComponent, EntRemovedFromContainerMessage>(OnRemovedContainer);
+        SubscribeLocalEvent<CryostorageComponent, ContainerIsInsertingAttemptEvent>(OnInsertAttempt);
+        SubscribeLocalEvent<CryostorageComponent, ComponentShutdown>(OnShutdownContainer);
+        SubscribeLocalEvent<CryostorageComponent, CanDropTargetEvent>(OnCanDropTarget);
+
+        SubscribeLocalEvent<CryostorageContainedComponent, EntGotRemovedFromContainerMessage>(OnRemovedContained);
+        SubscribeLocalEvent<CryostorageContainedComponent, EntityUnpausedEvent>(OnUnpaused);
+        SubscribeLocalEvent<CryostorageContainedComponent, ComponentShutdown>(OnShutdownContained);
+
+        SubscribeLocalEvent<RoundRestartCleanupEvent>(OnRoundRestart);
+
+        _configuration.OnValueChanged(CCVars.GameCryoSleepRejoining, OnCvarChanged);
+    }
+
+    public override void Shutdown()
+    {
+        base.Shutdown();
+
+        _configuration.UnsubValueChanged(CCVars.GameCryoSleepRejoining, OnCvarChanged);
+    }
+
+    private void OnCvarChanged(bool value)
+    {
+        CryoSleepRejoiningEnabled = value;
+    }
+
+    protected virtual void OnInsertedContainer(Entity<CryostorageComponent> ent, ref EntInsertedIntoContainerMessage args)
+    {
+        var (_, comp) = ent;
+        if (args.Container.ID != comp.ContainerId)
+            return;
+
+        _appearance.SetData(ent, CryostorageVisuals.Full, true);
+        if (!Timing.IsFirstTimePredicted)
+            return;
+
+        var containedComp = EnsureComp<CryostorageContainedComponent>(args.Entity);
+        var delay = Mind.TryGetMind(args.Entity, out _, out _) ? comp.GracePeriod : comp.NoMindGracePeriod;
+        containedComp.GracePeriodEndTime = Timing.CurTime + delay;
+        containedComp.Cryostorage = ent;
+        Dirty(args.Entity, containedComp);
+    }
+
+    private void OnRemovedContainer(Entity<CryostorageComponent> ent, ref EntRemovedFromContainerMessage args)
+    {
+        var (_, comp) = ent;
+        if (args.Container.ID != comp.ContainerId)
+            return;
+
+        _appearance.SetData(ent, CryostorageVisuals.Full, args.Container.ContainedEntities.Count > 0);
+    }
+
+    private void OnInsertAttempt(Entity<CryostorageComponent> ent, ref ContainerIsInsertingAttemptEvent args)
+    {
+        var (_, comp) = ent;
+        if (args.Container.ID != comp.ContainerId)
+            return;
+
+        if (!TryComp<MindContainerComponent>(args.EntityUid, out var mindContainer))
+        {
+            args.Cancel();
+            return;
+        }
+
+        if (Mind.TryGetMind(args.EntityUid, out _, out var mindComp, mindContainer) &&
+            (mindComp.PreventSuicide || mindComp.PreventGhosting))
+        {
+            args.Cancel();
+        }
+    }
+
+    private void OnShutdownContainer(Entity<CryostorageComponent> ent, ref ComponentShutdown args)
+    {
+        var comp = ent.Comp;
+        foreach (var stored in comp.StoredPlayers)
+        {
+            if (TryComp<CryostorageContainedComponent>(stored, out var containedComponent))
+            {
+                containedComponent.Cryostorage = null;
+                Dirty(stored, containedComponent);
+            }
+        }
+
+        comp.StoredPlayers.Clear();
+        Dirty(ent, comp);
+    }
+
+    private void OnCanDropTarget(Entity<CryostorageComponent> ent, ref CanDropTargetEvent args)
+    {
+        if (args.Dragged == args.User)
+            return;
+
+        if (!Mind.TryGetMind(args.Dragged, out _, out var mindComp) || mindComp.Session?.AttachedEntity != args.Dragged)
+            return;
+
+        args.CanDrop = false;
+        args.Handled = true;
+    }
+
+    private void OnRemovedContained(Entity<CryostorageContainedComponent> ent, ref EntGotRemovedFromContainerMessage args)
+    {
+        var (_, comp) = ent;
+        if (!comp.StoredWhileDisconnected)
+            RemCompDeferred(ent, comp);
+    }
+
+    private void OnUnpaused(Entity<CryostorageContainedComponent> ent, ref EntityUnpausedEvent args)
+    {
+        var comp = ent.Comp;
+        if (comp.GracePeriodEndTime != null)
+            comp.GracePeriodEndTime = comp.GracePeriodEndTime.Value + args.PausedTime;
+    }
+
+    private void OnShutdownContained(Entity<CryostorageContainedComponent> ent, ref ComponentShutdown args)
+    {
+        var comp = ent.Comp;
+
+        CompOrNull<CryostorageComponent>(comp.Cryostorage)?.StoredPlayers.Remove(ent);
+        ent.Comp.Cryostorage = null;
+        Dirty(ent, comp);
+    }
+
+    private void OnRoundRestart(RoundRestartCleanupEvent _)
+    {
+        DeletePausedMap();
+    }
+
+    private void DeletePausedMap()
+    {
+        if (PausedMap == null || !Exists(PausedMap))
+            return;
+
+        EntityManager.DeleteEntity(PausedMap.Value);
+        PausedMap = null;
+    }
+
+    protected void EnsurePausedMap()
+    {
+        if (PausedMap != null && Exists(PausedMap))
+            return;
+
+        var map = _mapManager.CreateMap();
+        _mapManager.SetMapPaused(map, true);
+        PausedMap = _mapManager.GetMapEntityId(map);
+    }
+}
index 5f7fa41ecc623c9e133f8cf29a571ac0295c9dbe..1bfca81e2a25a8f1b45ac40f6acb7b22519f1a06 100644 (file)
@@ -223,6 +223,12 @@ namespace Content.Shared.CCVar
         public static readonly CVarDef<bool>
             GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
 
+        /// <summary>
+        /// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
+        /// </summary>
+        public static readonly CVarDef<bool>
+            GameCryoSleepRejoining = CVarDef.Create("game.cryo_sleep_rejoining", false, CVar.SERVER | CVar.REPLICATED);
+
         /// <summary>
         ///     Whether a random position offset will be applied to the station on roundstart.
         /// </summary>
index 21809f475637f98ac0164ecb690a6ed6c48558b1..c54149243a48d00b2f68097c5f31d0fdbb58f215 100644 (file)
@@ -247,7 +247,7 @@ public sealed partial class ClimbSystem : VirtualController
         if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
             return;
 
-        if (!Resolve(climbable, ref comp))
+        if (!Resolve(climbable, ref comp, false))
             return;
 
         if (!ReplaceFixtures(uid, climbing, fixtures))
diff --git a/Content.Shared/Containers/DragInsertContainerComponent.cs b/Content.Shared/Containers/DragInsertContainerComponent.cs
new file mode 100644 (file)
index 0000000..e4cae26
--- /dev/null
@@ -0,0 +1,20 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Containers;
+
+/// <summary>
+/// This is used for a container that can have entities inserted into it via a
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(DragInsertContainerSystem))]
+public sealed partial class DragInsertContainerComponent : Component
+{
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string ContainerId;
+
+    /// <summary>
+    /// If true, there will also be verbs for inserting / removing objects from this container.
+    /// </summary>
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public bool UseVerbs = true;
+}
diff --git a/Content.Shared/Containers/DragInsertContainerSystem.cs b/Content.Shared/Containers/DragInsertContainerSystem.cs
new file mode 100644 (file)
index 0000000..6bba265
--- /dev/null
@@ -0,0 +1,120 @@
+using Content.Shared.ActionBlocker;
+using Content.Shared.Administration.Logs;
+using Content.Shared.Climbing.Systems;
+using Content.Shared.Database;
+using Content.Shared.DragDrop;
+using Content.Shared.Verbs;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Containers;
+
+public sealed class DragInsertContainerSystem : EntitySystem
+{
+    [Dependency] private readonly ISharedAdminLogManager _adminLog = default!;
+    [Dependency] private readonly ActionBlockerSystem _actionBlocker = default!;
+    [Dependency] private readonly ClimbSystem _climb = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<DragInsertContainerComponent, DragDropTargetEvent>(OnDragDropOn, before: new []{ typeof(ClimbSystem)});
+        SubscribeLocalEvent<DragInsertContainerComponent, CanDropTargetEvent>(OnCanDragDropOn);
+        SubscribeLocalEvent<DragInsertContainerComponent, GetVerbsEvent<AlternativeVerb>>(OnGetAlternativeVerb);
+    }
+
+    private void OnDragDropOn(Entity<DragInsertContainerComponent> ent, ref DragDropTargetEvent args)
+    {
+        if (args.Handled)
+            return;
+
+        var (_, comp) = ent;
+        if (!_container.TryGetContainer(ent, comp.ContainerId, out var container))
+            return;
+
+        args.Handled = Insert(args.Dragged, args.User, ent, container);
+    }
+
+    private void OnCanDragDropOn(Entity<DragInsertContainerComponent> ent, ref CanDropTargetEvent args)
+    {
+        var (_, comp) = ent;
+        if (!_container.TryGetContainer(ent, comp.ContainerId, out var container))
+            return;
+
+        args.Handled = true;
+        args.CanDrop |= _container.CanInsert(args.Dragged, container);
+    }
+
+    private void OnGetAlternativeVerb(Entity<DragInsertContainerComponent> ent, ref GetVerbsEvent<AlternativeVerb> args)
+    {
+        var (uid, comp) = ent;
+        if (!comp.UseVerbs)
+            return;
+
+        if (!args.CanInteract || !args.CanAccess || args.Hands == null)
+            return;
+
+        if (!_container.TryGetContainer(uid, comp.ContainerId, out var container))
+            return;
+
+        var user = args.User;
+        if (!_actionBlocker.CanInteract(user, ent))
+            return;
+
+        // Eject verb
+        if (container.ContainedEntities.Count > 0)
+        {
+            // make sure that we can actually take stuff out of the container
+            var emptyableCount = 0;
+            foreach (var contained in container.ContainedEntities)
+            {
+                if (!_container.CanRemove(contained, container))
+                    continue;
+                emptyableCount++;
+            }
+
+            if (emptyableCount > 0)
+            {
+                AlternativeVerb verb = new()
+                {
+                    Act = () =>
+                    {
+                        _adminLog.Add(LogType.Action, LogImpact.Low, $"{ToPrettyString(user):player} emptied container {ToPrettyString(ent)}");
+                        var ents = _container.EmptyContainer(container);
+                        foreach (var contained in ents)
+                        {
+                            _climb.ForciblySetClimbing(contained, ent);
+                        }
+                    },
+                    Category = VerbCategory.Eject,
+                    Text = Loc.GetString("container-verb-text-empty"),
+                    Priority = 1 // Promote to top to make ejecting the ALT-click action
+                };
+                args.Verbs.Add(verb);
+            }
+        }
+
+        // Self-insert verb
+        if (_container.CanInsert(user, container) &&
+            _actionBlocker.CanMove(user))
+        {
+            AlternativeVerb verb = new()
+            {
+                Act = () => Insert(user, user, ent, container),
+                Text = Loc.GetString("container-verb-text-enter"),
+                Priority = 2
+            };
+            args.Verbs.Add(verb);
+        }
+    }
+
+    public bool Insert(EntityUid target, EntityUid user, EntityUid containerEntity, BaseContainer container)
+    {
+        if (!_container.Insert(user, container))
+            return false;
+
+        _adminLog.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(user):player} inserted {ToPrettyString(target):player} into container {ToPrettyString(containerEntity)}");
+        return true;
+    }
+}
diff --git a/Content.Shared/Containers/ExitContainerOnMoveComponent.cs b/Content.Shared/Containers/ExitContainerOnMoveComponent.cs
new file mode 100644 (file)
index 0000000..aae4eec
--- /dev/null
@@ -0,0 +1,14 @@
+using Robust.Shared.GameStates;
+
+namespace Content.Shared.Containers;
+
+/// <summary>
+/// This is used for a container that is exited when the entity inside of it moves.
+/// </summary>
+[RegisterComponent, NetworkedComponent]
+[Access(typeof(ExitContainerOnMoveSystem))]
+public sealed partial class ExitContainerOnMoveComponent : Component
+{
+    [DataField, ViewVariables(VVAccess.ReadWrite)]
+    public string ContainerId;
+}
diff --git a/Content.Shared/Containers/ExitContainerOnMoveSystem.cs b/Content.Shared/Containers/ExitContainerOnMoveSystem.cs
new file mode 100644 (file)
index 0000000..8b15618
--- /dev/null
@@ -0,0 +1,31 @@
+using Content.Shared.Climbing.Systems;
+using Content.Shared.Movement.Events;
+using Robust.Shared.Containers;
+
+namespace Content.Shared.Containers;
+
+public sealed class ExitContainerOnMoveSystem : EntitySystem
+{
+    [Dependency] private readonly ClimbSystem _climb = default!;
+    [Dependency] private readonly SharedContainerSystem _container = default!;
+
+    public override void Initialize()
+    {
+        base.Initialize();
+
+        SubscribeLocalEvent<ExitContainerOnMoveComponent, ContainerRelayMovementEntityEvent>(OnContainerRelay);
+    }
+
+    private void OnContainerRelay(Entity<ExitContainerOnMoveComponent> ent, ref ContainerRelayMovementEntityEvent args)
+    {
+        var (_, comp) = ent;
+        if (!TryComp<ContainerManagerComponent>(ent, out var containerManager))
+            return;
+
+        if (!_container.TryGetContainer(ent, comp.ContainerId, out var container, containerManager) || !container.Contains(args.Entity))
+            return;
+
+        _climb.ForciblySetClimbing(args.Entity, ent);
+        _container.RemoveEntity(ent, args.Entity, containerManager);
+    }
+}
index 65b050c1c474c49329e55568be94324c8f0a1208..210e21c2c9d1fe0f950352ef5c58169194f2b4c0 100644 (file)
@@ -99,7 +99,7 @@ public partial class InventorySystem : EntitySystem
 
     public InventorySlotEnumerator GetSlotEnumerator(Entity<InventoryComponent?> entity, SlotFlags flags = SlotFlags.All)
     {
-        if (!Resolve(entity.Owner, ref entity.Comp))
+        if (!Resolve(entity.Owner, ref entity.Comp, false))
             return InventorySlotEnumerator.Empty;
 
         return new InventorySlotEnumerator(entity.Comp, flags);
diff --git a/Resources/Locale/en-US/containers/containers.ftl b/Resources/Locale/en-US/containers/containers.ftl
new file mode 100644 (file)
index 0000000..ab011f6
--- /dev/null
@@ -0,0 +1,2 @@
+container-verb-text-enter = Enter
+container-verb-text-empty = Empty
index b4859768ca541838fc9d867563d896e0e2e33c1e..0e8b1d9ac79f92fc53783c01986e2ac51f45cc2c 100644 (file)
@@ -1,6 +1,7 @@
 id-card-access-level-command = Command
 id-card-access-level-captain = Captain
 id-card-access-level-head-of-personnel = Head of Personnel
+id-card-access-level-cryogenics = Cryogenics
 
 id-card-access-level-head-of-security = Head of Security
 id-card-access-level-security = Security
diff --git a/Resources/Locale/en-US/round-end/cryostorage.ftl b/Resources/Locale/en-US/round-end/cryostorage.ftl
new file mode 100644 (file)
index 0000000..7b36b52
--- /dev/null
@@ -0,0 +1,10 @@
+cryostorage-insert-message-permanent = [color=white]You are now inside of a [bold][color=cyan]cryogenic sleep unit[/color][/bold]. If you [bold]disconnect[/bold], [bold]ghost[/bold], or [bold]wait {$time} minutes[/bold], [color=red]your body will be removed[/color] and your job slot will be opened. You can exit at any time to prevent this.[/color]
+cryostorage-insert-message-temp = [color=white]You are now inside of a [bold][color=cyan]cryogenic sleep unit[/color][/bold]. If you [bold]ghost[/bold] or [bold]wait {$time} minutes[/bold], [color=red]your body will be removed[/color] and your job slot will be opened. If you [bold][color=cyan]disconnect[/color][/bold], your body will be safely held until you rejoin.[/color]
+
+cryostorage-ui-window-title = Cryogenic Sleep Unit
+cryostorage-ui-label-slot-name = [bold]{CAPITALIZE($slot)}:[/bold]
+cryostorage-ui-button-remove = Remove
+cryostorage-ui-filler-hand = inhand
+cryostorage-ui-label-no-bodies = No bodies in cryostorage
+
+cryostorage-popup-access-denied = Access denied!
index f71ca12f3bab39fcfa416134ba5a4a3a5de146aa..62193d5ffeeab0c523cf980af51c56388d15ee6b 100644 (file)
   - Command
   - Captain
   - HeadOfPersonnel
+  - Cryogenics
 
 - type: accessLevel
   id: EmergencyShuttleRepealAll
+
+- type: accessLevel
+  id: Cryogenics
+  name: id-card-access-level-cryogenics
index 848a27f4138d951be2386f10ddc232f5a1deb7c3..f79f1779c22e809b81ea2fb8bf315a2c7169b8f7 100644 (file)
@@ -9,6 +9,7 @@
   - HeadOfSecurity
   - ResearchDirector
   - Command
+  - Cryogenics
   - Security
   - Detective
   - Armory
index ec832bc761c5cbf8693840de2dab7daf0f1fb1c3..cfe94dd78af6729d47e08ef61f170bc9a59f7a6c 100644 (file)
@@ -26,6 +26,7 @@
   - Armory
   - Brig
   - Detective
+  - Cryogenics
 
 - type: accessGroup
   id: Armory
diff --git a/Resources/Prototypes/Entities/Structures/cryopod.yml b/Resources/Prototypes/Entities/Structures/cryopod.yml
new file mode 100644 (file)
index 0000000..c4d1388
--- /dev/null
@@ -0,0 +1,62 @@
+- type: entity
+  parent: BaseStructure
+  id: CryogenicSleepUnit
+  name: cryogenic sleep unit
+  description: A super-cooled container that keeps crewmates safe during space travel.
+  components:
+  - type: Sprite
+    noRot: true
+    sprite: Structures/cryostorage.rsi
+    layers:
+    - state: sleeper_0
+      map: ["base"]
+  - type: UserInterface
+    interfaces:
+    - key: enum.CryostorageUIKey.Key
+      type: CryostorageBoundUserInterface
+  - type: ActivatableUI
+    key: enum.CryostorageUIKey.Key
+  - type: AccessReader
+    breakOnEmag: false
+    access: [["Cryogenics"]]
+  - type: InteractionOutline
+  - type: Cryostorage
+  - type: Climbable
+  - type: DragInsertContainer
+    containerId: storage
+  - type: ExitContainerOnMove
+    containerId: storage
+  - type: PointLight
+    color: Lime
+    radius: 1.5
+    energy: 0.5
+    castShadows: false
+  - type: ContainerContainer
+    containers:
+      storage: !type:ContainerSlot
+  - type: Appearance
+  - type: GenericVisualizer
+    visuals:
+      enum.CryostorageVisuals.Full:
+        base:
+          True: { state: sleeper_1 }
+          False: { state: sleeper_0 }
+
+# This one handles all spawns, latejoin and roundstart.
+- type: entity
+  parent: CryogenicSleepUnit
+  id: CryogenicSleepUnitSpawner
+  suffix: Spawner, All
+  components:
+  - type: ContainerSpawnPoint
+    containerId: storage
+
+# This one only handles latejoin spawns.
+- type: entity
+  parent: CryogenicSleepUnit
+  id: CryogenicSleepUnitSpawnerLateJoin
+  suffix: Spawner, LateJoin
+  components:
+    - type: ContainerSpawnPoint
+      containerId: storage
+      spawnType: LateJoin
diff --git a/Resources/Textures/Structures/cryostorage.rsi/meta.json b/Resources/Textures/Structures/cryostorage.rsi/meta.json
new file mode 100644 (file)
index 0000000..24426d5
--- /dev/null
@@ -0,0 +1,45 @@
+{
+  "version": 1,
+  "license": "CC-BY-SA-3.0",
+  "copyright": "Taken from vg at commit https://github.com/vgstation-coders/vgstation13/commit/a16e41020a93479e9a7e2af343b1b74f7f2a61bd",
+  "size": {
+    "x": 32,
+    "y": 32
+  },
+  "states": [
+    {
+      "name": "sleeper_0",
+      "directions": 4
+    },
+    {
+      "name": "sleeper_1",
+      "directions": 4,
+      "delays": [
+        [
+          0.2,
+          0.2,
+          0.2,
+          0.2
+        ],
+        [
+          0.2,
+          0.2,
+          0.2,
+          0.2
+        ],
+        [
+          0.2,
+          0.2,
+          0.2,
+          0.2
+        ],
+        [
+          0.2,
+          0.2,
+          0.2,
+          0.2
+        ]
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png b/Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png
new file mode 100644 (file)
index 0000000..7f67b0d
Binary files /dev/null and b/Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png differ
diff --git a/Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png b/Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png
new file mode 100644 (file)
index 0000000..2061c3a
Binary files /dev/null and b/Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png differ
index 42cf8d1cabbc0c86f75694f5fa712365f66348f3..620de4225321b15861c7eb906b8689fc508cede8 100644 (file)
@@ -585,6 +585,7 @@ public sealed partial class $CLASS$ : Shared$CLASS$ {
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Computus/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Constructible/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
+       <s:Boolean x:Key="/Default/UserDictionary/Words/=Cryostorage/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Deadminned/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Dentification/@EntryIndexedValue">True</s:Boolean>
        <s:Boolean x:Key="/Default/UserDictionary/Words/=Diethylamine/@EntryIndexedValue">True</s:Boolean>