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>
This commit is contained in:
Nemanja
2025-04-24 10:32:11 -04:00
committed by GitHub
parent 896f73c59d
commit dc9844edd1
37 changed files with 1403 additions and 580 deletions

View File

@@ -0,0 +1,9 @@
using Content.Shared.Security.Systems;
namespace Content.Client.Security;
/// <inheritdoc/>
public sealed class GenpopSystem : SharedGenpopSystem
{
}

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

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

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

View File

@@ -1,5 +1,6 @@
using System.Linq; using System.Linq;
using Content.Server.Administration.Logs; using Content.Server.Administration.Logs;
using Content.Server.Chat.Systems;
using Content.Server.Kitchen.Components; using Content.Server.Kitchen.Components;
using Content.Server.Popups; using Content.Server.Popups;
using Content.Shared.Access; using Content.Shared.Access;
@@ -19,6 +20,7 @@ public sealed class IdCardSystem : SharedIdCardSystem
[Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly IRobustRandom _random = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!; [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IAdminLogManager _adminLogger = default!; [Dependency] private readonly IAdminLogManager _adminLogger = default!;
[Dependency] private readonly ChatSystem _chat = default!;
[Dependency] private readonly MicrowaveSystem _microwave = default!; [Dependency] private readonly MicrowaveSystem _microwave = default!;
public override void Initialize() 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);
}
}
} }

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

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

View File

@@ -9,12 +9,15 @@ using Content.Shared.PDA;
using Content.Shared.Roles; using Content.Shared.Roles;
using Content.Shared.StatusIcon; using Content.Shared.StatusIcon;
using Robust.Shared.Prototypes; using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared.Access.Systems; namespace Content.Shared.Access.Systems;
public abstract class SharedIdCardSystem : EntitySystem public abstract class SharedIdCardSystem : EntitySystem
{ {
[Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [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 InventorySystem _inventorySystem = default!;
[Dependency] private readonly MetaDataSystem _metaSystem = default!; [Dependency] private readonly MetaDataSystem _metaSystem = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = 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)})" return $"{idCardComponent.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(idCardComponent.LocalizedJobTitle ?? string.Empty)})"
.Trim(); .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));
}
}
} }

View File

@@ -1,3 +1,4 @@
using Content.Shared.Access.Components;
using Content.Shared.DoAfter; using Content.Shared.DoAfter;
using Robust.Shared.Audio; using Robust.Shared.Audio;
using Robust.Shared.GameStates; using Robust.Shared.GameStates;
@@ -33,6 +34,12 @@ public sealed partial class LockComponent : Component
[DataField, AutoNetworkedField] [DataField, AutoNetworkedField]
public bool UnlockOnClick = true; public bool UnlockOnClick = true;
/// <summary>
/// Whether the lock requires access validation through <see cref="AccessReaderComponent"/>
/// </summary>
[DataField, AutoNetworkedField]
public bool UseAccess = true;
/// <summary> /// <summary>
/// The sound played when unlocked. /// The sound played when unlocked.
/// </summary> /// </summary>

View File

@@ -118,7 +118,7 @@ public sealed class LockSystem : EntitySystem
if (!CanToggleLock(uid, user, quiet: false)) if (!CanToggleLock(uid, user, quiet: false))
return false; return false;
if (!HasUserAccess(uid, user, quiet: false)) if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
return false; return false;
if (!skipDoAfter && lockComp.LockTime != TimeSpan.Zero) if (!skipDoAfter && lockComp.LockTime != TimeSpan.Zero)
@@ -145,6 +145,9 @@ public sealed class LockSystem : EntitySystem
if (!Resolve(uid, ref lockComp)) if (!Resolve(uid, ref lockComp))
return; return;
if (lockComp.Locked)
return;
if (user is { Valid: true }) if (user is { Valid: true })
{ {
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-lock-success", _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-lock-success",
@@ -175,6 +178,9 @@ public sealed class LockSystem : EntitySystem
if (!Resolve(uid, ref lockComp)) if (!Resolve(uid, ref lockComp))
return; return;
if (!lockComp.Locked)
return;
if (user is { Valid: true }) if (user is { Valid: true })
{ {
_sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-unlock-success", _sharedPopupSystem.PopupClient(Loc.GetString("lock-comp-do-unlock-success",
@@ -211,7 +217,7 @@ public sealed class LockSystem : EntitySystem
if (!CanToggleLock(uid, user, quiet: false)) if (!CanToggleLock(uid, user, quiet: false))
return false; return false;
if (!HasUserAccess(uid, user, quiet: false)) if (lockComp.UseAccess && !HasUserAccess(uid, user, quiet: false))
return false; return false;
if (!skipDoAfter && lockComp.UnlockTime != TimeSpan.Zero) if (!skipDoAfter && lockComp.UnlockTime != TimeSpan.Zero)

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

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

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

View File

@@ -159,7 +159,7 @@ public readonly record struct StorageBeforeOpenEvent;
public readonly record struct StorageAfterOpenEvent; public readonly record struct StorageAfterOpenEvent;
[ByRefEvent] [ByRefEvent]
public record struct StorageCloseAttemptEvent(bool Cancelled = false); public record struct StorageCloseAttemptEvent(EntityUid? User, bool Cancelled = false);
[ByRefEvent] [ByRefEvent]
public readonly record struct StorageBeforeCloseEvent(HashSet<EntityUid> Contents, HashSet<EntityUid> BypassChecks); public readonly record struct StorageBeforeCloseEvent(HashSet<EntityUid> Contents, HashSet<EntityUid> BypassChecks);

View File

@@ -181,7 +181,7 @@ public abstract class SharedEntityStorageSystem : EntitySystem
if (component.Open) if (component.Open)
{ {
TryCloseStorage(target); TryCloseStorage(target, user);
} }
else else
{ {
@@ -360,9 +360,9 @@ public abstract class SharedEntityStorageSystem : EntitySystem
return true; return true;
} }
public bool TryCloseStorage(EntityUid target) public bool TryCloseStorage(EntityUid target, EntityUid? user = null)
{ {
if (!CanClose(target)) if (!CanClose(target, user))
{ {
return false; return false;
} }
@@ -413,9 +413,9 @@ public abstract class SharedEntityStorageSystem : EntitySystem
return !ev.Cancelled; 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); RaiseLocalEvent(target, ref ev, silent);
return !ev.Cancelled; return !ev.Cancelled;

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

View File

@@ -9,6 +9,8 @@ id-card-access-level-security = Security
id-card-access-level-armory = Armory id-card-access-level-armory = Armory
id-card-access-level-brig = Brig id-card-access-level-brig = Brig
id-card-access-level-detective = Detective 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-chief-engineer = Chief Engineer
id-card-access-level-engineering = Engineering id-card-access-level-engineering = Engineering

View File

@@ -32,3 +32,5 @@
- Chapel - Chapel
- Hydroponics - Hydroponics
- Atmospherics - Atmospherics
- GenpopEnter
- GenpopLeave

View File

@@ -18,6 +18,14 @@
id: Detective id: Detective
name: id-card-access-level-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 - type: accessGroup
id: Security id: Security
tags: tags:

View File

@@ -722,6 +722,40 @@
- type: PresetIdCard - type: PresetIdCard
job: Detective 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 - type: entity
parent: CentcomIDCard parent: CentcomIDCard
id: CBURNIDcard id: CBURNIDcard

View File

@@ -126,6 +126,8 @@
- SyndicateAgent - SyndicateAgent
- Wizard - Wizard
- Xenoborg - Xenoborg
- GenpopEnter
- GenpopLeave
privilegedIdSlot: privilegedIdSlot:
name: id-card-console-privileged-id name: id-card-console-privileged-id
ejectSound: /Audio/Machines/id_swipe.ogg ejectSound: /Audio/Machines/id_swipe.ogg

View File

@@ -75,3 +75,21 @@
- type: Tag - type: Tag
tags: tags:
- HideContextMenu - 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"]]

View File

@@ -381,6 +381,111 @@
- type: AccessReader - type: AccessReader
access: [["Armory"]] 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 # Detective
- type: entity - type: entity
id: LockerDetective id: LockerDetective

View File

@@ -693,6 +693,15 @@
- type: Sprite - type: Sprite
state: nosmoking 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 - type: entity
parent: BaseSign parent: BaseSign
id: SignPrison id: SignPrison

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 B

View File

@@ -1,11 +1,11 @@
{ {
"version": 1, "version": 1,
"size": { "size": {
"x": 32, "x": 32,
"y": 32 "y": 32
}, },
"license": "CC-BY-SA-3.0", "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.", "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": [ "states": [
{ {
"name": "ai" "name": "ai"
@@ -258,6 +258,9 @@
{ {
"name": "cloning" "name": "cloning"
}, },
{
"name": "genpop"
},
{ {
"name": "gravi" "name": "gravi"
}, },