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);
|
||||
}
|
||||
|
||||
_stationJobs.TryAssignJob(station, jobPrototype);
|
||||
_stationJobs.TryAssignJob(station, jobPrototype, player.UserId);
|
||||
|
||||
if (lateJoin)
|
||||
_adminLogger.Add(LogType.LateJoin, LogImpact.Medium, $"Player {player.Name} late joined as {character.Name:characterName} on station {Name(station):stationName} with {ToPrettyString(mob):entity} as a {jobName:jobName}.");
|
||||
|
||||
@@ -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.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Dictionary;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom.Prototype.Set;
|
||||
|
||||
@@ -75,6 +77,13 @@ public sealed partial class StationJobsComponent : Component
|
||||
[DataField("overflowJobs", customTypeSerializer: typeof(PrototypeIdHashSetSerializer<JobPrototype>))]
|
||||
public HashSet<string> OverflowJobs = new();
|
||||
|
||||
/// <summary>
|
||||
/// A dictionary relating a NetUserId to the jobs they have on station.
|
||||
/// An OOC way to track where job slots have gone.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public Dictionary<NetUserId, List<ProtoId<JobPrototype>>> PlayerJobs = new();
|
||||
|
||||
[DataField("availableJobs", required: true,
|
||||
customTypeSerializer: typeof(PrototypeIdDictionarySerializer<List<int?>, JobPrototype>))]
|
||||
public Dictionary<string, List<int?>> SetupAvailableJobs = default!;
|
||||
|
||||
@@ -9,7 +9,9 @@ using Content.Shared.Roles;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Player;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Random;
|
||||
|
||||
namespace Content.Server.Station.Systems;
|
||||
@@ -84,13 +86,14 @@ public sealed partial class StationJobsSystem : EntitySystem
|
||||
|
||||
#region Public API
|
||||
|
||||
/// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <inheritdoc cref="TryAssignJob(Robust.Shared.GameObjects.EntityUid,string,NetUserId,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to assign a job on.</param>
|
||||
/// <param name="job">Job to assign.</param>
|
||||
/// <param name="netUserId">The net user ID of the player we're assigning this job to.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
public bool TryAssignJob(EntityUid station, JobPrototype job, StationJobsComponent? stationJobs = null)
|
||||
public bool TryAssignJob(EntityUid station, JobPrototype job, NetUserId netUserId, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TryAssignJob(station, job.ID, stationJobs);
|
||||
return TryAssignJob(station, job.ID, netUserId, stationJobs);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -98,12 +101,21 @@ public sealed partial class StationJobsSystem : EntitySystem
|
||||
/// </summary>
|
||||
/// <param name="station">Station to assign a job on.</param>
|
||||
/// <param name="jobPrototypeId">Job prototype ID to assign.</param>
|
||||
/// <param name="netUserId">The net user ID of the player we're assigning this job to.</param>
|
||||
/// <param name="stationJobs">Resolve pattern, station jobs component of the station.</param>
|
||||
/// <returns>Whether or not assignment was a success.</returns>
|
||||
/// <exception cref="ArgumentException">Thrown when the given station is not a station.</exception>
|
||||
public bool TryAssignJob(EntityUid station, string jobPrototypeId, StationJobsComponent? stationJobs = null)
|
||||
public bool TryAssignJob(EntityUid station, string jobPrototypeId, NetUserId netUserId, StationJobsComponent? stationJobs = null)
|
||||
{
|
||||
return TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs);
|
||||
if (!Resolve(station, ref stationJobs, false))
|
||||
return false;
|
||||
|
||||
if (!TryAdjustJobSlot(station, jobPrototypeId, -1, false, false, stationJobs))
|
||||
return false;
|
||||
|
||||
stationJobs.PlayerJobs.TryAdd(netUserId, new());
|
||||
stationJobs.PlayerJobs[netUserId].Add(jobPrototypeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TryAdjustJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
@@ -183,6 +195,28 @@ public sealed partial class StationJobsSystem : EntitySystem
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryGetPlayerJobs(EntityUid station,
|
||||
NetUserId userId,
|
||||
[NotNullWhen(true)] out List<ProtoId<JobPrototype>>? jobs,
|
||||
StationJobsComponent? jobsComponent = null)
|
||||
{
|
||||
jobs = null;
|
||||
if (!Resolve(station, ref jobsComponent, false))
|
||||
return false;
|
||||
|
||||
return jobsComponent.PlayerJobs.TryGetValue(userId, out jobs);
|
||||
}
|
||||
|
||||
public bool TryRemovePlayerJobs(EntityUid station,
|
||||
NetUserId userId,
|
||||
StationJobsComponent? jobsComponent = null)
|
||||
{
|
||||
if (!Resolve(station, ref jobsComponent, false))
|
||||
return false;
|
||||
|
||||
return jobsComponent.PlayerJobs.Remove(userId);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="TrySetJobSlot(Robust.Shared.GameObjects.EntityUid,string,int,bool,Content.Server.Station.Components.StationJobsComponent?)"/>
|
||||
/// <param name="station">Station to adjust the job slot on.</param>
|
||||
/// <param name="jobPrototype">Job prototype to adjust.</param>
|
||||
|
||||
@@ -63,6 +63,12 @@ public sealed partial class AccessReaderComponent : Component
|
||||
/// </summary>
|
||||
[DataField, ViewVariables(VVAccess.ReadWrite)]
|
||||
public int AccessLogLimit = 20;
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not emag interactions have an effect on this.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public bool BreakOnEmag = true;
|
||||
}
|
||||
|
||||
[DataDefinition, Serializable, NetSerializable]
|
||||
|
||||
@@ -56,6 +56,7 @@ public sealed partial class IdCardConsoleComponent : Component
|
||||
"ChiefEngineer",
|
||||
"ChiefMedicalOfficer",
|
||||
"Command",
|
||||
"Cryogenics",
|
||||
"Engineering",
|
||||
"External",
|
||||
"HeadOfPersonnel",
|
||||
|
||||
@@ -76,6 +76,8 @@ public sealed class AccessReaderSystem : EntitySystem
|
||||
|
||||
private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
|
||||
{
|
||||
if (!reader.BreakOnEmag)
|
||||
return;
|
||||
args.Handled = true;
|
||||
reader.Enabled = false;
|
||||
reader.AccessLog.Clear();
|
||||
|
||||
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>
|
||||
GameRoleTimers = CVarDef.Create("game.role_timers", true, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Whether or not disconnecting inside of a cryopod should remove the character or just store them until they reconnect.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<bool>
|
||||
GameCryoSleepRejoining = CVarDef.Create("game.cryo_sleep_rejoining", false, CVar.SERVER | CVar.REPLICATED);
|
||||
|
||||
/// <summary>
|
||||
/// Whether a random position offset will be applied to the station on roundstart.
|
||||
/// </summary>
|
||||
|
||||
@@ -247,7 +247,7 @@ public sealed partial class ClimbSystem : VirtualController
|
||||
if (!Resolve(uid, ref climbing, ref physics, ref fixtures, false))
|
||||
return;
|
||||
|
||||
if (!Resolve(climbable, ref comp))
|
||||
if (!Resolve(climbable, ref comp, false))
|
||||
return;
|
||||
|
||||
if (!ReplaceFixtures(uid, climbing, fixtures))
|
||||
|
||||
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)
|
||||
{
|
||||
if (!Resolve(entity.Owner, ref entity.Comp))
|
||||
if (!Resolve(entity.Owner, ref entity.Comp, false))
|
||||
return InventorySlotEnumerator.Empty;
|
||||
|
||||
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-captain = Captain
|
||||
id-card-access-level-head-of-personnel = Head of Personnel
|
||||
id-card-access-level-cryogenics = Cryogenics
|
||||
|
||||
id-card-access-level-head-of-security = Head of Security
|
||||
id-card-access-level-security = Security
|
||||
|
||||
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
|
||||
- Captain
|
||||
- HeadOfPersonnel
|
||||
- Cryogenics
|
||||
|
||||
- type: accessLevel
|
||||
id: EmergencyShuttleRepealAll
|
||||
|
||||
- type: accessLevel
|
||||
id: Cryogenics
|
||||
name: id-card-access-level-cryogenics
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
- HeadOfSecurity
|
||||
- ResearchDirector
|
||||
- Command
|
||||
- Cryogenics
|
||||
- Security
|
||||
- Detective
|
||||
- Armory
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
- Armory
|
||||
- Brig
|
||||
- Detective
|
||||
- Cryogenics
|
||||
|
||||
- type: accessGroup
|
||||
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/=Constructible/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cooldowns/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Cryostorage/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Deadminned/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Dentification/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Diethylamine/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
Reference in New Issue
Block a user