Genpop Closets & IDs (#36392)
* Genpop IDs and Lockers * placeholder generation, no ui yet. * UI * Fix time offset * fix meta.jsons * big speller * Scarkyo review * Add turnstile prototypes * make IDs recyclable --------- Co-authored-by: ArtisticRoomba <145879011+ArtisticRoomba@users.noreply.github.com> Co-authored-by: ScarKy0 <106310278+ScarKy0@users.noreply.github.com>
9
Content.Client/Security/GenpopSystem.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using Content.Shared.Security.Systems;
|
||||
|
||||
namespace Content.Client.Security;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public sealed class GenpopSystem : SharedGenpopSystem
|
||||
{
|
||||
|
||||
}
|
||||
36
Content.Client/Security/Ui/GenpopLockerBoundUserInterface.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Content.Shared.Security.Components;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Content.Client.Security.Ui;
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class GenpopLockerBoundUserInterface(EntityUid owner, Enum uiKey) : BoundUserInterface(owner, uiKey)
|
||||
{
|
||||
private GenpopLockerMenu? _menu;
|
||||
|
||||
protected override void Open()
|
||||
{
|
||||
base.Open();
|
||||
|
||||
_menu = new(Owner, EntMan);
|
||||
|
||||
_menu.OnConfigurationComplete += (name, time, crime) =>
|
||||
{
|
||||
SendMessage(new GenpopLockerIdConfiguredMessage(name, time, crime));
|
||||
Close();
|
||||
};
|
||||
|
||||
_menu.OnClose += Close;
|
||||
_menu.OpenCentered();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
if (!disposing)
|
||||
return;
|
||||
_menu?.Orphan();
|
||||
_menu = null;
|
||||
}
|
||||
}
|
||||
|
||||
24
Content.Client/Security/Ui/GenpopLockerMenu.xaml
Normal file
@@ -0,0 +1,24 @@
|
||||
<controls:FancyWindow
|
||||
xmlns="https://spacestation14.io"
|
||||
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
|
||||
MinSize="400 230"
|
||||
SetSize="450 260">
|
||||
<BoxContainer Orientation="Vertical" VerticalExpand="True" HorizontalExpand="True" Margin="10 5 10 10">
|
||||
<BoxContainer Orientation="Vertical" VerticalAlignment="Top" VerticalExpand="True" HorizontalExpand="True" Margin="20 0">
|
||||
<RichTextLabel Name="NameLabel"/>
|
||||
<LineEdit Name="NameEdit"/>
|
||||
<Control MinWidth="5"/>
|
||||
<RichTextLabel Name="SentenceLabel"/>
|
||||
<LineEdit Name="SentenceEdit"/>
|
||||
<Control MinWidth="5"/>
|
||||
<RichTextLabel Name="CrimeLabel"/>
|
||||
<LineEdit Name="CrimeEdit"/>
|
||||
</BoxContainer>
|
||||
<Control VerticalExpand="True"/>
|
||||
<BoxContainer VerticalExpand="True" VerticalAlignment="Bottom" HorizontalAlignment="Right">
|
||||
<Button Name="DoneButton" Text="{Loc 'genpop-locket-ui-button-done'}" Disabled="True"/>
|
||||
</BoxContainer>
|
||||
</BoxContainer>
|
||||
</controls:FancyWindow>
|
||||
|
||||
|
||||
49
Content.Client/Security/Ui/GenpopLockerMenu.xaml.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Content.Client.Message;
|
||||
using Content.Client.UserInterface.Controls;
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Security.Components;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Content.Client.Security.Ui;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
public sealed partial class GenpopLockerMenu : FancyWindow
|
||||
{
|
||||
public event Action<string, float, string>? OnConfigurationComplete;
|
||||
|
||||
public GenpopLockerMenu(EntityUid owner, IEntityManager entMan)
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
Title = entMan.GetComponent<MetaDataComponent>(owner).EntityName;
|
||||
|
||||
NameLabel.SetMarkup(Loc.GetString("genpop-locker-ui-label-name"));
|
||||
SentenceLabel.SetMarkup(Loc.GetString("genpop-locker-ui-label-sentence"));
|
||||
CrimeLabel.SetMarkup(Loc.GetString("genpop-locker-ui-label-crime"));
|
||||
|
||||
SentenceEdit.Text = "5";
|
||||
CrimeEdit.Text = Loc.GetString("genpop-prisoner-id-crime-default");
|
||||
|
||||
NameEdit.IsValid = val => !string.IsNullOrWhiteSpace(val) && val.Length <= IdCardConsoleComponent.MaxFullNameLength;
|
||||
SentenceEdit.IsValid = val => float.TryParse(val, out var f) && f >= 0;
|
||||
CrimeEdit.IsValid = val => !string.IsNullOrWhiteSpace(val) && val.Length <= GenpopLockerComponent.MaxCrimeLength;
|
||||
|
||||
NameEdit.OnTextChanged += _ => OnTextEdit();
|
||||
SentenceEdit.OnTextChanged += _ => OnTextEdit();
|
||||
CrimeEdit.OnTextChanged += _ => OnTextEdit();
|
||||
|
||||
DoneButton.OnPressed += _ =>
|
||||
{
|
||||
OnConfigurationComplete?.Invoke(NameEdit.Text, float.Parse(SentenceEdit.Text), CrimeEdit.Text);
|
||||
};
|
||||
}
|
||||
|
||||
private void OnTextEdit()
|
||||
{
|
||||
DoneButton.Disabled = string.IsNullOrWhiteSpace(NameEdit.Text) ||
|
||||
!float.TryParse(SentenceEdit.Text, out var sentence) ||
|
||||
sentence < 0 ||
|
||||
string.IsNullOrWhiteSpace(CrimeEdit.Text);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Linq;
|
||||
using Content.Server.Administration.Logs;
|
||||
using Content.Server.Chat.Systems;
|
||||
using Content.Server.Kitchen.Components;
|
||||
using Content.Server.Popups;
|
||||
using Content.Shared.Access;
|
||||
@@ -19,6 +20,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
||||
[Dependency] private readonly IRobustRandom _random = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
[Dependency] private readonly IAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly ChatSystem _chat = default!;
|
||||
[Dependency] private readonly MicrowaveSystem _microwave = default!;
|
||||
|
||||
public override void Initialize()
|
||||
@@ -93,4 +95,22 @@ public sealed class IdCardSystem : SharedIdCardSystem
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExpireId(Entity<ExpireIdCardComponent> ent)
|
||||
{
|
||||
if (ent.Comp.Expired)
|
||||
return;
|
||||
|
||||
base.ExpireId(ent);
|
||||
|
||||
if (ent.Comp.ExpireMessage != null)
|
||||
{
|
||||
_chat.TrySendInGameICMessage(
|
||||
ent,
|
||||
Loc.GetString(ent.Comp.ExpireMessage),
|
||||
InGameICChatType.Speak,
|
||||
ChatTransmitRange.Normal,
|
||||
true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
30
Content.Server/Security/GenpopSystem.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Content.Shared.Security.Components;
|
||||
using Content.Shared.Security.Systems;
|
||||
|
||||
namespace Content.Server.Security;
|
||||
|
||||
public sealed class GenpopSystem : SharedGenpopSystem
|
||||
{
|
||||
protected override void CreateId(Entity<GenpopLockerComponent> ent, string name, float sentence, string crime)
|
||||
{
|
||||
var xform = Transform(ent);
|
||||
var uid = Spawn(ent.Comp.IdCardProto, xform.Coordinates);
|
||||
ent.Comp.LinkedId = uid;
|
||||
IdCard.TryChangeFullName(uid, name);
|
||||
|
||||
if (TryComp<GenpopIdCardComponent>(uid, out var id))
|
||||
{
|
||||
id.Crime = crime;
|
||||
id.SentenceDuration = TimeSpan.FromMinutes(sentence);
|
||||
Dirty(uid, id);
|
||||
}
|
||||
if (sentence <= 0)
|
||||
IdCard.SetPermanent(uid, true);
|
||||
IdCard.SetExpireTime(uid, TimeSpan.FromMinutes(sentence) + Timing.CurTime);
|
||||
|
||||
var metaData = MetaData(ent);
|
||||
MetaDataSystem.SetEntityName(ent, Loc.GetString("genpop-locker-name-used", ("name", name)), metaData);
|
||||
MetaDataSystem.SetEntityDescription(ent, Loc.GetString("genpop-locker-desc-used", ("name", name)), metaData);
|
||||
Dirty(ent);
|
||||
}
|
||||
}
|
||||
44
Content.Shared/Access/Components/ExpireIdCardComponent.cs
Normal file
@@ -0,0 +1,44 @@
|
||||
using Content.Shared.Access.Systems;
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
|
||||
|
||||
namespace Content.Shared.Access.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for an ID that expires and replaces its access after a certain period has passed.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
[Access(typeof(SharedIdCardSystem))]
|
||||
public sealed partial class ExpireIdCardComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether this ID has expired yet and had its accesses replaced.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Expired;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this card will expire at all.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool Permanent;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which this card will expire and the access will be removed.
|
||||
/// </summary>
|
||||
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField, AutoNetworkedField]
|
||||
public TimeSpan ExpireTime = TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Access the replaces current access once this card expires.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public HashSet<ProtoId<AccessLevelPrototype>> ExpiredAccess = new();
|
||||
|
||||
/// <summary>
|
||||
/// Line spoken by the card when it expires.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public LocId? ExpireMessage;
|
||||
}
|
||||
@@ -9,12 +9,15 @@ using Content.Shared.PDA;
|
||||
using Content.Shared.Roles;
|
||||
using Content.Shared.StatusIcon;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Access.Systems;
|
||||
|
||||
public abstract class SharedIdCardSystem : EntitySystem
|
||||
{
|
||||
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
[Dependency] private readonly SharedAccessSystem _access = default!;
|
||||
[Dependency] private readonly InventorySystem _inventorySystem = default!;
|
||||
[Dependency] private readonly MetaDataSystem _metaSystem = default!;
|
||||
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
|
||||
@@ -256,4 +259,49 @@ public abstract class SharedIdCardSystem : EntitySystem
|
||||
return $"{idCardComponent.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(idCardComponent.LocalizedJobTitle ?? string.Empty)})"
|
||||
.Trim();
|
||||
}
|
||||
|
||||
public void SetExpireTime(Entity<ExpireIdCardComponent?> ent, TimeSpan time)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
ent.Comp.ExpireTime = time;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
public void SetPermanent(Entity<ExpireIdCardComponent?> ent, bool val)
|
||||
{
|
||||
if (!Resolve(ent, ref ent.Comp))
|
||||
return;
|
||||
ent.Comp.Permanent = val;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Marks an <see cref="ExpireIdCardComponent"/> as expired, setting the accesses.
|
||||
/// </summary>
|
||||
public virtual void ExpireId(Entity<ExpireIdCardComponent> ent)
|
||||
{
|
||||
if (ent.Comp.Expired)
|
||||
return;
|
||||
|
||||
_access.TrySetTags(ent, ent.Comp.ExpiredAccess);
|
||||
ent.Comp.Expired = true;
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
public override void Update(float frameTime)
|
||||
{
|
||||
base.Update(frameTime);
|
||||
var query = EntityQueryEnumerator<ExpireIdCardComponent>();
|
||||
while (query.MoveNext(out var uid, out var comp))
|
||||
{
|
||||
if (comp.Expired || comp.Permanent)
|
||||
continue;
|
||||
|
||||
if (_timing.CurTime < comp.ExpireTime)
|
||||
continue;
|
||||
|
||||
ExpireId((uid, comp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.DoAfter;
|
||||
using Robust.Shared.Audio;
|
||||
using Robust.Shared.GameStates;
|
||||
@@ -33,6 +34,12 @@ public sealed partial class LockComponent : Component
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool UnlockOnClick = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the lock requires access validation through <see cref="AccessReaderComponent"/>
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public bool UseAccess = true;
|
||||
|
||||
/// <summary>
|
||||
/// The sound played when unlocked.
|
||||
/// </summary>
|
||||
|
||||
@@ -118,7 +118,7 @@ public sealed class LockSystem : EntitySystem
|
||||
if (!CanToggleLock(uid, user, quiet: false))
|
||||
return false;
|
||||
|
||||
if (!HasUserAccess(uid, user, quiet: false))
|
||||
if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
|
||||
return false;
|
||||
|
||||
if (!skipDoAfter && lockComp.LockTime != TimeSpan.Zero)
|
||||
@@ -145,6 +145,9 @@ public sealed class LockSystem : EntitySystem
|
||||
if (!Resolve(uid, ref lockComp))
|
||||
return;
|
||||
|
||||
if (lockComp.Locked)
|
||||
return;
|
||||
|
||||
if (user is { Valid: true })
|
||||
{
|
||||
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-lock-success",
|
||||
@@ -175,6 +178,9 @@ public sealed class LockSystem : EntitySystem
|
||||
if (!Resolve(uid, ref lockComp))
|
||||
return;
|
||||
|
||||
if (!lockComp.Locked)
|
||||
return;
|
||||
|
||||
if (user is { Valid: true })
|
||||
{
|
||||
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-unlock-success",
|
||||
@@ -211,7 +217,7 @@ public sealed class LockSystem : EntitySystem
|
||||
if (!CanToggleLock(uid, user, quiet: false))
|
||||
return false;
|
||||
|
||||
if (!HasUserAccess(uid, user, quiet: false))
|
||||
if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
|
||||
return false;
|
||||
|
||||
if (!skipDoAfter && lockComp.UnlockTime != TimeSpan.Zero)
|
||||
|
||||
22
Content.Shared/Security/Components/GenpopIdCardComponent.cs
Normal file
@@ -0,0 +1,22 @@
|
||||
using Robust.Shared.GameStates;
|
||||
|
||||
namespace Content.Shared.Security.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for storing information about a Genpop ID in order to correctly display it on examine.
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState, AutoGenerateComponentPause]
|
||||
public sealed partial class GenpopIdCardComponent : Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The crime committed, as a string.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public string Crime = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The length of the sentence
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField, AutoPausedField]
|
||||
public TimeSpan SentenceDuration;
|
||||
}
|
||||
47
Content.Shared/Security/Components/GenpopLockerComponent.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using Robust.Shared.GameStates;
|
||||
using Robust.Shared.Prototypes;
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Content.Shared.Security.Components;
|
||||
|
||||
/// <summary>
|
||||
/// This is used for a locker that automatically sets up and handles a <see cref="GenpopIdCardComponent"/>
|
||||
/// </summary>
|
||||
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState]
|
||||
public sealed partial class GenpopLockerComponent : Component
|
||||
{
|
||||
public const int MaxCrimeLength = 48;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GenpopIdCardComponent"/> that this locker is currently associated with.
|
||||
/// </summary>
|
||||
[DataField, AutoNetworkedField]
|
||||
public EntityUid? LinkedId;
|
||||
|
||||
/// <summary>
|
||||
/// The Prototype spawned.
|
||||
/// </summary>
|
||||
[DataField]
|
||||
public EntProtoId<GenpopIdCardComponent> IdCardProto = "PrisonerIDCard";
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public sealed class GenpopLockerIdConfiguredMessage : BoundUserInterfaceMessage
|
||||
{
|
||||
public string Name;
|
||||
public float Sentence;
|
||||
public string Crime;
|
||||
|
||||
public GenpopLockerIdConfiguredMessage(string name, float sentence, string crime)
|
||||
{
|
||||
Name = name;
|
||||
Sentence = sentence;
|
||||
Crime = crime;
|
||||
}
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
public enum GenpopLockerUiKey : byte
|
||||
{
|
||||
Key
|
||||
}
|
||||
240
Content.Shared/Security/Systems/SharedGenpopSystem.cs
Normal file
@@ -0,0 +1,240 @@
|
||||
using Content.Shared.Access.Components;
|
||||
using Content.Shared.Access.Systems;
|
||||
using Content.Shared.Database;
|
||||
using Content.Shared.Examine;
|
||||
using Content.Shared.Lock;
|
||||
using Content.Shared.Popups;
|
||||
using Content.Shared.Security.Components;
|
||||
using Content.Shared.Storage.Components;
|
||||
using Content.Shared.Storage.EntitySystems;
|
||||
using Content.Shared.Verbs;
|
||||
using Robust.Shared.Timing;
|
||||
|
||||
namespace Content.Shared.Security.Systems;
|
||||
|
||||
public abstract class SharedGenpopSystem : EntitySystem
|
||||
{
|
||||
[Dependency] protected readonly IGameTiming Timing = default!;
|
||||
[Dependency] private readonly AccessReaderSystem _accessReader = default!;
|
||||
[Dependency] private readonly SharedEntityStorageSystem _entityStorage = default!;
|
||||
[Dependency] protected readonly SharedIdCardSystem IdCard = default!;
|
||||
[Dependency] private readonly LockSystem _lock = default!;
|
||||
[Dependency] protected readonly MetaDataSystem MetaDataSystem = default!;
|
||||
[Dependency] private readonly SharedPopupSystem _popup = default!;
|
||||
[Dependency] private readonly SharedUserInterfaceSystem _userInterface = default!;
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Initialize()
|
||||
{
|
||||
SubscribeLocalEvent<GenpopLockerComponent, GenpopLockerIdConfiguredMessage>(OnIdConfigured);
|
||||
SubscribeLocalEvent<GenpopLockerComponent, StorageCloseAttemptEvent>(OnCloseAttempt);
|
||||
SubscribeLocalEvent<GenpopLockerComponent, LockToggleAttemptEvent>(OnLockToggleAttempt);
|
||||
SubscribeLocalEvent<GenpopLockerComponent, LockToggledEvent>(OnLockToggled);
|
||||
SubscribeLocalEvent<GenpopLockerComponent, GetVerbsEvent<Verb>>(OnGetVerbs);
|
||||
SubscribeLocalEvent<GenpopIdCardComponent, ExaminedEvent>(OnExamine);
|
||||
}
|
||||
|
||||
private void OnIdConfigured(Entity<GenpopLockerComponent> ent, ref GenpopLockerIdConfiguredMessage args)
|
||||
{
|
||||
// validation.
|
||||
if (string.IsNullOrWhiteSpace(args.Name) || args.Name.Length > IdCardConsoleComponent.MaxFullNameLength ||
|
||||
args.Sentence < 0 ||
|
||||
string.IsNullOrWhiteSpace(args.Crime) || args.Crime.Length > GenpopLockerComponent.MaxCrimeLength)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_accessReader.IsAllowed(args.Actor, ent))
|
||||
return;
|
||||
|
||||
// We don't spawn the actual ID now because then the locker would eat it.
|
||||
// Instead, we just fill in the spot temporarily til the checks pass.
|
||||
ent.Comp.LinkedId = EntityUid.Invalid;
|
||||
|
||||
_lock.Lock(ent.Owner, null);
|
||||
_entityStorage.CloseStorage(ent);
|
||||
|
||||
CreateId(ent, args.Name, args.Sentence, args.Crime);
|
||||
}
|
||||
|
||||
private void OnCloseAttempt(Entity<GenpopLockerComponent> ent, ref StorageCloseAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
// We cancel no matter what. Our second option is just opening the closet.
|
||||
if (ent.Comp.LinkedId == null)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
}
|
||||
|
||||
if (args.User is not { } user)
|
||||
return;
|
||||
|
||||
if (!_accessReader.IsAllowed(user, ent))
|
||||
{
|
||||
_popup.PopupClient(Loc.GetString("lock-comp-has-user-access-fail"), user);
|
||||
return;
|
||||
}
|
||||
|
||||
// my heart yearns for this to be predicted but for some reason opening an entitystorage via
|
||||
// verb does not predict it properly.
|
||||
_userInterface.TryOpenUi(ent.Owner, GenpopLockerUiKey.Key, user);
|
||||
}
|
||||
|
||||
private void OnLockToggleAttempt(Entity<GenpopLockerComponent> ent, ref LockToggleAttemptEvent args)
|
||||
{
|
||||
if (args.Cancelled)
|
||||
return;
|
||||
|
||||
if (ent.Comp.LinkedId == null)
|
||||
{
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure that we both have the linked ID on our person AND the ID has actually expired.
|
||||
// That way, even if someone escapes early, they can't get ahold of their things.
|
||||
if (!_accessReader.FindPotentialAccessItems(args.User).Contains(ent.Comp.LinkedId.Value))
|
||||
{
|
||||
if (!args.Silent)
|
||||
_popup.PopupClient(Loc.GetString("lock-comp-has-user-access-fail"), ent, args.User);
|
||||
args.Cancelled = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TryComp<ExpireIdCardComponent>(ent.Comp.LinkedId.Value, out var expireIdCard) ||
|
||||
!expireIdCard.Expired)
|
||||
{
|
||||
if (!args.Silent)
|
||||
_popup.PopupClient(Loc.GetString("genpop-prisoner-id-popup-not-served"), ent, args.User);
|
||||
args.Cancelled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLockToggled(Entity<GenpopLockerComponent> ent, ref LockToggledEvent args)
|
||||
{
|
||||
if (args.Locked)
|
||||
return;
|
||||
|
||||
// If we unlock the door, then we're gonna reset the ID.
|
||||
CancelIdCard(ent);
|
||||
}
|
||||
|
||||
private void OnGetVerbs(Entity<GenpopLockerComponent> ent, ref GetVerbsEvent<Verb> args)
|
||||
{
|
||||
if (ent.Comp.LinkedId == null)
|
||||
return;
|
||||
|
||||
if (!args.CanAccess || !args.CanComplexInteract || !args.CanInteract)
|
||||
return;
|
||||
|
||||
if (!TryComp<ExpireIdCardComponent>(ent.Comp.LinkedId, out var expire) ||
|
||||
!TryComp<GenpopIdCardComponent>(ent.Comp.LinkedId, out var genpopId))
|
||||
return;
|
||||
|
||||
var user = args.User;
|
||||
var hasAccess = _accessReader.IsAllowed(args.User, ent);
|
||||
args.Verbs.Add(new Verb // End sentence early.
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
IdCard.ExpireId((ent.Comp.LinkedId.Value, expire));
|
||||
},
|
||||
Priority = 13,
|
||||
Text = Loc.GetString("genpop-locker-action-end-early"),
|
||||
Impact = LogImpact.Medium,
|
||||
DoContactInteraction = true,
|
||||
Disabled = !hasAccess,
|
||||
});
|
||||
|
||||
args.Verbs.Add(new Verb // Cancel Sentence.
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
CancelIdCard(ent, user);
|
||||
},
|
||||
Priority = 12,
|
||||
Text = Loc.GetString("genpop-locker-action-clear-id"),
|
||||
Impact = LogImpact.Medium,
|
||||
DoContactInteraction = true,
|
||||
Disabled = !hasAccess,
|
||||
});
|
||||
|
||||
var servedTime = 1 - (expire.ExpireTime - Timing.CurTime).TotalSeconds / genpopId.SentenceDuration.TotalSeconds;
|
||||
|
||||
// Can't reset it after its expired.
|
||||
if (expire.Expired)
|
||||
return;
|
||||
|
||||
args.Verbs.Add(new Verb // Reset Sentence.
|
||||
{
|
||||
Act = () =>
|
||||
{
|
||||
IdCard.SetExpireTime((ent.Comp.LinkedId.Value, expire), Timing.CurTime + genpopId.SentenceDuration);
|
||||
},
|
||||
Priority = 11,
|
||||
Text = Loc.GetString("genpop-locker-action-reset-sentence", ("percent", Math.Clamp(servedTime, 0, 1) * 100)),
|
||||
Impact = LogImpact.Medium,
|
||||
DoContactInteraction = true,
|
||||
Disabled = !hasAccess,
|
||||
});
|
||||
}
|
||||
|
||||
private void CancelIdCard(Entity<GenpopLockerComponent> ent, EntityUid? user = null)
|
||||
{
|
||||
if (ent.Comp.LinkedId == null)
|
||||
return;
|
||||
|
||||
var metaData = MetaData(ent);
|
||||
MetaDataSystem.SetEntityName(ent, Loc.GetString("genpop-locker-name-default"), metaData);
|
||||
MetaDataSystem.SetEntityDescription(ent, Loc.GetString("genpop-locker-desc-default"), metaData);
|
||||
|
||||
ent.Comp.LinkedId = null;
|
||||
_lock.Unlock(ent.Owner, user);
|
||||
_entityStorage.OpenStorage(ent.Owner);
|
||||
|
||||
if (TryComp<ExpireIdCardComponent>(ent.Comp.LinkedId, out var expire))
|
||||
IdCard.ExpireId((ent.Comp.LinkedId.Value, expire));
|
||||
|
||||
Dirty(ent);
|
||||
}
|
||||
|
||||
private void OnExamine(Entity<GenpopIdCardComponent> ent, ref ExaminedEvent args)
|
||||
{
|
||||
// This component holds the contextual data for the sentence end time and other such things.
|
||||
if (!TryComp<ExpireIdCardComponent>(ent, out var expireIdCard))
|
||||
return;
|
||||
|
||||
if (expireIdCard.Permanent)
|
||||
{
|
||||
args.PushText(Loc.GetString("genpop-prisoner-id-examine-wait-perm",
|
||||
("crime", ent.Comp.Crime)));
|
||||
}
|
||||
else
|
||||
{
|
||||
if (expireIdCard.Expired)
|
||||
{
|
||||
args.PushText(Loc.GetString("genpop-prisoner-id-examine-served",
|
||||
("crime", ent.Comp.Crime)));
|
||||
}
|
||||
else
|
||||
{
|
||||
var sentence = ent.Comp.SentenceDuration;
|
||||
var served = ent.Comp.SentenceDuration - (expireIdCard.ExpireTime - Timing.CurTime);
|
||||
|
||||
args.PushText(Loc.GetString("genpop-prisoner-id-examine-wait",
|
||||
("minutes", served.Minutes),
|
||||
("seconds", served.Seconds),
|
||||
("sentence", sentence.TotalMinutes),
|
||||
("crime", ent.Comp.Crime)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual void CreateId(Entity<GenpopLockerComponent> ent, string name, float sentence, string crime)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -159,7 +159,7 @@ public readonly record struct StorageBeforeOpenEvent;
|
||||
public readonly record struct StorageAfterOpenEvent;
|
||||
|
||||
[ByRefEvent]
|
||||
public record struct StorageCloseAttemptEvent(bool Cancelled = false);
|
||||
public record struct StorageCloseAttemptEvent(EntityUid? User, bool Cancelled = false);
|
||||
|
||||
[ByRefEvent]
|
||||
public readonly record struct StorageBeforeCloseEvent(HashSet<EntityUid> Contents, HashSet<EntityUid> BypassChecks);
|
||||
|
||||
@@ -181,7 +181,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem
|
||||
|
||||
if (component.Open)
|
||||
{
|
||||
TryCloseStorage(target);
|
||||
TryCloseStorage(target, user);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -360,9 +360,9 @@ public abstract class SharedEntityStorageSystem : EntitySystem
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryCloseStorage(EntityUid target)
|
||||
public bool TryCloseStorage(EntityUid target, EntityUid? user = null)
|
||||
{
|
||||
if (!CanClose(target))
|
||||
if (!CanClose(target, user))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -413,9 +413,9 @@ public abstract class SharedEntityStorageSystem : EntitySystem
|
||||
return !ev.Cancelled;
|
||||
}
|
||||
|
||||
public bool CanClose(EntityUid target, bool silent = false)
|
||||
public bool CanClose(EntityUid target, EntityUid? user = null, bool silent = false)
|
||||
{
|
||||
var ev = new StorageCloseAttemptEvent();
|
||||
var ev = new StorageCloseAttemptEvent(user);
|
||||
RaiseLocalEvent(target, ref ev, silent);
|
||||
|
||||
return !ev.Cancelled;
|
||||
|
||||
28
Resources/Locale/en-US/access/components/genpop.ftl
Normal file
@@ -0,0 +1,28 @@
|
||||
genpop-prisoner-id-expire = You have served your sentence! You may now exit prison through the turnstiles and collect your belongings.
|
||||
genpop-prisoner-id-popup-not-served = Sentence not yet served!
|
||||
|
||||
genpop-prisoner-id-crime-default = [Redacted]
|
||||
genpop-prisoner-id-examine-wait = You have served {$minutes} {$minutes ->
|
||||
[1] minute
|
||||
*[other] minutes
|
||||
} {$seconds} {$seconds ->
|
||||
[1] second
|
||||
*[other] seconds
|
||||
} of your {$sentence} minute sentence for {$crime}.
|
||||
genpop-prisoner-id-examine-wait-perm = You are serving a permanent sentence for {$crime}.
|
||||
genpop-prisoner-id-examine-served = You have served your sentence for {$crime}.
|
||||
|
||||
genpop-locker-name-default = prisoner closet
|
||||
genpop-locker-desc-default = It's a secure locker for an inmate's personal belongings during their time in prison.
|
||||
|
||||
genpop-locker-name-used = prisoner closet ({$name})
|
||||
genpop-locker-desc-used = It's a secure locker for an inmate's personal belongings during their time in prison. It contains the personal effects of {$name}.
|
||||
|
||||
genpop-locker-ui-label-name = [bold]Convict Name:[/bold]
|
||||
genpop-locker-ui-label-sentence = [bold]Sentence length in minutes:[/bold] [color=gray](0 for perma)[/color]
|
||||
genpop-locker-ui-label-crime = [bold]Crime:[/bold]
|
||||
genpop-locket-ui-button-done = Done
|
||||
|
||||
genpop-locker-action-end-early = End Sentence Early
|
||||
genpop-locker-action-clear-id = Clear ID
|
||||
genpop-locker-action-reset-sentence = Reset Sentence ({NATURALFIXED($percent, 0)}% served)
|
||||
@@ -9,6 +9,8 @@ id-card-access-level-security = Security
|
||||
id-card-access-level-armory = Armory
|
||||
id-card-access-level-brig = Brig
|
||||
id-card-access-level-detective = Detective
|
||||
id-card-access-level-genpop-enter = Enter Genpop
|
||||
id-card-access-level-genpop-leave = Leave Genpop
|
||||
|
||||
id-card-access-level-chief-engineer = Chief Engineer
|
||||
id-card-access-level-engineering = Engineering
|
||||
@@ -50,4 +52,4 @@ id-card-access-level-station-ai = Artifical Intelligence
|
||||
id-card-access-level-borg = Cyborg
|
||||
id-card-access-level-basic-silicon = Robot
|
||||
|
||||
id-card-access-level-basic-xenoborg = Xenoborg
|
||||
id-card-access-level-basic-xenoborg = Xenoborg
|
||||
|
||||
@@ -32,3 +32,5 @@
|
||||
- Chapel
|
||||
- Hydroponics
|
||||
- Atmospherics
|
||||
- GenpopEnter
|
||||
- GenpopLeave
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
id: Detective
|
||||
name: id-card-access-level-detective
|
||||
|
||||
- type: accessLevel
|
||||
id: GenpopEnter
|
||||
name: id-card-access-level-genpop-enter
|
||||
|
||||
- type: accessLevel
|
||||
id: GenpopLeave
|
||||
name: id-card-access-level-genpop-leave
|
||||
|
||||
- type: accessGroup
|
||||
id: Security
|
||||
tags:
|
||||
|
||||
@@ -722,6 +722,40 @@
|
||||
- type: PresetIdCard
|
||||
job: Detective
|
||||
|
||||
- type: entity
|
||||
parent: IDCardStandard
|
||||
id: PrisonerIDCard
|
||||
name: prisoner ID card
|
||||
description: A generically printed ID card for scummy prisoners.
|
||||
components:
|
||||
- type: Sprite
|
||||
layers:
|
||||
- state: orange
|
||||
- type: Item
|
||||
heldPrefix: orange
|
||||
- type: Access
|
||||
tags:
|
||||
- GenpopEnter
|
||||
- type: GenpopIdCard
|
||||
- type: IdCard
|
||||
jobTitle: job-name-prisoner
|
||||
jobIcon: JobIconPrisoner
|
||||
canMicrowave: false
|
||||
- type: ExpireIdCard
|
||||
expireMessage: genpop-prisoner-id-expire
|
||||
expiredAccess:
|
||||
- GenpopLeave
|
||||
- type: Speech
|
||||
speechVerb: Robotic
|
||||
- type: Tag
|
||||
tags:
|
||||
- DoorBumpOpener
|
||||
- WhitelistChameleon
|
||||
- WhitelistChameleonIdCard
|
||||
- Recyclable
|
||||
- type: StaticPrice # these are infinitely producible.
|
||||
price: 0
|
||||
|
||||
- type: entity
|
||||
parent: CentcomIDCard
|
||||
id: CBURNIDcard
|
||||
|
||||
@@ -126,6 +126,8 @@
|
||||
- SyndicateAgent
|
||||
- Wizard
|
||||
- Xenoborg
|
||||
- GenpopEnter
|
||||
- GenpopLeave
|
||||
privilegedIdSlot:
|
||||
name: id-card-console-privileged-id
|
||||
ejectSound: /Audio/Machines/id_swipe.ogg
|
||||
|
||||
@@ -75,3 +75,21 @@
|
||||
- type: Tag
|
||||
tags:
|
||||
- HideContextMenu
|
||||
|
||||
# Genpop
|
||||
|
||||
- type: entity
|
||||
id: TurnstileGenpopEnter
|
||||
parent: Turnstile
|
||||
suffix: Genpop Enter
|
||||
components:
|
||||
- type: AccessReader
|
||||
access: [["GenpopEnter"]]
|
||||
|
||||
- type: entity
|
||||
id: TurnstileGenpopLeave
|
||||
parent: Turnstile
|
||||
suffix: Genpop Leave
|
||||
components:
|
||||
- type: AccessReader
|
||||
access: [["GenpopLeave"]]
|
||||
|
||||
@@ -381,6 +381,111 @@
|
||||
- type: AccessReader
|
||||
access: [["Armory"]]
|
||||
|
||||
# Genpop Storage
|
||||
- type: entity
|
||||
id: LockerPrisoner
|
||||
parent: LockerBaseSecure
|
||||
name: prisoner closet
|
||||
description: It's a secure locker for an inmate's personal belongings during their time in prison.
|
||||
suffix: 1
|
||||
components:
|
||||
- type: GenpopLocker
|
||||
- type: EntityStorageVisuals
|
||||
stateBaseClosed: genpop
|
||||
stateDoorOpen: genpop_open
|
||||
stateDoorClosed: genpop_door_1
|
||||
- type: UserInterface
|
||||
interfaces:
|
||||
enum.GenpopLockerUiKey.Key:
|
||||
type: GenpopLockerBoundUserInterface
|
||||
- type: AccessReader # note! this access is for the UI, not the door. door access is handled on GenpopLocker
|
||||
access: [["Security"]]
|
||||
- type: Lock
|
||||
locked: false
|
||||
useAccess: false
|
||||
- type: Fixtures
|
||||
fixtures:
|
||||
fix1:
|
||||
shape: !type:PolygonShape
|
||||
radius: 0.01
|
||||
vertices:
|
||||
- -0.25,-0.48
|
||||
- 0.25,-0.48
|
||||
- 0.25,0.48
|
||||
- -0.25,0.48
|
||||
mask:
|
||||
- Impassable
|
||||
- TableLayer
|
||||
- LowImpassable
|
||||
layer:
|
||||
- BulletImpassable
|
||||
- Opaque
|
||||
density: 75
|
||||
hard: True
|
||||
restitution: 0
|
||||
friction: 0.4
|
||||
- type: EntityStorage
|
||||
open: True
|
||||
removedMasks: 20
|
||||
- type: PlaceableSurface
|
||||
isPlaceable: True
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner2
|
||||
parent: LockerPrisoner
|
||||
suffix: 2
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_2
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner3
|
||||
parent: LockerPrisoner
|
||||
suffix: 3
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_3
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner4
|
||||
parent: LockerPrisoner
|
||||
suffix: 4
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_4
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner5
|
||||
parent: LockerPrisoner
|
||||
suffix: 5
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_5
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner6
|
||||
parent: LockerPrisoner
|
||||
suffix: 6
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_6
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner7
|
||||
parent: LockerPrisoner
|
||||
suffix: 7
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_7
|
||||
|
||||
- type: entity
|
||||
id: LockerPrisoner8
|
||||
parent: LockerPrisoner
|
||||
suffix: 8
|
||||
components:
|
||||
- type: EntityStorageVisuals
|
||||
stateDoorClosed: genpop_door_8
|
||||
|
||||
# Detective
|
||||
- type: entity
|
||||
id: LockerDetective
|
||||
|
||||
@@ -693,6 +693,15 @@
|
||||
- type: Sprite
|
||||
state: nosmoking
|
||||
|
||||
- type: entity
|
||||
parent: BaseSign
|
||||
id: SignGenpop
|
||||
name: genpop sign
|
||||
description: A sign indicating the genpop prison.
|
||||
components:
|
||||
- type: Sprite
|
||||
state: genpop
|
||||
|
||||
- type: entity
|
||||
parent: BaseSign
|
||||
id: SignPrison
|
||||
|
||||
BIN
Resources/Textures/Structures/Storage/closet.rsi/genpop.png
Normal file
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 391 B |
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 378 B |
|
After Width: | Height: | Size: 391 B |
|
After Width: | Height: | Size: 390 B |
|
After Width: | Height: | Size: 380 B |
|
After Width: | Height: | Size: 385 B |
BIN
Resources/Textures/Structures/Storage/closet.rsi/genpop_open.png
Normal file
|
After Width: | Height: | Size: 303 B |
BIN
Resources/Textures/Structures/Wallmounts/signs.rsi/genpop.png
Normal file
|
After Width: | Height: | Size: 390 B |
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from https://github.com/discordia-space/CEV-Eris at commit 4e0bbe682d0a00192d24708fdb7031008aa03f18 and bee station at commit https://github.com/BeeStation/BeeStation-Hornet/commit/13dd5ac712385642574138f6d7b30eea7c2fab9c, Job signs by EmoGarbage404 (github) with inspiration from yogstation and tgstation, 'direction_exam' and 'direction_icu' made by rosieposieeee (github), 'direction_atmos' made by SlamBamActionman, 'vox' based on sprites taken from vgstation13 at https://github.com/vgstation-coders/vgstation13/blob/e7f005f8b8d3f7d89cbee3b87f76c23f9e951c27/icons/obj/decals.dmi, 'direction_pods' derived by WarPigeon from existing directional signs.",
|
||||
"version": 1,
|
||||
"size": {
|
||||
"x": 32,
|
||||
"y": 32
|
||||
},
|
||||
"license": "CC-BY-SA-3.0",
|
||||
"copyright": "Taken from https://github.com/discordia-space/CEV-Eris at commit 4e0bbe682d0a00192d24708fdb7031008aa03f18 and bee station at commit https://github.com/BeeStation/BeeStation-Hornet/commit/13dd5ac712385642574138f6d7b30eea7c2fab9c, Job signs by EmoGarbage404 (github) with inspiration from yogstation and tgstation, 'direction_exam' and 'direction_icu' made by rosieposieeee (github), 'direction_atmos' made by SlamBamActionman, 'vox' based on sprites taken from vgstation13 at https://github.com/vgstation-coders/vgstation13/blob/e7f005f8b8d3f7d89cbee3b87f76c23f9e951c27/icons/obj/decals.dmi, 'direction_pods' derived by WarPigeon from existing directional signs.",
|
||||
"states": [
|
||||
{
|
||||
"name": "ai"
|
||||
@@ -258,6 +258,9 @@
|
||||
{
|
||||
"name": "cloning"
|
||||
},
|
||||
{
|
||||
"name": "genpop"
|
||||
},
|
||||
{
|
||||
"name": "gravi"
|
||||
},
|
||||
|
||||