diff --git a/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
new file mode 100644
index 0000000000..bb0f870f5a
--- /dev/null
+++ b/Content.Client/Access/UI/AgentIDCardBoundUserInterface.cs
@@ -0,0 +1,64 @@
+using Content.Shared.Access.Systems;
+using Robust.Client.GameObjects;
+
+namespace Content.Client.Access.UI
+{
+ ///
+ /// Initializes a and updates it when new server messages are received.
+ ///
+ public sealed class AgentIDCardBoundUserInterface : BoundUserInterface
+ {
+ private AgentIDCardWindow? _window;
+
+ public AgentIDCardBoundUserInterface(ClientUserInterfaceComponent owner, object uiKey) : base(owner, uiKey)
+ {
+ }
+
+ protected override void Open()
+ {
+ base.Open();
+
+ _window = new AgentIDCardWindow();
+ if (State != null)
+ UpdateState(State);
+
+ _window.OpenCentered();
+
+ _window.OnClose += Close;
+ _window.OnNameEntered += OnNameChanged;
+ _window.OnJobEntered += OnJobChanged;
+ }
+
+ private void OnNameChanged(string newName)
+ {
+ SendMessage(new AgentIDCardNameChangedMessage(newName));
+ }
+
+ private void OnJobChanged(string newJob)
+ {
+ SendMessage(new AgentIDCardJobChangedMessage(newJob));
+ }
+
+ ///
+ /// Update the UI state based on server-sent info
+ ///
+ ///
+ protected override void UpdateState(BoundUserInterfaceState state)
+ {
+ base.UpdateState(state);
+ if (_window == null || state is not AgentIDCardBoundUserInterfaceState cast)
+ return;
+
+ _window.SetCurrentName(cast.CurrentName);
+ _window.SetCurrentJob(cast.CurrentJob);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+ if (!disposing) return;
+ _window?.Dispose();
+ }
+ }
+
+}
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml b/Content.Client/Access/UI/AgentIDCardWindow.xaml
new file mode 100644
index 0000000000..22bb3cb275
--- /dev/null
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
diff --git a/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
new file mode 100644
index 0000000000..9813a2946e
--- /dev/null
+++ b/Content.Client/Access/UI/AgentIDCardWindow.xaml.cs
@@ -0,0 +1,32 @@
+using Robust.Client.UserInterface.CustomControls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Access.UI
+{
+ [GenerateTypedNameReferences]
+ public sealed partial class AgentIDCardWindow : DefaultWindow
+ {
+ public event Action? OnNameEntered;
+
+ public event Action? OnJobEntered;
+
+ public AgentIDCardWindow()
+ {
+ RobustXamlLoader.Load(this);
+
+ NameLineEdit.OnTextEntered += e => OnNameEntered?.Invoke(e.Text);
+ JobLineEdit.OnTextEntered += e => OnJobEntered?.Invoke(e.Text);
+ }
+
+ public void SetCurrentName(string name)
+ {
+ NameLineEdit.Text = name;
+ }
+
+ public void SetCurrentJob(string job)
+ {
+ JobLineEdit.Text = job;
+ }
+ }
+}
diff --git a/Content.Client/Entry/IgnoredComponents.cs b/Content.Client/Entry/IgnoredComponents.cs
index 10778032cc..ec340c623e 100644
--- a/Content.Client/Entry/IgnoredComponents.cs
+++ b/Content.Client/Entry/IgnoredComponents.cs
@@ -64,6 +64,7 @@ namespace Content.Client.Entry
"DiseaseSwab",
"FloorTile",
"RandomInsulation",
+ "AgentIDCard",
"Electrified",
"Electrocution",
"Paper",
diff --git a/Content.Server/Access/Components/AgentIDCardComponent.cs b/Content.Server/Access/Components/AgentIDCardComponent.cs
new file mode 100644
index 0000000000..cedb5e49d9
--- /dev/null
+++ b/Content.Server/Access/Components/AgentIDCardComponent.cs
@@ -0,0 +1,9 @@
+using Content.Shared.Access.Systems;
+using Content.Shared.PDA;
+
+namespace Content.Server.Access.Components
+{
+ [RegisterComponent]
+ public sealed class AgentIDCardComponent : Component
+ {}
+}
diff --git a/Content.Server/Access/Systems/AgentIDCardSystem.cs b/Content.Server/Access/Systems/AgentIDCardSystem.cs
new file mode 100644
index 0000000000..e083f820d4
--- /dev/null
+++ b/Content.Server/Access/Systems/AgentIDCardSystem.cs
@@ -0,0 +1,66 @@
+using Content.Shared.Access.Components;
+using Content.Server.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Interaction;
+using Content.Server.Popups;
+using Robust.Shared.Player;
+
+namespace Content.Server.Access.Systems
+{
+ public sealed class AgentIDCardSystem : SharedAgentIdCardSystem
+ {
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly IdCardSystem _cardSystem = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+ SubscribeLocalEvent(OnAfterInteract);
+ // BUI
+ SubscribeLocalEvent(OnNameChanged);
+ SubscribeLocalEvent (OnJobChanged);
+
+ }
+
+ private void OnAfterInteract(EntityUid uid, AgentIDCardComponent component, AfterInteractEvent args)
+ {
+ if (!TryComp(args.Target, out var targetAccess) || !TryComp(uid, out var targetIDCard) || args.Target == null)
+ return;
+
+ if (!TryComp(uid, out var access) || !TryComp(uid, out var idCard))
+ return;
+
+ var beforeLength = access.Tags.Count;
+ access.Tags.UnionWith(targetAccess.Tags);
+ var addedLength = access.Tags.Count - beforeLength;
+
+ if (addedLength == 0)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("agent-id-no-new", ("card", args.Target)), args.Target.Value, Filter.Pvs(args.User));
+ return;
+ }
+ else if (addedLength == 1)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("agent-id-new-1", ("card", args.Target)), args.Target.Value, Filter.Pvs(args.User));
+ return;
+ }
+ _popupSystem.PopupEntity(Loc.GetString("agent-id-new", ("number", addedLength), ("card", args.Target)), args.Target.Value, Filter.Pvs(args.User));
+ }
+
+ private void OnJobChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardJobChangedMessage args)
+ {
+ if (!TryComp(uid, out var idCard))
+ return;
+
+ _cardSystem.TryChangeJobTitle(uid, args.Job, idCard);
+ }
+
+ private void OnNameChanged(EntityUid uid, AgentIDCardComponent comp, AgentIDCardNameChangedMessage args)
+ {
+ if (!TryComp(uid, out var idCard))
+ return;
+
+ _cardSystem.TryChangeFullName(uid, args.Name, idCard);
+ }
+ }
+}
diff --git a/Content.Server/Bible/BibleSystem.cs b/Content.Server/Bible/BibleSystem.cs
index 877057f111..26f94c67e5 100644
--- a/Content.Server/Bible/BibleSystem.cs
+++ b/Content.Server/Bible/BibleSystem.cs
@@ -1,20 +1,13 @@
-using System;
-using Robust.Shared.GameObjects;
using Content.Shared.Interaction;
using Content.Shared.Inventory;
using Content.Shared.MobState.Components;
using Content.Shared.Damage;
-using Content.Shared.Popups;
using Content.Server.Cooldown;
-using Content.Server.Inventory;
-using Content.Server.Mind.Components;
using Content.Server.Bible.Components;
using Content.Server.Popups;
-using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Audio;
using Robust.Shared.Player;
-using Robust.Shared.Localization;
using Robust.Shared.Timing;
@@ -70,7 +63,7 @@ namespace Content.Server.Bible
if (_random.Prob(component.FailChance))
{
var othersFailMessage = Loc.GetString("bible-heal-fail-others", ("user", args.User),("target", args.Target),("bible", uid));
- _popupSystem.PopupEntity(othersFailMessage, args.User, Filter.Pvs(args.User).RemoveWhereAttachedEntity(puid => puid == args.User));
+ _popupSystem.PopupEntity(othersFailMessage, args.User, Filter.Pvs(args.User).RemoveWhereAttachedEntity(puid => puid == args.User));
var selfFailMessage = Loc.GetString("bible-heal-fail-self", ("target", args.Target),("bible", uid));
_popupSystem.PopupEntity(selfFailMessage, args.User, Filter.Entities(args.User));
diff --git a/Content.Shared/Access/Components/IdCardComponent.cs b/Content.Shared/Access/Components/IdCardComponent.cs
index ccd2ef47e3..c114e0dba4 100644
--- a/Content.Shared/Access/Components/IdCardComponent.cs
+++ b/Content.Shared/Access/Components/IdCardComponent.cs
@@ -1,15 +1,12 @@
using Content.Shared.Access.Systems;
using Content.Shared.PDA;
-using Robust.Shared.Analyzers;
-using Robust.Shared.GameObjects;
-using Robust.Shared.Serialization.Manager.Attributes;
namespace Content.Shared.Access.Components
{
// TODO BUI NETWORKING if ever clients can open their own BUI's (id card console, pda), then this data should be
// networked.
[RegisterComponent]
- [Friend(typeof(SharedIdCardSystem), typeof(SharedPDASystem))]
+ [Friend(typeof(SharedIdCardSystem), typeof(SharedPDASystem), typeof(SharedAgentIdCardSystem))]
public sealed class IdCardComponent : Component
{
[DataField("originalOwnerName")]
diff --git a/Content.Shared/Access/SharedAgentIDCardSystem.cs b/Content.Shared/Access/SharedAgentIDCardSystem.cs
new file mode 100644
index 0000000000..bb4a1fd1d1
--- /dev/null
+++ b/Content.Shared/Access/SharedAgentIDCardSystem.cs
@@ -0,0 +1,55 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Access.Systems
+{
+ public class SharedAgentIdCardSystem : EntitySystem
+ {
+ /// Just for friending for now
+ }
+ ///
+ /// Key representing which is currently open.
+ /// Useful when there are multiple UI for an object. Here it's future-proofing only.
+ ///
+ [Serializable, NetSerializable]
+ public enum AgentIDCardUiKey
+ {
+ Key,
+ }
+
+ ///
+ /// Represents an state that can be sent to the client
+ ///
+ [Serializable, NetSerializable]
+ public sealed class AgentIDCardBoundUserInterfaceState : BoundUserInterfaceState
+ {
+ public string CurrentName { get; }
+ public string CurrentJob { get; }
+
+ public AgentIDCardBoundUserInterfaceState(string currentName, string currentJob)
+ {
+ CurrentName = currentName;
+ CurrentJob = currentJob;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class AgentIDCardNameChangedMessage : BoundUserInterfaceMessage
+ {
+ public string Name { get; }
+
+ public AgentIDCardNameChangedMessage(string name)
+ {
+ Name = name;
+ }
+ }
+
+ [Serializable, NetSerializable]
+ public sealed class AgentIDCardJobChangedMessage : BoundUserInterfaceMessage
+ {
+ public string Job { get; }
+ public AgentIDCardJobChangedMessage(string job)
+ {
+ Job = job;
+ }
+ }
+}
diff --git a/Resources/Locale/en-US/access/components/agent-id-card-component.ftl b/Resources/Locale/en-US/access/components/agent-id-card-component.ftl
new file mode 100644
index 0000000000..2628813db5
--- /dev/null
+++ b/Resources/Locale/en-US/access/components/agent-id-card-component.ftl
@@ -0,0 +1,6 @@
+agent-id-no-new = Didn't gain any new accesses from {THE($card)}.
+agent-id-new-1 = Gained one new access from {THE($card)}.
+agent-id-new = Gained {$number} new accesses from {THE($card)}.
+agent-id-card-current-name = Name:
+agent-id-card-current-job = Job:
+agent-id-menu-title = Agent ID Card
diff --git a/Resources/Prototypes/Catalog/uplink_catalog.yml b/Resources/Prototypes/Catalog/uplink_catalog.yml
index fd24c3a990..b12a613053 100644
--- a/Resources/Prototypes/Catalog/uplink_catalog.yml
+++ b/Resources/Prototypes/Catalog/uplink_catalog.yml
@@ -157,6 +157,15 @@
icon: /Textures/Objects/Tools/emag.rsi/icon.png
price: 8
+- type: uplinkListing
+ id: UplinkAgentIDCard
+ category: Utility
+ itemId: AgentIDCard
+ listingName: Agent ID Card
+ description: A modified ID card that can copy accesses from other cards and change its name and job title at-will.
+ icon: Objects/Misc/id_cards.rsi/default.png
+ price: 3
+
- type: uplinkListing
id: UplinkHypopen
category: Utility
diff --git a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
index ae7821399f..84a1f423dc 100644
--- a/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
+++ b/Resources/Prototypes/Entities/Objects/Misc/identification_cards.yml
@@ -377,3 +377,16 @@
- state: idmusician
- type: PresetIdCard
job: Musician
+
+- type: entity
+ parent: AssistantIDCard
+ id: AgentIDCard
+ suffix: Agent
+ components:
+ - type: AgentIDCard
+ - type: ActivatableUI
+ key: enum.AgentIDCardUiKey.Key
+ - type: UserInterface
+ interfaces:
+ - key: enum.AgentIDCardUiKey.Key
+ type: AgentIDCardBoundUserInterface