Cryogenic Sleep Units (#24096)
* Cryogenic sleep units * pause map support * no more body deletion * Cryogenic Storage Units * boowomp * no more emag, no more dropping present people
This commit is contained in:
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
Normal file
21
Content.Client/Bed/Cryostorage/CryostorageEntryControl.xaml
Normal file
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml
Normal file
33
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml
Normal file
@@ -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>
|
||||||
54
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml.cs
Normal file
54
Content.Client/Bed/Cryostorage/CryostorageMenu.xaml.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml
Normal file
13
Content.Client/Bed/Cryostorage/CryostorageSlotControl.xaml
Normal file
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Content.Client/Bed/Cryostorage/CryostorageSystem.cs
Normal file
9
Content.Client/Bed/Cryostorage/CryostorageSystem.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Content.Shared.Bed.Cryostorage;
|
||||||
|
|
||||||
|
namespace Content.Client.Bed.Cryostorage;
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public sealed class CryostorageSystem : SharedCryostorageSystem
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
309
Content.Server/Bed/Cryostorage/CryostorageSystem.cs
Normal file
309
Content.Server/Bed/Cryostorage/CryostorageSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -231,7 +231,7 @@ namespace Content.Server.GameTicking
|
|||||||
EntityManager.AddComponent<OwOAccentComponent>(mob);
|
EntityManager.AddComponent<OwOAccentComponent>(mob);
|
||||||
}
|
}
|
||||||
|
|
||||||
_stationJobs.TryAssignJob(station, jobPrototype);
|
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
|
||||||
|
|
||||||
if (lateJoin)
|
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}.");
|
_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}.");
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
using Content.Server.Station.Systems;
|
using Content.Server.Station.Systems;
|
||||||
using Content.Shared.Roles;
|
using Content.Shared.Roles;
|
||||||
using JetBrains.Annotations;
|
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.Dictionary;
|
||||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
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>))]
|
[DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||||
public HashSet<string> OverflowJobs = new();
|
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,
|
[DataField("availableJobs", required: true,
|
||||||
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
|
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
|
||||||
public Dictionary<string, List<int?>> SetupAvailableJobs = default!;
|
public Dictionary<string, List<int?>> SetupAvailableJobs = default!;
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ using Content.Shared.Roles;
|
|||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Robust.Server.Player;
|
using Robust.Server.Player;
|
||||||
using Robust.Shared.Configuration;
|
using Robust.Shared.Configuration;
|
||||||
|
using Robust.Shared.Network;
|
||||||
using Robust.Shared.Player;
|
using Robust.Shared.Player;
|
||||||
|
using Robust.Shared.Prototypes;
|
||||||
using Robust.Shared.Random;
|
using Robust.Shared.Random;
|
||||||
|
|
||||||
namespace Content.Server.Station.Systems;
|
namespace Content.Server.Station.Systems;
|
||||||
@@ -84,13 +86,14 @@ public sealed partial class StationJobsSystem : EntitySystem
|
|||||||
|
|
||||||
#region Public API
|
#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="station">Station to assign a job on.</param>
|
||||||
/// <param name="job">Job to assign.</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>
|
/// <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>
|
/// <summary>
|
||||||
@@ -98,12 +101,21 @@ public sealed partial class StationJobsSystem : EntitySystem
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="station">Station to assign a job on.</param>
|
/// <param name="station">Station to assign a job on.</param>
|
||||||
/// <param name="jobPrototypeId">Job prototype ID to assign.</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>
|
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||||
/// <returns>Whether or not assignment was a success.</returns>
|
/// <returns>Whether or not assignment was a success.</returns>
|
||||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
/// <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?)"/>
|
/// <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?)"/>
|
/// <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="station">Station to adjust the job slot on.</param>
|
||||||
/// <param name="jobPrototype">Job prototype to adjust.</param>
|
/// <param name="jobPrototype">Job prototype to adjust.</param>
|
||||||
|
|||||||
@@ -63,6 +63,12 @@ public sealed partial class AccessReaderComponent : Component
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||||
public int AccessLogLimit = 20;
|
public int AccessLogLimit = 20;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether or not emag interactions have an effect on this.
|
||||||
|
/// </summary>
|
||||||
|
[DataField]
|
||||||
|
public bool BreakOnEmag = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
[DataDefinition, Serializable, NetSerializable]
|
[DataDefinition, Serializable, NetSerializable]
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ public sealed partial class IdCardConsoleComponent : Component
|
|||||||
"ChiefEngineer",
|
"ChiefEngineer",
|
||||||
"ChiefMedicalOfficer",
|
"ChiefMedicalOfficer",
|
||||||
"Command",
|
"Command",
|
||||||
|
"Cryogenics",
|
||||||
"Engineering",
|
"Engineering",
|
||||||
"External",
|
"External",
|
||||||
"HeadOfPersonnel",
|
"HeadOfPersonnel",
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ public sealed class AccessReaderSystem : EntitySystem
|
|||||||
|
|
||||||
private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
|
private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
|
||||||
{
|
{
|
||||||
|
if (!reader.BreakOnEmag)
|
||||||
|
return;
|
||||||
args.Handled = true;
|
args.Handled = true;
|
||||||
reader.Enabled = false;
|
reader.Enabled = false;
|
||||||
reader.AccessLog.Clear();
|
reader.AccessLog.Clear();
|
||||||
|
|||||||
110
Content.Shared/Bed/Cryostorage/CryostorageComponent.cs
Normal file
110
Content.Shared/Bed/Cryostorage/CryostorageComponent.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
179
Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs
Normal file
179
Content.Shared/Bed/Cryostorage/SharedCryostorageSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -223,6 +223,12 @@ namespace Content.Shared.CCVar
|
|||||||
public static readonly CVarDef<bool>
|
public static readonly CVarDef<bool>
|
||||||
GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
|
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>
|
/// <summary>
|
||||||
/// Whether a random position offset will be applied to the station on roundstart.
|
/// Whether a random position offset will be applied to the station on roundstart.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ public sealed partial class ClimbSystem : VirtualController
|
|||||||
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
|
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!Resolve(climbable, ref comp))
|
if (!Resolve(climbable, ref comp, false))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (!ReplaceFixtures(uid, climbing, fixtures))
|
if (!ReplaceFixtures(uid, climbing, fixtures))
|
||||||
|
|||||||
20
Content.Shared/Containers/DragInsertContainerComponent.cs
Normal file
20
Content.Shared/Containers/DragInsertContainerComponent.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
120
Content.Shared/Containers/DragInsertContainerSystem.cs
Normal file
120
Content.Shared/Containers/DragInsertContainerSystem.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Content.Shared/Containers/ExitContainerOnMoveComponent.cs
Normal file
14
Content.Shared/Containers/ExitContainerOnMoveComponent.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
31
Content.Shared/Containers/ExitContainerOnMoveSystem.cs
Normal file
31
Content.Shared/Containers/ExitContainerOnMoveSystem.cs
Normal file
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -99,7 +99,7 @@ public partial class InventorySystem : EntitySystem
|
|||||||
|
|
||||||
public InventorySlotEnumerator GetSlotEnumerator(Entity<InventoryComponent?> entity, SlotFlags flags = SlotFlags.All)
|
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 InventorySlotEnumerator.Empty;
|
||||||
|
|
||||||
return new InventorySlotEnumerator(entity.Comp, flags);
|
return new InventorySlotEnumerator(entity.Comp, flags);
|
||||||
|
|||||||
2
Resources/Locale/en-US/containers/containers.ftl
Normal file
2
Resources/Locale/en-US/containers/containers.ftl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
container-verb-text-enter = Enter
|
||||||
|
container-verb-text-empty = Empty
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
id-card-access-level-command = Command
|
id-card-access-level-command = Command
|
||||||
id-card-access-level-captain = Captain
|
id-card-access-level-captain = Captain
|
||||||
id-card-access-level-head-of-personnel = Head of Personnel
|
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-head-of-security = Head of Security
|
||||||
id-card-access-level-security = Security
|
id-card-access-level-security = Security
|
||||||
|
|||||||
10
Resources/Locale/en-US/round-end/cryostorage.ftl
Normal file
10
Resources/Locale/en-US/round-end/cryostorage.ftl
Normal file
@@ -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!
|
||||||
@@ -16,6 +16,11 @@
|
|||||||
- Command
|
- Command
|
||||||
- Captain
|
- Captain
|
||||||
- HeadOfPersonnel
|
- HeadOfPersonnel
|
||||||
|
- Cryogenics
|
||||||
|
|
||||||
- type: accessLevel
|
- type: accessLevel
|
||||||
id: EmergencyShuttleRepealAll
|
id: EmergencyShuttleRepealAll
|
||||||
|
|
||||||
|
- type: accessLevel
|
||||||
|
id: Cryogenics
|
||||||
|
name: id-card-access-level-cryogenics
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
- HeadOfSecurity
|
- HeadOfSecurity
|
||||||
- ResearchDirector
|
- ResearchDirector
|
||||||
- Command
|
- Command
|
||||||
|
- Cryogenics
|
||||||
- Security
|
- Security
|
||||||
- Detective
|
- Detective
|
||||||
- Armory
|
- Armory
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
- Armory
|
- Armory
|
||||||
- Brig
|
- Brig
|
||||||
- Detective
|
- Detective
|
||||||
|
- Cryogenics
|
||||||
|
|
||||||
- type: accessGroup
|
- type: accessGroup
|
||||||
id: Armory
|
id: Armory
|
||||||
|
|||||||
62
Resources/Prototypes/Entities/Structures/cryopod.yml
Normal file
62
Resources/Prototypes/Entities/Structures/cryopod.yml
Normal file
@@ -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
|
||||||
45
Resources/Textures/Structures/cryostorage.rsi/meta.json
Normal file
45
Resources/Textures/Structures/cryostorage.rsi/meta.json
Normal file
@@ -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
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png
Normal file
BIN
Resources/Textures/Structures/cryostorage.rsi/sleeper_0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
BIN
Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png
Normal file
BIN
Resources/Textures/Structures/cryostorage.rsi/sleeper_1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@@ -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/=Computus/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Constructible/@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/=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/=Deadminned/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dentification/@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>
|
<s:Boolean x:Key="/Default/UserDictionary/Words/=Diethylamine/@EntryIndexedValue">True</s:Boolean>
|
||||||
|
|||||||
Reference in New Issue
Block a user