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:
Nemanja
2024-01-15 01:35:28 -05:00
committed by GitHub
parent 1fc3c411ca
commit 736b9dd7df
38 changed files with 1376 additions and 8 deletions

View File

@@ -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();
}
}

View 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>

View File

@@ -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;
}
}

View 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>

View 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;
}
}

View 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>

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
using Content.Shared.Bed.Cryostorage;
namespace Content.Client.Bed.Cryostorage;
/// <inheritdoc/>
public sealed class CryostorageSystem : SharedCryostorageSystem
{
}

View 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);
}
}
}

View File

@@ -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}.");

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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!;

View File

@@ -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>

View File

@@ -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]

View File

@@ -56,6 +56,7 @@ public sealed partial class IdCardConsoleComponent : Component
"ChiefEngineer", "ChiefEngineer",
"ChiefMedicalOfficer", "ChiefMedicalOfficer",
"Command", "Command",
"Cryogenics",
"Engineering", "Engineering",
"External", "External",
"HeadOfPersonnel", "HeadOfPersonnel",

View File

@@ -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();

View 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
}

View File

@@ -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;
}

View 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);
}
}

View File

@@ -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>

View File

@@ -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))

View 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;
}

View 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;
}
}

View 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;
}

View 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);
}
}

View File

@@ -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);

View File

@@ -0,0 +1,2 @@
container-verb-text-enter = Enter
container-verb-text-empty = Empty

View File

@@ -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

View 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!

View File

@@ -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

View File

@@ -9,6 +9,7 @@
- HeadOfSecurity - HeadOfSecurity
- ResearchDirector - ResearchDirector
- Command - Command
- Cryogenics
- Security - Security
- Detective - Detective
- Armory - Armory

View File

@@ -26,6 +26,7 @@
- Armory - Armory
- Brig - Brig
- Detective - Detective
- Cryogenics
- type: accessGroup - type: accessGroup
id: Armory id: Armory

View 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

View 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
]
]
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

View 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/=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>