diff --git a/Content.Server/DeltaV/Paper/SignAttemptEvent.cs b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs
new file mode 100644
index 00000000000..f606e250c94
--- /dev/null
+++ b/Content.Server/DeltaV/Paper/SignAttemptEvent.cs
@@ -0,0 +1,8 @@
+namespace Content.Server.DeltaV.Paper;
+
+///
+/// Raised on the pen when trying to sign a paper.
+/// If it's cancelled the signature isn't made.
+///
+[ByRefEvent]
+public record struct SignAttemptEvent(EntityUid Paper, EntityUid User, bool Cancelled = false);
diff --git a/Content.Server/DeltaV/Paper/SignatureSystem.cs b/Content.Server/DeltaV/Paper/SignatureSystem.cs
new file mode 100644
index 00000000000..34b8bf35637
--- /dev/null
+++ b/Content.Server/DeltaV/Paper/SignatureSystem.cs
@@ -0,0 +1,106 @@
+using Content.Server.Access.Systems;
+using Content.Server.Paper;
+using Content.Server.Popups;
+using Content.Shared.Paper;
+using Content.Shared.Popups;
+using Content.Shared.Tag;
+using Content.Shared.Verbs;
+using Robust.Server.Audio;
+using Robust.Shared.Player;
+
+namespace Content.Server.DeltaV.Paper;
+
+public sealed class SignatureSystem : EntitySystem
+{
+ [Dependency] private readonly AudioSystem _audio = default!;
+ [Dependency] private readonly IdCardSystem _idCard = default!;
+ [Dependency] private readonly PaperSystem _paper = default!;
+ [Dependency] private readonly PopupSystem _popup = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+
+ // The sprite used to visualize "signatures" on paper entities.
+ private const string SignatureStampState = "paper_stamp-signature";
+
+ public override void Initialize()
+ {
+ SubscribeLocalEvent>(OnGetAltVerbs);
+ }
+
+ private void OnGetAltVerbs(Entity ent, ref GetVerbsEvent args)
+ {
+ if (!args.CanAccess || !args.CanInteract)
+ return;
+
+ if (args.Using is not {} pen || !_tagSystem.HasTag(pen, "Write"))
+ return;
+
+ var user = args.User;
+ AlternativeVerb verb = new()
+ {
+ Act = () =>
+ {
+ TrySignPaper(ent, user, pen);
+ },
+ Text = Loc.GetString("paper-sign-verb"),
+ DoContactInteraction = true,
+ Priority = 10
+ };
+ args.Verbs.Add(verb);
+ }
+
+ ///
+ /// Tries add add a signature to the paper with signer's name.
+ ///
+ public bool TrySignPaper(Entity paper, EntityUid signer, EntityUid pen)
+ {
+ var comp = paper.Comp;
+
+ var ev = new SignAttemptEvent(paper, signer);
+ RaiseLocalEvent(pen, ref ev);
+ if (ev.Cancelled)
+ return false;
+
+ var signatureName = DetermineEntitySignature(signer);
+
+ var stampInfo = new StampDisplayInfo()
+ {
+ StampedName = signatureName,
+ StampedColor = Color.DarkSlateGray, // TODO: make configurable? Perhaps it should depend on the pen.
+ };
+
+ if (!comp.StampedBy.Contains(stampInfo) && _paper.TryStamp(paper, stampInfo, SignatureStampState, comp))
+ {
+ // Show popups and play a paper writing sound
+ var signedOtherMessage = Loc.GetString("paper-signed-other", ("user", signer), ("target", paper.Owner));
+ _popup.PopupEntity(signedOtherMessage, signer, Filter.PvsExcept(signer, entityManager: EntityManager), true);
+
+ var signedSelfMessage = Loc.GetString("paper-signed-self", ("target", paper.Owner));
+ _popup.PopupEntity(signedSelfMessage, signer, signer);
+
+ _audio.PlayPvs(comp.Sound, signer);
+
+ _paper.UpdateUserInterface(paper, comp);
+
+ return true;
+ }
+ else
+ {
+ // Show an error popup
+ _popup.PopupEntity(Loc.GetString("paper-signed-failure", ("target", paper.Owner)), signer, signer, PopupType.SmallCaution);
+
+ return false;
+ }
+ }
+
+ private string DetermineEntitySignature(EntityUid uid)
+ {
+ // If the entity has an ID, use the name on it.
+ if (_idCard.TryFindIdCard(uid, out var id) && !string.IsNullOrWhiteSpace(id.Comp.FullName))
+ {
+ return id.Comp.FullName;
+ }
+
+ // Alternatively, return the entity name
+ return Name(uid);
+ }
+}
diff --git a/Resources/Locale/en-US/deltav/paper/signature.ftl b/Resources/Locale/en-US/deltav/paper/signature.ftl
new file mode 100644
index 00000000000..87741c962c0
--- /dev/null
+++ b/Resources/Locale/en-US/deltav/paper/signature.ftl
@@ -0,0 +1,5 @@
+paper-sign-verb = Sign
+
+paper-signed-other = {CAPITALIZE(THE($user))} signs {THE($target)}.
+paper-signed-self = You sign {THE($target)}.
+paper-signed-failure = You cannot sign {THE($target)}
diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
index 5117df77356..b57f9844bc7 100644
--- a/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
+++ b/Resources/Textures/Objects/Misc/bureaucracy.rsi/meta.json
@@ -1,7 +1,7 @@
{
"version": 1,
"license": "CC-BY-SA-3.0",
- "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name",
+ "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/e1142f20f5e4661cb6845cfcf2dd69f864d67432. paper_stamp-syndicate by Veritius. paper_receipt, paper_receipt_horizontal by eoineoineoin. pen_centcom is a resprited version of pen_cap by PuroSlavKing (Github). Luxury pen is drawn by Ubaser. Lawyer and psychologist paper stamp resprited by Guess-My-Name. paper_stamp-signature by Mnemotechnician.",
"size": {
"x": 32,
"y": 32
@@ -259,6 +259,9 @@
},
{
"name": "paper_stamp-psychologist"
+ },
+ {
+ "name": "paper_stamp-signature"
}
]
}
diff --git a/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png
new file mode 100644
index 00000000000..6a7aa083ee5
Binary files /dev/null and b/Resources/Textures/Objects/Misc/bureaucracy.rsi/paper_stamp-signature.png differ