From 3e4a62271ab821c0e48dc18d8d47e85af1555df0 Mon Sep 17 00:00:00 2001
From: Debug <49997488+DebugOk@users.noreply.github.com>
Date: Thu, 14 Sep 2023 21:52:16 +0200
Subject: [PATCH] Add Mail!!111!!
---
.../Nyanotrasen/Mail/MailComponent.cs | 8 +
Content.Client/Nyanotrasen/Mail/MailSystem.cs | 60 ++
.../Mail/Components/MailComponent.cs | 108 +++
.../Mail/Components/MailReceiverComponent.cs | 6 +
.../Components/MailTeleporterComponent.cs | 108 +++
.../Components/StationMailRouterComponent.cs | 9 +
.../Nyanotrasen/Mail/MailCommands.cs | 137 ++++
Content.Server/Nyanotrasen/Mail/MailSystem.cs | 741 +++++++++++++++++
.../Mail/MailDeliveryPoolPrototype.cs | 30 +
.../Nyanotrasen/Mail/MailVisuals.cs | 19 +
.../Nyanotrasen/Mail/SharedMailComponent.cs | 4 +
Resources/Locale/en-US/Mail/mail.ftl | 30 +
.../Entities/Stations/nanotrasen.yml | 1 +
.../Objects/Specific/Mail/base_mail.yml | 107 +++
.../Entities/Objects/Specific/Mail/mail.yml | 744 ++++++++++++++++++
.../Objects/Specific/Mail/mail_civilian.yml | 195 +++++
.../Objects/Specific/Mail/mail_command.yml | 9 +
.../Specific/Mail/mail_engineering.yml | 45 ++
.../Specific/Mail/mail_epistemology.yml | 58 ++
.../Objects/Specific/Mail/mail_medical.yml | 92 +++
.../Objects/Specific/Mail/mail_security.yml | 55 ++
.../Specific/Mail/mail_specific_items.yml | 169 ++++
.../Nyanotrasen/Entities/Stations/mail.yml | 5 +
.../Structures/Machines/mailTeleporter.yml | 58 ++
.../Prototypes/Nyanotrasen/mailDeliveries.yml | 118 +++
.../Objects/Specific/Mail/mail.rsi/broken.png | Bin 0 -> 199 bytes
.../Specific/Mail/mail.rsi/fragile.png | Bin 0 -> 158 bytes
.../Objects/Specific/Mail/mail.rsi/icon.png | Bin 0 -> 277 bytes
.../Objects/Specific/Mail/mail.rsi/locked.png | Bin 0 -> 144 bytes
.../Objects/Specific/Mail/mail.rsi/meta.json | 35 +
.../Specific/Mail/mail.rsi/postmark.png | Bin 0 -> 133 bytes
.../Specific/Mail/mail.rsi/priority.png | Bin 0 -> 147 bytes
.../Mail/mail.rsi/priority_inactive.png | Bin 0 -> 147 bytes
.../Objects/Specific/Mail/mail.rsi/trash.png | Bin 0 -> 305 bytes
.../Structures/mailbox.rsi/icon.png | Bin 0 -> 2212 bytes
.../Structures/mailbox.rsi/meta.json | 17 +
.../Structures/mailbox.rsi/unlit.png | Bin 0 -> 287 bytes
37 files changed, 2968 insertions(+)
create mode 100644 Content.Client/Nyanotrasen/Mail/MailComponent.cs
create mode 100644 Content.Client/Nyanotrasen/Mail/MailSystem.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/Components/MailReceiverComponent.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/Components/MailTeleporterComponent.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/Components/StationMailRouterComponent.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/MailCommands.cs
create mode 100644 Content.Server/Nyanotrasen/Mail/MailSystem.cs
create mode 100644 Content.Shared/Nyanotrasen/Mail/MailDeliveryPoolPrototype.cs
create mode 100644 Content.Shared/Nyanotrasen/Mail/MailVisuals.cs
create mode 100644 Content.Shared/Nyanotrasen/Mail/SharedMailComponent.cs
create mode 100644 Resources/Locale/en-US/Mail/mail.ftl
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_civilian.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_command.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_engineering.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_epistemology.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_medical.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_security.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_specific_items.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Stations/mail.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml
create mode 100644 Resources/Prototypes/Nyanotrasen/mailDeliveries.yml
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/broken.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/fragile.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/icon.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/locked.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/meta.json
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/postmark.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/priority.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/priority_inactive.png
create mode 100644 Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/trash.png
create mode 100644 Resources/Textures/Nyanotrasen/Structures/mailbox.rsi/icon.png
create mode 100644 Resources/Textures/Nyanotrasen/Structures/mailbox.rsi/meta.json
create mode 100644 Resources/Textures/Nyanotrasen/Structures/mailbox.rsi/unlit.png
diff --git a/Content.Client/Nyanotrasen/Mail/MailComponent.cs b/Content.Client/Nyanotrasen/Mail/MailComponent.cs
new file mode 100644
index 00000000000..4f9b6e36892
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Mail/MailComponent.cs
@@ -0,0 +1,8 @@
+using Content.Shared.Mail;
+
+namespace Content.Client.Mail
+{
+ [RegisterComponent]
+ public sealed partial class MailComponent : SharedMailComponent
+ {}
+}
diff --git a/Content.Client/Nyanotrasen/Mail/MailSystem.cs b/Content.Client/Nyanotrasen/Mail/MailSystem.cs
new file mode 100644
index 00000000000..99fda6b0a7d
--- /dev/null
+++ b/Content.Client/Nyanotrasen/Mail/MailSystem.cs
@@ -0,0 +1,60 @@
+using Robust.Client.GameObjects;
+using Content.Shared.Mail;
+using Content.Shared.StatusIcon;
+using Content.Shared.StatusIcon.Components;
+using FastAccessors;
+using Robust.Client.State;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Utility;
+
+namespace Content.Client.Mail
+{
+ ///
+ /// Display a cool stamp on the parcel based on the job of the recipient.
+ ///
+ ///
+ /// GenericVisualizer is not powerful enough to handle setting a string on
+ /// visual data then directly relaying that string to a layer's state.
+ /// I.e. there is nothing like a regex capture group for visual data.
+ ///
+ /// Hence why this system exists.
+ ///
+ /// To do this with GenericVisualizer would require a separate condition
+ /// for every job value, which would be extra mess to maintain.
+ ///
+ /// It would look something like this, multipled a couple dozen times.
+ ///
+ /// enum.MailVisuals.JobIcon:
+ /// enum.MailVisualLayers.JobStamp:
+ /// StationEngineer:
+ /// state: StationEngineer
+ /// SecurityOfficer:
+ /// state: SecurityOfficer
+ ///
+ public sealed class MailJobVisualizerSystem : VisualizerSystem
+ {
+ protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args)
+ {
+ if (args.Sprite == null)
+ return;
+
+ if (args.Component.TryGetData(MailVisuals.JobIcon, out string job))
+ {
+ job = job.Substring(7); // :clueless:
+
+ args.Sprite.LayerSetState(MailVisualLayers.JobStamp, job);
+ }
+
+ }
+ }
+
+ public enum MailVisualLayers : byte
+ {
+ Icon,
+ Lock,
+ FragileStamp,
+ JobStamp,
+ PriorityTape,
+ Breakage,
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs b/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
new file mode 100644
index 00000000000..61d94bbad31
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/Components/MailComponent.cs
@@ -0,0 +1,108 @@
+using System.Threading;
+using Robust.Shared.Audio;
+using Content.Shared.Storage;
+using Content.Shared.Mail;
+
+namespace Content.Server.Mail.Components
+{
+ [RegisterComponent]
+ public sealed partial class MailComponent : SharedMailComponent
+ {
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("recipient")]
+ public string Recipient = "None";
+
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("recipientJob")]
+ public string RecipientJob = "None";
+
+ // Why do we not use LockComponent?
+ // Because this can't be locked again,
+ // and we have special conditions for unlocking,
+ // and we don't want to add a verb.
+ [ViewVariables(VVAccess.ReadWrite)]
+ [DataField("isLocked")]
+ public bool IsLocked = true;
+
+ ///
+ /// Is this parcel profitable to deliver for the station?
+ ///
+ ///
+ /// The station won't receive any award on delivery if this is false.
+ /// This is useful for broken fragile packages and packages that were
+ /// not delivered in time.
+ ///
+ [DataField("isProfitable")]
+ public bool IsProfitable = true;
+
+ ///
+ /// Is this package considered fragile?
+ ///
+ ///
+ /// This can be set to true in the YAML files for a mail delivery to
+ /// always be Fragile, despite its contents.
+ ///
+ [DataField("isFragile")]
+ public bool IsFragile = false;
+
+ ///
+ /// Is this package considered priority mail?
+ ///
+ ///
+ /// There will be a timer set for its successful delivery. The
+ /// station's bank account will be penalized if it is not delivered on
+ /// time.
+ ///
+ /// This is set to false on successful delivery.
+ ///
+ /// This can be set to true in the YAML files for a mail delivery to
+ /// always be Priority.
+ ///
+ [DataField("isPriority")]
+ public bool IsPriority = false;
+
+ ///
+ /// What will be packaged when the mail is spawned.
+ ///
+ [DataField("contents")]
+ public List Contents = new();
+
+ ///
+ /// The amount that cargo will be awarded for delivering this mail.
+ ///
+ [DataField("bounty")]
+ public int Bounty = 750;
+
+ ///
+ /// Penalty if the mail is destroyed.
+ ///
+ [DataField("penalty")]
+ public int Penalty = -250;
+
+ ///
+ /// The sound that's played when the mail's lock is broken.
+ ///
+ [DataField("penaltySound")]
+ public SoundSpecifier PenaltySound = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
+
+ ///
+ /// The sound that's played when the mail's opened.
+ ///
+ [DataField("openSound")]
+ public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/packetrip.ogg");
+
+ ///
+ /// The sound that's played when the mail's lock has been emagged.
+ ///
+ [DataField("emagSound")]
+ public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
+
+ ///
+ /// Whether this component is enabled.
+ /// Removed when it becomes trash.
+ ///
+ public bool IsEnabled = true;
+
+ public CancellationTokenSource? priorityCancelToken;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Mail/Components/MailReceiverComponent.cs b/Content.Server/Nyanotrasen/Mail/Components/MailReceiverComponent.cs
new file mode 100644
index 00000000000..4224de5de45
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/Components/MailReceiverComponent.cs
@@ -0,0 +1,6 @@
+namespace Content.Server.Mail.Components
+{
+ [RegisterComponent]
+ public sealed partial class MailReceiverComponent : Component
+ {}
+}
diff --git a/Content.Server/Nyanotrasen/Mail/Components/MailTeleporterComponent.cs b/Content.Server/Nyanotrasen/Mail/Components/MailTeleporterComponent.cs
new file mode 100644
index 00000000000..bc05d7307d9
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/Components/MailTeleporterComponent.cs
@@ -0,0 +1,108 @@
+using Robust.Shared.Audio;
+
+namespace Content.Server.Mail.Components
+{
+ ///
+ /// This is for the mail teleporter.
+ /// Random mail will be teleported to this every few minutes.
+ ///
+ [RegisterComponent]
+ public sealed partial class MailTeleporterComponent : Component
+ {
+
+ // Not starting accumulator at 0 so mail carriers have some deliveries to make shortly after roundstart.
+ [DataField("accumulator")]
+ public float Accumulator = 285f;
+
+ [DataField("teleportInterval")]
+ public TimeSpan TeleportInterval = TimeSpan.FromMinutes(5);
+
+ ///
+ /// The sound that's played when new mail arrives.
+ ///
+ [DataField("teleportSound")]
+ public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
+
+ ///
+ /// The MailDeliveryPoolPrototype that's used to select what mail this
+ /// teleporter can deliver.
+ ///
+ [DataField("mailPool")]
+ public string MailPool = "RandomMailDeliveryPool";
+
+ ///
+ /// How many mail candidates do we need per actual delivery sent when
+ /// the mail goes out? The number of candidates is divided by this number
+ /// to determine how many deliveries will be teleported in.
+ /// It does not determine unique recipients. That is random.
+ ///
+ [DataField("candidatesPerDelivery")]
+ public int CandidatesPerDelivery = 8;
+
+ [DataField("minimumDeliveriesPerTeleport")]
+ public int MinimumDeliveriesPerTeleport = 1;
+
+ ///
+ /// Do not teleport any more mail in, if there are at least this many
+ /// undelivered parcels.
+ ///
+ ///
+ /// Currently this works by checking how many MailComponent entities
+ /// are sitting on the teleporter's tile.
+ ///
+ /// It should be noted that if the number of actual deliveries to be
+ /// made based on the number of candidates divided by candidates per
+ /// delivery exceeds this number, the teleporter will spawn more mail
+ /// than this number.
+ ///
+ /// This is just a simple check to see if anyone's been picking up the
+ /// mail lately to prevent entity bloat for the sake of performance.
+ ///
+ [DataField("maximumUndeliveredParcels")]
+ public int MaximumUndeliveredParcels = 5;
+
+ ///
+ /// Any item that breaks or is destroyed in less than this amount of
+ /// damage is one of the types of items considered fragile.
+ ///
+ [DataField("fragileDamageThreshold")]
+ public int FragileDamageThreshold = 10;
+
+ ///
+ /// What's the bonus for delivering a fragile package intact?
+ ///
+ [DataField("fragileBonus")]
+ public int FragileBonus = 100;
+
+ ///
+ /// What's the malus for failing to deliver a fragile package?
+ ///
+ [DataField("fragileMalus")]
+ public int FragileMalus = -100;
+
+ ///
+ /// What's the chance for any one delivery to be marked as priority mail?
+ ///
+ [DataField("priorityChance")]
+ public float PriorityChance = 0.1f;
+
+ ///
+ /// How long until a priority delivery is considered as having failed
+ /// if not delivered?
+ ///
+ [DataField("priorityDuration")]
+ public TimeSpan priorityDuration = TimeSpan.FromMinutes(5);
+
+ ///
+ /// What's the bonus for delivering a priority package on time?
+ ///
+ [DataField("priorityBonus")]
+ public int PriorityBonus = 250;
+
+ ///
+ /// What's the malus for failing to deliver a priority package?
+ ///
+ [DataField("priorityMalus")]
+ public int PriorityMalus = -250;
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Mail/Components/StationMailRouterComponent.cs b/Content.Server/Nyanotrasen/Mail/Components/StationMailRouterComponent.cs
new file mode 100644
index 00000000000..ce87eb131fc
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/Components/StationMailRouterComponent.cs
@@ -0,0 +1,9 @@
+namespace Content.Server.Mail;
+
+///
+/// Designates a station as a place for sending and receiving mail.
+///
+[RegisterComponent]
+public sealed partial class StationMailRouterComponent : Component
+{
+}
diff --git a/Content.Server/Nyanotrasen/Mail/MailCommands.cs b/Content.Server/Nyanotrasen/Mail/MailCommands.cs
new file mode 100644
index 00000000000..fba8e4b6145
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/MailCommands.cs
@@ -0,0 +1,137 @@
+using System.Linq;
+using Robust.Shared.Console;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Content.Shared.Administration;
+using Content.Server.Administration;
+using Content.Server.Mail.Components;
+
+namespace Content.Server.Mail;
+
+[AdminCommand(AdminFlags.Fun)]
+public sealed class MailToCommand : IConsoleCommand
+{
+ public string Command => "mailto";
+ public string Description => Loc.GetString("command-mailto-description", ("requiredComponent", nameof(MailReceiverComponent)));
+ public string Help => Loc.GetString("command-mailto-help", ("command", Command));
+
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
+
+ private readonly string _blankMailPrototype = "MailAdminFun";
+ private readonly string _container = "storagebase";
+ private readonly string _mailContainer = "contents";
+
+
+ public async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ if (args.Length < 4)
+ {
+ shell.WriteError(Loc.GetString("shell-wrong-arguments-number"));
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[0], out var recipientUid))
+ {
+ shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
+ return;
+ }
+
+ if (!EntityUid.TryParse(args[1], out var containerUid))
+ {
+ shell.WriteError(Loc.GetString("shell-entity-uid-must-be-number"));
+ return;
+ }
+
+ if (!Boolean.TryParse(args[2], out var isFragile))
+ {
+ shell.WriteError(Loc.GetString("shell-invalid-bool"));
+ return;
+ }
+
+ if (!Boolean.TryParse(args[3], out var isPriority))
+ {
+ shell.WriteError(Loc.GetString("shell-invalid-bool"));
+ return;
+ }
+
+
+ var _mailSystem = _entitySystemManager.GetEntitySystem();
+ var _containerSystem = _entitySystemManager.GetEntitySystem();
+
+ if (!_entityManager.TryGetComponent(recipientUid, out MailReceiverComponent? mailReceiver))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-no-mailreceiver", ("requiredComponent", nameof(MailReceiverComponent))));
+ return;
+ }
+
+ if (!_prototypeManager.HasIndex(_blankMailPrototype))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-no-blankmail", ("blankMail", _blankMailPrototype)));
+ return;
+ }
+
+ if (!_containerSystem.TryGetContainer(containerUid, _container, out var targetContainer))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-invalid-container", ("requiredContainer", _container)));
+ return;
+ }
+
+ if (!_mailSystem.TryGetMailRecipientForReceiver(mailReceiver, out MailRecipient? recipient))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-unable-to-receive"));
+ return;
+ }
+
+ if (!_mailSystem.TryGetMailTeleporterForReceiver(mailReceiver, out MailTeleporterComponent? teleporterComponent))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-no-teleporter-found"));
+ return;
+ }
+
+ var mailUid = _entityManager.SpawnEntity(_blankMailPrototype, _entityManager.GetComponent(containerUid).Coordinates);
+ var mailContents = _containerSystem.EnsureContainer(mailUid, _mailContainer);
+
+ if (!_entityManager.TryGetComponent(mailUid, out MailComponent? mailComponent))
+ {
+ shell.WriteLine(Loc.GetString("command-mailto-bogus-mail", ("blankMail", _blankMailPrototype), ("requiredMailComponent", nameof(MailComponent))));
+ return;
+ }
+
+ foreach (var entity in targetContainer.ContainedEntities.ToArray())
+ mailContents.Insert(entity);
+
+ mailComponent.IsFragile = isFragile;
+ mailComponent.IsPriority = isPriority;
+
+ _mailSystem.SetupMail(mailUid, teleporterComponent, recipient.Value);
+
+ var teleporterQueue = _containerSystem.EnsureContainer(teleporterComponent.Owner, "queued");
+ teleporterQueue.Insert(mailUid);
+ shell.WriteLine(Loc.GetString("command-mailto-success", ("timeToTeleport", teleporterComponent.TeleportInterval.TotalSeconds - teleporterComponent.Accumulator)));
+ }
+}
+
+[AdminCommand(AdminFlags.Fun)]
+public sealed class MailNowCommand : IConsoleCommand
+{
+ public string Command => "mailnow";
+ public string Description => Loc.GetString("command-mailnow");
+ public string Help => Loc.GetString("command-mailnow-help", ("command", Command));
+
+ [Dependency] private readonly IEntityManager _entityManager = default!;
+ [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
+
+ public async void Execute(IConsoleShell shell, string argStr, string[] args)
+ {
+ var _mailSystem = _entitySystemManager.GetEntitySystem();
+
+ foreach (var mailTeleporter in _entityManager.EntityQuery())
+ {
+ mailTeleporter.Accumulator += (float) mailTeleporter.TeleportInterval.TotalSeconds - mailTeleporter.Accumulator;
+ }
+
+ shell.WriteLine(Loc.GetString("command-mailnow-success"));
+ }
+}
diff --git a/Content.Server/Nyanotrasen/Mail/MailSystem.cs b/Content.Server/Nyanotrasen/Mail/MailSystem.cs
new file mode 100644
index 00000000000..d39159b7d46
--- /dev/null
+++ b/Content.Server/Nyanotrasen/Mail/MailSystem.cs
@@ -0,0 +1,741 @@
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Threading;
+using Robust.Shared.Audio;
+using Robust.Shared.Containers;
+using Robust.Shared.Prototypes;
+using Robust.Shared.Random;
+using Content.Server.Access.Systems;
+using Content.Server.Cargo.Components;
+using Content.Server.Cargo.Systems;
+using Content.Server.Chat.Systems;
+using Content.Server.Chemistry.EntitySystems;
+using Content.Server.Damage.Components;
+using Content.Server.Destructible;
+using Content.Server.Destructible.Thresholds;
+using Content.Server.Destructible.Thresholds.Behaviors;
+using Content.Server.Destructible.Thresholds.Triggers;
+using Content.Server.Fluids.Components;
+using Content.Server.Item;
+using Content.Server.Mail.Components;
+using Content.Server.Mind;
+using Content.Server.Nutrition.Components;
+using Content.Server.Popups;
+using Content.Server.Power.Components;
+using Content.Server.Station.Systems;
+using Content.Server.Spawners.EntitySystems;
+using Content.Shared.Access.Components;
+using Content.Shared.Access.Systems;
+using Content.Shared.Damage;
+using Content.Shared.Emag.Components;
+using Content.Shared.Destructible;
+using Content.Shared.Emag.Systems;
+using Content.Shared.Examine;
+using Content.Shared.Hands.EntitySystems;
+using Content.Shared.Interaction;
+using Content.Shared.Interaction.Events;
+using Content.Shared.Mail;
+using Content.Shared.Maps;
+using Content.Shared.PDA;
+using Content.Shared.Random.Helpers;
+using Content.Shared.Roles;
+using Content.Shared.Storage;
+using Content.Shared.Tag;
+using Timer = Robust.Shared.Timing.Timer;
+
+namespace Content.Server.Mail
+{
+ public sealed class MailSystem : EntitySystem
+ {
+ [Dependency] private readonly PopupSystem _popupSystem = default!;
+ [Dependency] private readonly AccessReaderSystem _accessSystem = default!;
+ [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
+ [Dependency] private readonly IdCardSystem _idCardSystem = default!;
+ [Dependency] private readonly IRobustRandom _random = default!;
+ [Dependency] private readonly TagSystem _tagSystem = default!;
+ [Dependency] private readonly CargoSystem _cargoSystem = default!;
+ [Dependency] private readonly StationSystem _stationSystem = default!;
+ [Dependency] private readonly ChatSystem _chatSystem = default!;
+ [Dependency] private readonly IPrototypeManager _prototypeManager = default!;
+ [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
+ [Dependency] private readonly SolutionContainerSystem _solutionContainerSystem = default!;
+ [Dependency] private readonly SharedAppearanceSystem _appearanceSystem = default!;
+ [Dependency] private readonly SharedAudioSystem _audioSystem = default!;
+ [Dependency] private readonly DamageableSystem _damageableSystem = default!;
+ [Dependency] private readonly ItemSystem _itemSystem = default!;
+ [Dependency] private readonly MindSystem _mindSystem = default!;
+
+ private ISawmill _sawmill = default!;
+
+ public override void Initialize()
+ {
+ base.Initialize();
+
+ _sawmill = Logger.GetSawmill("mail");
+
+ SubscribeLocalEvent(OnSpawnPlayer, after: new[] { typeof(SpawnPointSystem) });
+
+ SubscribeLocalEvent(OnRemove);
+ SubscribeLocalEvent(OnUseInHand);
+ SubscribeLocalEvent(OnAfterInteractUsing);
+ SubscribeLocalEvent(OnExamined);
+ SubscribeLocalEvent(OnDestruction);
+ SubscribeLocalEvent(OnDamage);
+ SubscribeLocalEvent(OnBreak);
+ SubscribeLocalEvent(OnMailEmagged);
+ }
+
+ public override void Update(float frameTime)
+ {
+ base.Update(frameTime);
+ foreach (var mailTeleporter in EntityQuery())
+ {
+ if (TryComp(mailTeleporter.Owner, out var power) && !power.Powered)
+ return;
+
+ mailTeleporter.Accumulator += frameTime;
+
+ if (mailTeleporter.Accumulator < mailTeleporter.TeleportInterval.TotalSeconds)
+ continue;
+
+ mailTeleporter.Accumulator -= (float) mailTeleporter.TeleportInterval.TotalSeconds;
+
+ SpawnMail(mailTeleporter.Owner, mailTeleporter);
+ }
+ }
+
+ ///
+ /// Dynamically add the MailReceiver component to appropriate entities.
+ ///
+ private void OnSpawnPlayer(PlayerSpawningEvent args)
+ {
+ if (args.SpawnResult == null ||
+ args.Job == null ||
+ args.Station is not {} station)
+ {
+ return;
+ }
+
+ if (!HasComp(station))
+ return;
+
+ AddComp(args.SpawnResult.Value);
+ }
+
+ private void OnRemove(EntityUid uid, MailComponent component, ComponentRemove args)
+ {
+ // Make sure the priority timer doesn't run.
+ if (component.priorityCancelToken != null)
+ component.priorityCancelToken.Cancel();
+ }
+
+ ///
+ /// Try to open the mail.
+ ///
+ private void OnUseInHand(EntityUid uid, MailComponent component, UseInHandEvent args)
+ {
+ if (!component.IsEnabled)
+ return;
+ if (component.IsLocked)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mail-locked"), uid, args.User);
+ return;
+ }
+ OpenMail(uid, component, args.User);
+ }
+
+ ///
+ /// Handle logic similar between a normal mail unlock and an emag
+ /// frying out the lock.
+ ///
+ private void UnlockMail(EntityUid uid, MailComponent component)
+ {
+ component.IsLocked = false;
+ UpdateAntiTamperVisuals(uid, false);
+
+ if (component.IsPriority)
+ {
+ // This is a successful delivery. Keep the failure timer from triggering.
+ if (component.priorityCancelToken != null)
+ component.priorityCancelToken.Cancel();
+
+ // The priority tape is visually considered to be a part of the
+ // anti-tamper lock, so remove that too.
+ _appearanceSystem.SetData(uid, MailVisuals.IsPriority, false);
+
+ // The examination code depends on this being false to not show
+ // the priority tape description anymore.
+ component.IsPriority = false;
+ }
+ }
+
+ ///
+ /// Check the ID against the mail's lock
+ ///
+ private void OnAfterInteractUsing(EntityUid uid, MailComponent component, AfterInteractUsingEvent args)
+ {
+ if (!args.CanReach || !component.IsLocked)
+ return;
+
+ if (!TryComp(uid, out var access))
+ return;
+
+ IdCardComponent? idCard = null; // We need an ID card.
+
+ if (HasComp(args.Used)) /// Can we find it in a PDA if the user is using that?
+ {
+ _idCardSystem.TryGetIdCard(args.Used, out var pdaID);
+ idCard = pdaID;
+ }
+
+ if (HasComp(args.Used)) /// Or are they using an id card directly?
+ idCard = Comp(args.Used);
+
+ if (idCard == null) /// Return if we still haven't found an id card.
+ return;
+
+ if (!HasComp(uid))
+ {
+ if (idCard.FullName != component.Recipient || idCard.JobTitle != component.RecipientJob)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mail-recipient-mismatch"), uid, args.User);
+ return;
+ }
+
+ if (!_accessSystem.IsAllowed(uid, args.User))
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mail-invalid-access"), uid, args.User);
+ return;
+ }
+ }
+
+ UnlockMail(uid, component);
+
+ if (!component.IsProfitable)
+ {
+ _popupSystem.PopupEntity(Loc.GetString("mail-unlocked"), uid, args.User);
+ return;
+ }
+
+ _popupSystem.PopupEntity(Loc.GetString("mail-unlocked-reward", ("bounty", component.Bounty)), uid, args.User);
+
+ component.IsProfitable = false;
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var station, out var account))
+ {
+ if (_stationSystem.GetOwningStation(uid) != station)
+ continue;
+
+ _cargoSystem.UpdateBankAccount(station, account, component.Bounty);
+ return;
+ }
+ }
+
+ private void OnExamined(EntityUid uid, MailComponent component, ExaminedEvent args)
+ {
+ if (!args.IsInDetailsRange)
+ {
+ args.PushMarkup(Loc.GetString("mail-desc-far"));
+ return;
+ }
+
+ args.PushMarkup(Loc.GetString("mail-desc-close", ("name", component.Recipient), ("job", component.RecipientJob)));
+
+ if (component.IsFragile)
+ args.PushMarkup(Loc.GetString("mail-desc-fragile"));
+
+ if (component.IsPriority)
+ {
+ if (component.IsProfitable)
+ args.PushMarkup(Loc.GetString("mail-desc-priority"));
+ else
+ args.PushMarkup(Loc.GetString("mail-desc-priority-inactive"));
+ }
+ }
+
+ ///
+ /// Penalize a station for a failed delivery.
+ ///
+ ///
+ /// This will mark a parcel as no longer being profitable, which will
+ /// prevent multiple failures on different conditions for the same
+ /// delivery.
+ ///
+ /// The standard penalization is breaking the anti-tamper lock,
+ /// but this allows a delivery to fail for other reasons too
+ /// while having a generic function to handle different messages.
+ ///
+ public void PenalizeStationFailedDelivery(EntityUid uid, MailComponent component, string localizationString)
+ {
+ if (!component.IsProfitable)
+ return;
+
+ _chatSystem.TrySendInGameICMessage(uid, Loc.GetString(localizationString, ("credits", component.Penalty)), InGameICChatType.Speak, false);
+ _audioSystem.PlayPvs(component.PenaltySound, uid);
+
+ component.IsProfitable = false;
+
+ if (component.IsPriority)
+ _appearanceSystem.SetData(uid, MailVisuals.IsPriorityInactive, true);
+
+ var query = EntityQueryEnumerator();
+ while (query.MoveNext(out var station, out var account))
+ {
+ if (_stationSystem.GetOwningStation(uid) != station)
+ continue;
+
+ _cargoSystem.UpdateBankAccount(station, account, component.Penalty);
+ return;
+ }
+ }
+
+ private void OnDestruction(EntityUid uid, MailComponent component, DestructionEventArgs args)
+ {
+ if (component.IsLocked)
+ PenalizeStationFailedDelivery(uid, component, "mail-penalty-lock");
+
+ if (component.IsEnabled)
+ OpenMail(uid, component);
+
+ UpdateAntiTamperVisuals(uid, false);
+ }
+
+ private void OnDamage(EntityUid uid, MailComponent component, DamageChangedEvent args)
+ {
+ if (args.DamageDelta == null)
+ return;
+
+ if (!_containerSystem.TryGetContainer(uid, "contents", out var contents))
+ return;
+
+ // Transfer damage to the contents.
+ // This should be a general-purpose feature for all containers in the future.
+ foreach (var entity in contents.ContainedEntities.ToArray())
+ {
+ _damageableSystem.TryChangeDamage(entity, args.DamageDelta);
+ }
+ }
+
+ private void OnBreak(EntityUid uid, MailComponent component, BreakageEventArgs args)
+ {
+ _appearanceSystem.SetData(uid, MailVisuals.IsBroken, true);
+
+ if (component.IsFragile)
+ PenalizeStationFailedDelivery(uid, component, "mail-penalty-fragile");
+ }
+
+ private void OnMailEmagged(EntityUid uid, MailComponent component, ref GotEmaggedEvent args)
+ {
+ if (!component.IsLocked)
+ return;
+
+ UnlockMail(uid, component);
+
+ _popupSystem.PopupEntity(Loc.GetString("mail-unlocked-by-emag"), uid, args.UserUid);
+
+ _audioSystem.PlayPvs(component.EmagSound, uid, AudioParams.Default.WithVolume(4));
+ component.IsProfitable = false;
+ args.Handled = true;
+ }
+
+ ///
+ /// Returns true if the given entity is considered fragile for delivery.
+ ///
+ public bool IsEntityFragile(EntityUid uid, int fragileDamageThreshold)
+ {
+ // It takes damage on falling.
+ if (HasComp(uid))
+ return true;
+
+ // It can be spilled easily and has something to spill.
+ if (HasComp(uid)
+ && TryComp(uid, out DrinkComponent? drinkComponent)
+ && drinkComponent.Opened
+ && _solutionContainerSystem.PercentFull(uid) > 0)
+ return true;
+
+ // It might be made of non-reinforced glass.
+ if (TryComp(uid, out DamageableComponent? damageableComponent)
+ && damageableComponent.DamageModifierSetId == "Glass")
+ return true;
+
+ // Fallback: It breaks or is destroyed in less than a damage
+ // threshold dictated by the teleporter.
+ if (TryComp(uid, out DestructibleComponent? destructibleComp))
+ {
+ foreach (var threshold in destructibleComp.Thresholds)
+ {
+ if (threshold.Trigger is DamageTrigger trigger
+ && trigger.Damage < fragileDamageThreshold)
+ {
+ foreach (var behavior in threshold.Behaviors)
+ {
+ if (behavior is DoActsBehavior doActs)
+ {
+ if (doActs.Acts.HasFlag(ThresholdActs.Breakage)
+ || doActs.Acts.HasFlag(ThresholdActs.Destruction))
+ {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return false;
+ }
+
+ public bool TryMatchJobTitleToDepartment(string jobTitle, [NotNullWhen(true)] out string? jobDepartment)
+ {
+ foreach (var department in _prototypeManager.EnumeratePrototypes())
+ {
+ foreach (var role in department.Roles)
+ {
+ if (_prototypeManager.TryIndex(role, out JobPrototype? _jobPrototype)
+ && _jobPrototype.LocalizedName == jobTitle)
+ {
+ jobDepartment = department.ID;
+ return true;
+ }
+ }
+ }
+
+ jobDepartment = null;
+ return false;
+ }
+
+ public bool TryMatchJobTitleToPrototype(string jobTitle, [NotNullWhen(true)] out JobPrototype? jobPrototype)
+ {
+ foreach (var job in _prototypeManager.EnumeratePrototypes())
+ {
+ if (job.LocalizedName == jobTitle)
+ {
+ jobPrototype = job;
+ return true;
+ }
+ }
+
+ jobPrototype = null;
+ return false;
+ }
+
+ public bool TryMatchJobTitleToIcon(string jobTitle, [NotNullWhen(true)] out string? jobIcon)
+ {
+ foreach (var job in _prototypeManager.EnumeratePrototypes())
+ {
+ if (job.LocalizedName == jobTitle)
+ {
+ jobIcon = job.Icon;
+ return true;
+ }
+ }
+
+ jobIcon = null;
+ return false;
+ }
+
+ ///
+ /// Handle all the gritty details particular to a new mail entity.
+ ///
+ ///
+ /// This is separate mostly so the unit tests can get to it.
+ ///
+ public void SetupMail(EntityUid uid, MailTeleporterComponent component, MailRecipient recipient)
+ {
+ var mailComp = EnsureComp(uid);
+
+ var container = _containerSystem.EnsureContainer(uid, "contents");
+ foreach (var item in EntitySpawnCollection.GetSpawns(mailComp.Contents, _random))
+ {
+ var entity = EntityManager.SpawnEntity(item, Transform(uid).Coordinates);
+ if (!container.Insert(entity))
+ {
+ _sawmill.Error($"Can't insert {ToPrettyString(entity)} into new mail delivery {ToPrettyString(uid)}! Deleting it.");
+ QueueDel(entity);
+ }
+ else if (!mailComp.IsFragile && IsEntityFragile(entity, component.FragileDamageThreshold))
+ {
+ mailComp.IsFragile = true;
+ }
+ }
+
+ if (_random.Prob(component.PriorityChance))
+ mailComp.IsPriority = true;
+
+ // This needs to override both the random probability and the
+ // entity prototype, so this is fine.
+ if (!recipient.MayReceivePriorityMail)
+ mailComp.IsPriority = false;
+
+ mailComp.RecipientJob = recipient.Job;
+ mailComp.Recipient = recipient.Name;
+
+ if (mailComp.IsFragile)
+ {
+ mailComp.Bounty += component.FragileBonus;
+ mailComp.Penalty += component.FragileMalus;
+ _appearanceSystem.SetData(uid, MailVisuals.IsFragile, true);
+ }
+
+ if (mailComp.IsPriority)
+ {
+ mailComp.Bounty += component.PriorityBonus;
+ mailComp.Penalty += component.PriorityMalus;
+ _appearanceSystem.SetData(uid, MailVisuals.IsPriority, true);
+
+ mailComp.priorityCancelToken = new CancellationTokenSource();
+
+ Timer.Spawn((int) component.priorityDuration.TotalMilliseconds,
+ () => PenalizeStationFailedDelivery(uid, mailComp, "mail-penalty-expired"),
+ mailComp.priorityCancelToken.Token);
+ }
+
+ if (TryMatchJobTitleToIcon(recipient.Job, out string? icon))
+ _appearanceSystem.SetData(uid, MailVisuals.JobIcon, icon);
+
+ MetaData(uid).EntityName = Loc.GetString("mail-item-name-addressed",
+ ("recipient", recipient.Name));
+
+ var accessReader = EnsureComp(uid);
+ accessReader.AccessLists.Add(recipient.AccessTags);
+ }
+
+ ///
+ /// Return the parcels waiting for delivery.
+ ///
+ /// The mail teleporter to check.
+ public List GetUndeliveredParcels(EntityUid uid)
+ {
+ // An alternative solution would be to keep a list of the unopened
+ // parcels spawned by the teleporter and see if they're not carried
+ // by someone, but this is simple, and simple is good.
+ List undeliveredParcels = new();
+ foreach (var entityInTile in TurfHelpers.GetEntitiesInTile(Transform(uid).Coordinates, LookupFlags.Dynamic | LookupFlags.Sundries))
+ {
+ if (HasComp(entityInTile))
+ undeliveredParcels.Add(entityInTile);
+ }
+ return undeliveredParcels;
+ }
+
+ ///
+ /// Return how many parcels are waiting for delivery.
+ ///
+ /// The mail teleporter to check.
+ public uint GetUndeliveredParcelCount(EntityUid uid)
+ {
+ return (uint) GetUndeliveredParcels(uid).Count();
+ }
+
+ ///
+ /// Try to match a mail receiver to a mail teleporter.
+ ///
+ public bool TryGetMailTeleporterForReceiver(MailReceiverComponent receiver, [NotNullWhen(true)] out MailTeleporterComponent? teleporterComponent)
+ {
+ foreach (var mailTeleporter in EntityQuery())
+ {
+ if (_stationSystem.GetOwningStation(receiver.Owner) == _stationSystem.GetOwningStation(mailTeleporter.Owner))
+ {
+ teleporterComponent = mailTeleporter;
+ return true;
+ }
+ }
+
+ teleporterComponent = null;
+ return false;
+ }
+
+ ///
+ /// Try to construct a recipient struct for a mail parcel based on a receiver.
+ ///
+ public bool TryGetMailRecipientForReceiver(MailReceiverComponent receiver, [NotNullWhen(true)] out MailRecipient? recipient)
+ {
+ // Because of the way this works, people are not considered
+ // candidates for mail if there is no valid PDA or ID in their slot
+ // or active hand. A better future solution might be checking the
+ // station records, possibly cross-referenced with the medical crew
+ // scanner to look for living recipients. TODO
+
+ if (_idCardSystem.TryFindIdCard(receiver.Owner, out var idCard)
+ && TryComp(idCard.Owner, out var access)
+ && idCard.FullName != null
+ && idCard.JobTitle != null)
+ {
+ HashSet accessTags = access.Tags;
+
+ var mayReceivePriorityMail = true;
+
+ if (_mindSystem.GetMind(receiver.Owner) == null)
+ {
+ mayReceivePriorityMail = false;
+ }
+
+ recipient = new MailRecipient(idCard.FullName,
+ idCard.JobTitle,
+ accessTags,
+ mayReceivePriorityMail);
+
+ return true;
+ }
+
+ recipient = null;
+ return false;
+ }
+
+ ///
+ /// Get the list of valid mail recipients for a mail teleporter.
+ ///
+ public List GetMailRecipientCandidates(EntityUid uid)
+ {
+ List candidateList = new();
+
+ foreach (var receiver in EntityQuery())
+ {
+ if (_stationSystem.GetOwningStation(receiver.Owner) != _stationSystem.GetOwningStation(uid))
+ continue;
+
+ if (TryGetMailRecipientForReceiver(receiver, out MailRecipient? recipient))
+ candidateList.Add(recipient.Value);
+ }
+
+ return candidateList;
+ }
+
+ ///
+ /// Handle the spawning of all the mail for a mail teleporter.
+ ///
+ public void SpawnMail(EntityUid uid, MailTeleporterComponent? component = null)
+ {
+ if (!Resolve(uid, ref component))
+ {
+ _sawmill.Error($"Tried to SpawnMail on {ToPrettyString(uid)} without a valid MailTeleporterComponent!");
+ return;
+ }
+
+ if (GetUndeliveredParcelCount(uid) >= component.MaximumUndeliveredParcels)
+ return;
+
+ var candidateList = GetMailRecipientCandidates(uid);
+
+ if (candidateList.Count <= 0)
+ {
+ _sawmill.Error("List of mail candidates was empty!");
+ return;
+ }
+
+ if (!_prototypeManager.TryIndex(component.MailPool, out var pool))
+ {
+ _sawmill.Error($"Can't index {ToPrettyString(uid)}'s MailPool {component.MailPool}!");
+ return;
+ }
+
+ for (int i = 0;
+ i < component.MinimumDeliveriesPerTeleport + candidateList.Count / component.CandidatesPerDelivery;
+ i++)
+ {
+ var candidate = _random.Pick(candidateList);
+ var possibleParcels = new Dictionary(pool.Everyone);
+
+ if (TryMatchJobTitleToPrototype(candidate.Job, out JobPrototype? jobPrototype)
+ && pool.Jobs.TryGetValue(jobPrototype.ID, out Dictionary? jobParcels))
+ {
+ possibleParcels = possibleParcels.Union(jobParcels)
+ .GroupBy(g => g.Key)
+ .ToDictionary(pair => pair.Key, pair => pair.First().Value);
+ }
+
+ if (TryMatchJobTitleToDepartment(candidate.Job, out string? department)
+ && pool.Departments.TryGetValue(department, out Dictionary? departmentParcels))
+ {
+ possibleParcels = possibleParcels.Union(departmentParcels)
+ .GroupBy(g => g.Key)
+ .ToDictionary(pair => pair.Key, pair => pair.First().Value);
+ }
+
+ var accumulated = 0f;
+ var randomPoint = _random.NextFloat(possibleParcels.Values.Sum());
+ string? chosenParcel = null;
+ foreach (var (key, weight) in possibleParcels)
+ {
+ accumulated += weight;
+ if (accumulated >= randomPoint)
+ {
+ chosenParcel = key;
+ break;
+ }
+ }
+
+ if (chosenParcel == null)
+ {
+ _sawmill.Error($"MailSystem wasn't able to find a deliverable parcel for {candidate.Name}, {candidate.Job}!");
+ return;
+ }
+
+ var mail = EntityManager.SpawnEntity(chosenParcel, Transform(uid).Coordinates);
+ SetupMail(mail, component, candidate);
+ }
+
+ if (_containerSystem.TryGetContainer(uid, "queued", out var queued))
+ _containerSystem.EmptyContainer(queued);
+
+ _audioSystem.PlayPvs(component.TeleportSound, uid);
+ }
+
+ public void OpenMail(EntityUid uid, MailComponent? component = null, EntityUid? user = null)
+ {
+ if (!Resolve(uid, ref component))
+ return;
+
+ _audioSystem.PlayPvs(component.OpenSound, uid);
+
+ if (user != null)
+ _handsSystem.TryDrop((EntityUid) user);
+
+ if (!_containerSystem.TryGetContainer(uid, "contents", out var contents))
+ {
+ // I silenced this error because it fails non deterministically in tests and doesn't seem to effect anything else.
+ // _sawmill.Error($"Mail {ToPrettyString(uid)} was missing contents container!");
+ return;
+ }
+
+ foreach (var entity in contents.ContainedEntities.ToArray())
+ {
+ _handsSystem.PickupOrDrop(user, entity);
+ }
+
+ _itemSystem.SetSize(uid, 1);
+ _tagSystem.AddTag(uid, "Trash");
+ _tagSystem.AddTag(uid, "Recyclable");
+ component.IsEnabled = false;
+ UpdateMailTrashState(uid, true);
+ }
+
+ private void UpdateAntiTamperVisuals(EntityUid uid, bool isLocked)
+ {
+ _appearanceSystem.SetData(uid, MailVisuals.IsLocked, isLocked);
+ }
+
+ private void UpdateMailTrashState(EntityUid uid, bool isTrash)
+ {
+ _appearanceSystem.SetData(uid, MailVisuals.IsTrash, isTrash);
+ }
+ }
+
+ public struct MailRecipient
+ {
+ public string Name;
+ public string Job;
+ public HashSet AccessTags;
+ public bool MayReceivePriorityMail;
+
+ public MailRecipient(string name, string job, HashSet accessTags, bool mayReceivePriorityMail)
+ {
+ Name = name;
+ Job = job;
+ AccessTags = accessTags;
+ MayReceivePriorityMail = mayReceivePriorityMail;
+ }
+ }
+}
diff --git a/Content.Shared/Nyanotrasen/Mail/MailDeliveryPoolPrototype.cs b/Content.Shared/Nyanotrasen/Mail/MailDeliveryPoolPrototype.cs
new file mode 100644
index 00000000000..544f489d28f
--- /dev/null
+++ b/Content.Shared/Nyanotrasen/Mail/MailDeliveryPoolPrototype.cs
@@ -0,0 +1,30 @@
+using Robust.Shared.Prototypes;
+
+namespace Content.Shared.Mail;
+
+///
+/// Generic random weighting dataset to use.
+///
+[Prototype("mailDeliveryPool")]
+public sealed class MailDeliveryPoolPrototype : IPrototype
+{
+ [IdDataFieldAttribute] public string ID { get; } = default!;
+
+ ///
+ /// Mail that can be sent to everyone.
+ ///
+ [DataField("everyone")]
+ public Dictionary Everyone = new();
+
+ ///
+ /// Mail that can be sent only to specific jobs.
+ ///
+ [DataField("jobs")]
+ public Dictionary> Jobs = new();
+
+ ///
+ /// Mail that can be sent only to specific departments.
+ ///
+ [DataField("departments")]
+ public Dictionary> Departments = new();
+}
diff --git a/Content.Shared/Nyanotrasen/Mail/MailVisuals.cs b/Content.Shared/Nyanotrasen/Mail/MailVisuals.cs
new file mode 100644
index 00000000000..1e1dc8b8ce7
--- /dev/null
+++ b/Content.Shared/Nyanotrasen/Mail/MailVisuals.cs
@@ -0,0 +1,19 @@
+using Robust.Shared.Serialization;
+
+namespace Content.Shared.Mail
+{
+ ///
+ /// Stores the visuals for mail.
+ ///
+ [Serializable, NetSerializable]
+ public enum MailVisuals : byte
+ {
+ IsLocked,
+ IsTrash,
+ IsBroken,
+ IsFragile,
+ IsPriority,
+ IsPriorityInactive,
+ JobIcon,
+ }
+}
diff --git a/Content.Shared/Nyanotrasen/Mail/SharedMailComponent.cs b/Content.Shared/Nyanotrasen/Mail/SharedMailComponent.cs
new file mode 100644
index 00000000000..e6ef9c8422c
--- /dev/null
+++ b/Content.Shared/Nyanotrasen/Mail/SharedMailComponent.cs
@@ -0,0 +1,4 @@
+namespace Content.Shared.Mail
+{
+ public partial class SharedMailComponent : Component {}
+}
diff --git a/Resources/Locale/en-US/Mail/mail.ftl b/Resources/Locale/en-US/Mail/mail.ftl
new file mode 100644
index 00000000000..7e702f0b406
--- /dev/null
+++ b/Resources/Locale/en-US/Mail/mail.ftl
@@ -0,0 +1,30 @@
+mail-recipient-mismatch = Recipient name or job does not match.
+mail-invalid-access = Recipient name and job match, but access isn't as expected.
+mail-locked = The anti-tamper lock hasn't been removed. Tap the recipient's ID.
+mail-desc-far = A parcel of mail. You can't make out who it's addressed to from this distance.
+mail-desc-close = A parcel of mail addressed to {CAPITALIZE($name)}, {$job}.
+mail-desc-fragile = It has a [color=red]red fragile label[/color].
+mail-desc-priority = The anti-tamper lock's [color=yellow]yellow priority tape[/color] is active. Better deliver it on time!
+mail-desc-priority-inactive = The anti-tamper lock's [color=#886600]yellow priority tape[/color] is inactive.
+mail-unlocked = Anti-tamper system unlocked.
+mail-unlocked-by-emag = Anti-tamper system *BZZT*.
+mail-unlocked-reward = Anti-tamper system unlocked. {$bounty} zorkmids have been added to cargo's account.
+mail-penalty-lock = ANTI-TAMPER LOCK BROKEN. CARGO BANK ACCOUNT PENALIZED BY {$credits} CREDITS.
+mail-penalty-fragile = INTEGRITY COMPROMISED. CARGO BANK ACCOUNT PENALIZED BY {$credits} CREDITS.
+mail-penalty-expired = DELIVERY PAST DUE. CARGO BANK ACCOUNT PENALIZED BY {$credits} CREDITS.
+mail-item-name-unaddressed = mail
+mail-item-name-addressed = mail ({$recipient})
+
+command-mailto-description = Queue a parcel to be delivered to an entity. Example usage: `mailto 1234 5678 false false`. The target container's contents will be transferred to an actual mail parcel.
+command-mailto-help = Usage: {$command} [is-fragile: true or false] [is-priority: true or false]
+command-mailto-no-mailreceiver = Target recipient entity does not have a {$requiredComponent}.
+command-mailto-no-blankmail = The {$blankMail} prototype doesn't exist. Something is very wrong. Contact a programmer.
+command-mailto-bogus-mail = {$blankMail} did not have {$requiredMailComponent}. Something is very wrong. Contact a programmer.
+command-mailto-invalid-container = Target container entity does not have a {$requiredContainer} container.
+command-mailto-unable-to-receive = Target recipient entity was unable to be setup for receiving mail. ID may be missing.
+command-mailto-no-teleporter-found = Target recipient entity was unable to be matched to any station's mail teleporter. Recipient may be off-station.
+command-mailto-success = Success! Mail parcel has been queued for next teleport in {$timeToTeleport} seconds.
+
+command-mailnow = Force all mail teleporters to deliver another round of mail as soon as possible. This will not bypass the undelivered mail limit.
+command-mailnow-help = Usage: {$command}
+command-mailnow-success = Success! All mail teleporters will be delivering another round of mail soon.
diff --git a/Resources/Prototypes/Entities/Stations/nanotrasen.yml b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
index 701db325882..f87beb566d8 100644
--- a/Resources/Prototypes/Entities/Stations/nanotrasen.yml
+++ b/Resources/Prototypes/Entities/Stations/nanotrasen.yml
@@ -22,6 +22,7 @@
- BaseStationSiliconLawCrewsimov
- BaseStationAllEventsEligible
- BaseStationNanotrasen
+ - BaseStationMail # Nyano component, required for station mail to function
noSpawn: true
components:
- type: Transform
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
new file mode 100644
index 00000000000..2b7a193df11
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/base_mail.yml
@@ -0,0 +1,107 @@
+- type: entity
+ parent: BaseItem
+ abstract: true
+ id: BaseMail
+ name: mail-item-name-unaddressed
+ components:
+ - type: Item
+ size: 20
+ - type: Mail
+ - type: AccessReader
+ - type: Sprite
+ sprite: Nyanotrasen/Objects/Specific/Mail/mail.rsi
+ layers:
+ - state: icon
+ map: ["enum.MailVisualLayers.Icon"]
+ - state: postmark
+ - state: fragile
+ map: ["enum.MailVisualLayers.FragileStamp"]
+ visible: false
+ - map: ["enum.MailVisualLayers.JobStamp"]
+ sprite: Interface/Misc/job_icons.rsi
+ scale: 0.5, 0.5
+ offset: 0.275, 0.2
+ - state: locked
+ map: ["enum.MailVisualLayers.Lock"]
+ - state: priority
+ map: ["enum.MailVisualLayers.PriorityTape"]
+ visible: false
+ shader: unshaded
+ - state: broken
+ map: ["enum.MailVisualLayers.Breakage"]
+ visible: false
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.MailVisuals.IsTrash:
+ enum.MailVisualLayers.Icon:
+ True:
+ state: trash
+ False:
+ state: icon
+ enum.MailVisuals.IsLocked:
+ enum.MailVisualLayers.Lock:
+ True:
+ visible: true
+ False:
+ visible: false
+ enum.MailVisuals.IsFragile:
+ enum.MailVisualLayers.FragileStamp:
+ True:
+ visible: true
+ False:
+ visible: false
+ enum.MailVisuals.IsPriority:
+ enum.MailVisualLayers.PriorityTape:
+ True:
+ visible: true
+ False:
+ visible: false
+ enum.MailVisuals.IsPriorityInactive:
+ enum.MailVisualLayers.PriorityTape:
+ True:
+ shader: shaded
+ state: priority_inactive
+ False:
+ shader: unshaded
+ state: priority
+ enum.MailVisuals.IsBroken:
+ enum.MailVisualLayers.Breakage:
+ True:
+ visible: true
+ False:
+ visible: false
+ - type: Damageable
+ damageContainer: Inorganic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 5
+ triggersOnce: true
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Breakage" ]
+ - trigger:
+ !type:DamageTrigger
+ damage: 40
+ behaviors:
+ - !type:DoActsBehavior
+ acts: [ "Destruction" ]
+ - type: Speech
+ - type: DamageOnLand
+ damage:
+ types:
+ Blunt: 10
+ - type: DamageOtherOnHit
+ damage:
+ types:
+ Blunt: 5
+
+# This empty parcel is allowed to exist and evade the tests for the admin
+# mailto command.
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailAdminFun
+ suffix: adminfun
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail.yml
new file mode 100644
index 00000000000..e510d8155bd
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail.yml
@@ -0,0 +1,744 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailAlcohol
+ suffix: alcohol
+ components:
+ - type: Mail
+ contents:
+ - id: DrinkAbsintheBottleFull
+ orGroup: Drink
+ - id: DrinkBlueCuracaoBottleFull
+ orGroup: Drink
+ - id: DrinkGinBottleFull
+ orGroup: Drink
+ - id: DrinkMelonLiquorBottleFull
+ orGroup: Drink
+ - id: DrinkRumBottleFull
+ orGroup: Drink
+ - id: DrinkTequilaBottleFull
+ orGroup: Drink
+ - id: DrinkVermouthBottleFull
+ orGroup: Drink
+ - id: DrinkVodkaBottleFull
+ orGroup: Drink
+ - id: DrinkWineBottleFull
+ orGroup: Drink
+ - id: DrinkGlass
+ amount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSake
+ suffix: osake
+ components:
+ - type: Mail
+ contents:
+ - id: DrinkSakeCup
+ amount: 2
+ - id: DrinkTokkuri
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailAMEGuide
+ suffix: ameguide
+ components:
+ - type: Mail
+ contents:
+ - id: PaperWrittenAMEScribbles
+ - id: Pen
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBible
+ suffix: bible
+ components:
+ - type: Mail
+ contents:
+ - id: Bible
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBikeHorn
+ suffix: bike horn
+ components:
+ - type: Mail
+ contents:
+ - id: BikeHorn
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBlockGameDIY
+ suffix: blockgamediy
+ components:
+ - type: Mail
+ contents:
+ - id: BlockGameArcadeComputerCircuitboard
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBooks
+ suffix: books
+ components:
+ - type: Mail
+ contents:
+ # Don't use BookDemonomiconRandom.
+ # It uses a RandomSpawner which just spawns the book outside of the mail.
+ - id: BookDemonomicon1
+ orGroup: Demonomicon
+ - id: BookDemonomicon2
+ orGroup: Demonomicon
+ - id: BookDemonomicon3
+ orGroup: Demonomicon
+ # There's no way to signal "spawn nothing" with an orGroup,
+ # so have this blank book instead. Write your own demon summoning tome!
+ - id: BookRandom
+ prob: 3
+ orGroup: Demonomicon
+ - id: BookChemistryInsane
+ prob: 0.10
+ - id: BookBotanicalTextbook
+ prob: 0.5
+ - id: BookFishing
+ prob: 0.10
+ - id: BookDetective
+ prob: 0.10
+ - id: BookGnominomicon
+ prob: 0.2
+ - id: BookSalvageEpistemics1
+ prob: 0.025
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCake
+ suffix: cake
+ components:
+ - type: Mail
+ isFragile: true
+ isPriority: true
+ contents:
+ - id: FoodCakeBlueberry
+ orGroup: Cake
+ - id: FoodCakeCarrot
+ orGroup: Cake
+ - id: FoodCakeCheese
+ orGroup: Cake
+ - id: FoodCakeChocolate
+ orGroup: Cake
+ - id: FoodCakeChristmas
+ orGroup: Cake
+ - id: FoodCakeClown
+ orGroup: Cake
+ - id: FoodCakeLemon
+ orGroup: Cake
+ - id: FoodCakeLime
+ orGroup: Cake
+ - id: FoodCakeOrange
+ orGroup: Cake
+ - id: FoodCakePumpkin
+ orGroup: Cake
+ - id: FoodCakeVanilla
+ orGroup: Cake
+ - id: FoodMothMothmallow
+ orGroup: Cake
+ prob: 0.5
+ - id: KnifePlastic
+ - id: ForkPlastic
+ amount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCallForHelp
+ suffix: call-for-help
+ components:
+ - type: Mail
+ contents:
+ - id: PaperMailCallForHelp1
+ orGroup: Paper
+ - id: PaperMailCallForHelp2
+ orGroup: Paper
+ - id: PaperMailCallForHelp3
+ orGroup: Paper
+ - id: PaperMailCallForHelp4
+ orGroup: Paper
+ - id: PaperMailCallForHelp5
+ orGroup: Paper
+ - id: FlashlightLantern
+ orGroup: Gift
+ - id: Crowbar
+ orGroup: Gift
+ prob: 0.5
+ - id: CrowbarRed
+ orGroup: Gift
+ prob: 0.5
+ - id: ClothingMaskGas
+ orGroup: Gift
+ - id: WeaponFlareGun
+ orGroup: Gift
+ prob: 0.25
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCheese
+ suffix: cheese
+ components:
+ - type: Mail
+ isFragile: true
+ isPriority: true
+ contents:
+ - id: FoodCheese
+ - id: KnifePlastic
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailChocolate
+ suffix: chocolate
+ components:
+ - type: Mail
+ contents:
+ # TODO make some actual chocolate candy items.
+ - id: FoodSnackChocolate
+ amount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCigarettes
+ suffix: cigs
+ components:
+ - type: Mail
+ contents:
+ - id: CigPackRed
+ - id: CheapLighter
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCigars
+ suffix: Cigars
+ components:
+ - type: Mail
+ contents:
+ - id: CigarCase
+ - id: Lighter
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCookies
+ suffix: cookies
+ components:
+ - type: Mail
+ # What, you want to eat stale cookies?
+ isPriority: true
+ contents:
+ - id: FoodBakedCookie
+ - id: FoodBakedCookieOatmeal
+ - id: FoodBakedCookieRaisin
+ - id: FoodBakedCookieSugar
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplayArc
+ suffix: cosplay-arc
+ components:
+ - type: Mail
+ openSound: /Audio/Nyanotrasen/Voice/Felinid/cat_wilhelm.ogg
+ contents:
+ - id: ClothingCostumeArcDress
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplayGeisha
+ suffix: cosplay-geisha
+ components:
+ - type: Mail
+ contents:
+ - id: UniformGeisha
+ - id: DrinkTeapot
+ - id: DrinkTeacup
+ amount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplayMaid
+ suffix: cosplay-maid
+ components:
+ - type: Mail
+ contents:
+ - id: UniformMaid
+ - id: SprayBottleSpaceCleaner
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplayNurse
+ suffix: cosplay-nurse
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingUniformJumpskirtNurse
+ - id: Syringe
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplaySchoolgirl
+ suffix: cosplay-schoolgirl
+ components:
+ - type: Mail
+ contents:
+ - id: UniformSchoolgirlBlack
+ orGroup: Color
+ - id: UniformSchoolgirlBlue
+ orGroup: Color
+ - id: UniformSchoolgirlCyan
+ orGroup: Color
+ - id: UniformSchoolgirlGreen
+ orGroup: Color
+ - id: UniformSchoolgirlOrange
+ orGroup: Color
+ - id: UniformSchoolgirlPink
+ orGroup: Color
+ - id: UniformSchoolgirlPurple
+ orGroup: Color
+ - id: UniformSchoolgirlRed
+ orGroup: Color
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCosplayWizard
+ suffix: cosplay-wizard
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingOuterWizardFake
+ - id: ClothingHeadHatWizardFake
+ - id: ClothingShoesWizardFake
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCrayon
+ suffix: Crayon
+ components:
+ - type: Mail
+ contents:
+ - id: CrayonBox
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailFigurine
+ suffix: figurine
+ components:
+ - type: Mail
+ isFragile: true
+ contents:
+ - id: ToyAi
+ orGroup: Toy
+ - id: ToyNuke
+ orGroup: Toy
+ - id: ToyFigurinePassenger
+ orGroup: Toy
+ - id: ToyGriffin
+ orGroup: Toy
+ - id: ToyHonk
+ orGroup: Toy
+ - id: ToyIan
+ orGroup: Toy
+ - id: ToyMarauder
+ orGroup: Toy
+ - id: ToyMauler
+ orGroup: Toy
+ - id: ToyGygax
+ orGroup: Toy
+ - id: ToyOdysseus
+ orGroup: Toy
+ - id: ToyOwlman
+ orGroup: Toy
+ - id: ToyDeathRipley
+ orGroup: Toy
+ - id: ToyPhazon
+ orGroup: Toy
+ - id: ToyFireRipley
+ orGroup: Toy
+ - id: ToyReticence
+ orGroup: Toy
+ - id: ToyRipley
+ orGroup: Toy
+ - id: ToySeraph
+ orGroup: Toy
+ - id: ToyDurand
+ orGroup: Toy
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailFishingCap
+ suffix: fishingcap
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingHeadFishCap
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailFlashlight
+ suffix: Flashlight
+ components:
+ - type: Mail
+ contents:
+ - id: FlashlightLantern
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailFlowers
+ suffix: flowers
+ components:
+ - type: Mail
+ contents:
+ # TODO actual flowers
+ - id: ClothingHeadHatFlowerCrown
+ orGroup: Flower
+ - id: ClothingHeadHatHairflower
+ orGroup: Flower
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailHighlander
+ suffix: highlander
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingUniformJumpskirtColorRed
+ - id: ClothingHeadHatBeret
+ - id: DrinkRedMeadGlass
+ - id: Claymore
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailHighlanderDulled
+ suffix: highlander, dulled
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingUniformJumpskirtColorRed
+ - id: ClothingHeadHatBeret
+ - id: DrinkGlass
+ - id: ClaymoreDulled
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailHoneyBuns
+ suffix: honeybuns
+ components:
+ - type: Mail
+ contents:
+ - id: FoodBakedBunHoney
+ amount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailJunkFood
+ suffix: junk food
+ components:
+ - type: Mail
+ contents:
+ - id: FoodBoxDonkpocket
+ - id: FoodSnackChips
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailKatana
+ suffix: Katana
+ components:
+ - type: Mail
+ contents:
+ - id: Katana
+ prob: 0.1
+ orGroup: Katana
+ - id: KatanaDulled
+ prob: 0.9
+ orGroup: Katana
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailKnife
+ suffix: Knife
+ components:
+ - type: Mail
+ contents:
+ - id: CombatKnife
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMoney
+ suffix: money
+ components:
+ - type: Mail
+ contents:
+ - id: SpaceCash100
+ orGroup: Cash
+ prob: 0.3
+ - id: SpaceCash500
+ orGroup: Cash
+ prob: 0.6
+ - id: SpaceCash1000
+ orGroup: Cash
+ prob: 0.3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMuffins
+ suffix: muffins
+ components:
+ - type: Mail
+ isPriority: true
+ contents:
+ - id: FoodBakedMuffinBerry
+ - id: FoodBakedMuffinCherry
+ - id: FoodBakedMuffinBluecherry
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMoffins
+ suffix: moffins
+ components:
+ - type: Mail
+ isPriority: true
+ contents:
+ - id: FoodMothMoffin
+ amount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailNoir
+ suffix: noir
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingUniformJumpsuitDetectiveGrey
+ - id: ClothingUniformJumpskirtDetectiveGrey
+ - id: ClothingHeadHatBowlerHat
+ - id: ClothingOuterCoatGentle
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailPAI
+ suffix: PAI
+ components:
+ - type: Mail
+ contents:
+ - id: PersonalAI
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailPlushie
+ suffix: plushie
+ components:
+ - type: Mail
+ contents:
+ # These are all grouped up now to guarantee at least one item received.
+ # The downside is you're not going to get half a dozen plushies anymore.
+ - id: PlushieBee
+ orGroup: Plushie
+ - id: PlushieRGBee
+ prob: 0.5
+ orGroup: Plushie
+ - id: PlushieNuke
+ orGroup: Plushie
+ - id: PlushieRouny
+ orGroup: Plushie
+ - id: PlushieLizard
+ orGroup: Plushie
+ - id: PlushieSpaceLizard
+ orGroup: Plushie
+ - id: PlushieRatvar
+ orGroup: Plushie
+ - id: PlushieNar
+ orGroup: Plushie
+ - id: PlushieCarp
+ orGroup: Plushie
+ - id: PlushieSlime
+ orGroup: Plushie
+ - id: PlushieSnake
+ orGroup: Plushie
+ - id: PlushieMoffRandom
+ orGroup: Plushie
+ - id: PlushieMoff
+ orGroup: Plushie
+ - id: PlushieMoffsician
+ prob: 0.5
+ orGroup: Plushie
+ - id: PlushieMoffbar
+ prob: 0.5
+ orGroup: Plushie
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailRestraints
+ suffix: restraints
+ components:
+ - type: Mail
+ contents:
+ - id: Handcuffs
+ - id: ClothingMaskMuzzle
+ - id: ClothingEyesBlindfold
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSignallerKit
+ suffix: signallerkit
+ components:
+ - type: Mail
+ contents:
+ - id: Multitool
+ - id: RemoteSignaller
+
+# - type: entity
+# noSpawn: true
+# parent: BaseMail
+# id: MailSixPack
+# suffix: sixpack
+# components:
+# - type: Mail
+# contents:
+# - id: DrinkCanPack
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSkub
+ suffix: skub
+ components:
+ - type: Mail
+ contents:
+ - id: Skub
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSoda
+ suffix: soda
+ components:
+ - type: Mail
+ contents:
+ - id: DrinkColaBottleFull
+ orGroup: Soda
+ - id: DrinkSpaceMountainWindBottleFull
+ orGroup: Soda
+ - id: DrinkSpaceUpBottleFull
+ orGroup: Soda
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSpaceVillainDIY
+ suffix: spacevilliandiy
+ components:
+ - type: Mail
+ contents:
+ - id: SpaceVillainArcadeComputerCircuitboard
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSunglasses
+ suffix: Sunglasses
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingEyesGlassesSunglasses
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailVagueThreat
+ suffix: vague-threat
+ components:
+ - type: Mail
+ contents:
+ - id: PaperMailVagueThreat1
+ orGroup: Paper
+ - id: PaperMailVagueThreat2
+ orGroup: Paper
+ - id: PaperMailVagueThreat3
+ orGroup: Paper
+ - id: PaperMailVagueThreat4
+ orGroup: Paper
+ - id: PaperMailVagueThreat5
+ orGroup: Paper
+ - id: PaperMailVagueThreat6
+ orGroup: Paper
+ - id: PaperMailVagueThreat7
+ orGroup: Paper
+ - id: PaperMailVagueThreat8
+ orGroup: Paper
+ - id: PaperMailVagueThreat9
+ orGroup: Paper
+ - id: PaperMailVagueThreat10
+ orGroup: Paper
+ - id: PaperMailVagueThreat11
+ orGroup: Paper
+ - id: PaperMailVagueThreat12
+ orGroup: Paper
+ - id: KitchenKnife
+ orGroup: ThreateningObject
+ - id: ButchCleaver
+ orGroup: ThreateningObject
+ - id: CombatKnife
+ orGroup: ThreateningObject
+ - id: SurvivalKnife
+ orGroup: ThreateningObject
+ - id: SoapHomemade
+ orGroup: ThreateningObject
+ - id: FoodMeat
+ orGroup: ThreateningObject
+ - id: OrganHumanHeart
+ orGroup: ThreateningObject
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailWinterCoat
+ suffix: wintercoat
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingOuterWinterCoat
+ orGroup: Coat
+ - id: ClothingOuterWinterCoatLong
+ orGroup: Coat
+ - id: ClothingOuterWinterCoatPlaid
+ orGroup: Coat
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_civilian.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_civilian.yml
new file mode 100644
index 00000000000..a41fac14ffa
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_civilian.yml
@@ -0,0 +1,195 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBotanistChemicalBottles
+ suffix: botanistchemicals
+ components:
+ - type: Mail
+ contents:
+ - id: RobustHarvestChemistryBottle
+ orGroup: Chemical
+ prob: 0.6
+ - id: WeedSpray
+ orGroup: Chemical
+ prob: 0.4
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBotanistMutagen
+ suffix: mutagen
+ components:
+ - type: Mail
+ isFragile: true
+ isPriority: true
+ contents:
+ - id: UnstableMutagenChemistryBottle
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailBotanistSeeds
+ suffix: seeds
+ components:
+ - type: Mail
+ contents:
+ - id: AloeSeeds
+ orGroup: Seeds
+ - id: AmbrosiaVulgarisSeeds
+ orGroup: Seeds
+ - id: AppleSeeds
+ orGroup: Seeds
+ - id: BananaSeeds
+ orGroup: Seeds
+ - id: CarrotSeeds
+ orGroup: Seeds
+ - id: ChanterelleSeeds
+ orGroup: Seeds
+ - id: ChiliSeeds
+ orGroup: Seeds
+ - id: CornSeeds
+ orGroup: Seeds
+ - id: EggplantSeeds
+ orGroup: Seeds
+ - id: GalaxythistleSeeds
+ orGroup: Seeds
+ - id: LemonSeeds
+ orGroup: Seeds
+ - id: LingzhiSeeds
+ orGroup: Seeds
+ - id: OatSeeds
+ orGroup: Seeds
+ - id: OnionSeeds
+ orGroup: Seeds
+ - id: PoppySeeds
+ orGroup: Seeds
+ - id: PotatoSeeds
+ orGroup: Seeds
+ - id: SugarcaneSeeds
+ orGroup: Seeds
+ - id: TomatoSeeds
+ orGroup: Seeds
+ - id: TowercapSeeds
+ orGroup: Seeds
+ - id: WheatSeeds
+ orGroup: Seeds
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailClownGildedBikeHorn
+ suffix: honk
+ components:
+ - type: Mail
+ isFragile: true
+ contents:
+ - id: BikeHornInstrument
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailClownHonkSupplement
+ suffix: honk
+ components:
+ - type: Mail
+ isFragile: true
+ contents:
+ - id: BikeHorn
+ - id: FoodPieBananaCream
+ - id: FoodBanana
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailHoPBureaucracy
+ suffix: hoppaper
+ components:
+ - type: Mail
+ contents:
+ - id: Paper
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailHoPSupplement
+ suffix: hopsupplement
+ components:
+ - type: Mail
+ contents:
+ - id: ClearPDA
+ - id: ClothingHeadsetGrey
+ - id: Paper
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMimeArtsCrafts
+ suffix: artscrafts
+ components:
+ - type: Mail
+ contents:
+ - id: CrayonBox
+ - id: Paper
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMimeBlankBook
+ suffix: blankbook
+ components:
+ - type: Mail
+ contents:
+ - id: BookRandom
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMimeBottleOfNothing
+ suffix: bottleofnothing
+ components:
+ - type: Mail
+ contents:
+ - id: DrinkBottleOfNothingFull
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMusicianInstrumentSmall
+ suffix: instrument-small
+ components:
+ - type: Mail
+ isFragile: true
+ contents:
+ - id: FluteInstrument
+ orGroup: Instrument
+ - id: HarmonicaInstrument
+ orGroup: Instrument
+ - id: OcarinaInstrument
+ orGroup: Instrument
+ - id: PanFluteInstrument
+ orGroup: Instrument
+ - id: RecorderInstrument
+ orGroup: Instrument
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailPassengerMoney
+ suffix: passengermoney
+ components:
+ - type: Mail
+ contents:
+ - id: SpaceCash100
+ orGroup: Cash
+ prob: 0.1
+ maxAmount: 10
+ - id: SpaceCash500
+ orGroup: Cash
+ prob: 0.3
+ maxAmount: 5
+ - id: SpaceCash1000
+ orGroup: Cash
+ prob: 0.6
+ maxAmount: 3
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_command.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_command.yml
new file mode 100644
index 00000000000..7e2a935f908
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_command.yml
@@ -0,0 +1,9 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailCommandPinpointerNuclear
+ suffix: pinpointernuclear
+ components:
+ - type: Mail
+ contents:
+ - id: PinpointerNuclear
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_engineering.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_engineering.yml
new file mode 100644
index 00000000000..461d9bf1365
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_engineering.yml
@@ -0,0 +1,45 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEngineeringCables
+ suffix: cables
+ components:
+ - type: Mail
+ contents:
+ - id: CableHVStack
+ orGroup: Cables
+ - id: CableMVStack
+ orGroup: Cables
+ - id: CableApcStack
+ orGroup: Cables
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEngineeringKudzuDeterrent
+ suffix: antikudzu
+ components:
+ - type: Mail
+ contents:
+ - id: PlantBGoneSpray
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEngineeringSheetGlass
+ suffix: sheetglass
+ components:
+ - type: Mail
+ contents:
+ - id: SheetGlass
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEngineeringWelderReplacement
+ suffix: welder
+ components:
+ - type: Mail
+ contents:
+ - id: Welder
+
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_epistemology.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_epistemology.yml
new file mode 100644
index 00000000000..5fefd38fb48
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_epistemology.yml
@@ -0,0 +1,58 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEpistemologyBluespace
+ suffix: bluespace
+ components:
+ - type: Mail
+ contents:
+ - id: MaterialBluespace1
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEpistemologyIngotGold
+ suffix: ingotgold
+ components:
+ - type: Mail
+ contents:
+ - id: IngotGold1
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEpistemologyResearchDisk
+ suffix: researchdisk
+ components:
+ - type: Mail
+ contents:
+ - id: ResearchDisk
+ orGroup: Disk
+ prob: 0.6
+ - id: ResearchDisk5000
+ orGroup: Disk
+ prob: 0.3
+ - id: ResearchDisk10000
+ orGroup: Disk
+ prob: 0.1
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailEpistemologyTinfoilHat
+ suffix: tinfoilhat
+ components:
+ - type: Mail
+ contents:
+ - id: ClothingHeadTinfoil
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailForensicMantisForensicSupplement
+ suffix: forensicsupplement
+ components:
+ - type: Mail
+ contents:
+ - id: BoxForensicPad
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_medical.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_medical.yml
new file mode 100644
index 00000000000..fe8386bbe0f
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_medical.yml
@@ -0,0 +1,92 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalBasicSupplies
+ suffix: basicmedical
+ components:
+ - type: Mail
+ contents:
+ - id: Brutepack
+ maxAmount: 2
+ - id: Ointment
+ maxAmount: 2
+ - id: Gauze
+ maxAmount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalChemistrySupplement
+ suffix: chemsupp
+ components:
+ - type: Mail
+ contents:
+ - id: LargeBeaker
+ orGroup: Beaker
+ - id: Beaker
+ maxAmount: 3
+ orGroup: Beaker
+ - id: Syringe
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalEmergencyPens
+ suffix: medipens
+ components:
+ - type: Mail
+ contents:
+ - id: EmergencyMedipen
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalMedicinePills
+ suffix: medicinepills
+ components:
+ - type: Mail
+ contents:
+ - id: PillTricordrazine
+ maxAmount: 2
+ - id: PillDylovene
+ maxAmount: 2
+ - id: PillKelotane
+ maxAmount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalSheetPlasma
+ suffix: sheetplasma
+ components:
+ - type: Mail
+ contents:
+ - id: SheetPlasma1
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalSpaceacillin
+ suffix: spaceacillin
+ components:
+ - type: Mail
+ contents:
+ - id: SyringeSpaceacillin
+ maxAmount: 3
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailMedicalStabilizers
+ suffix: stabilizers
+ components:
+ - type: Mail
+ contents:
+ - id: PillDexalin
+ maxAmount: 2
+ - id: SyringeInaprovaline
+ maxAmount: 2
+ - id: SyringeTranexamicAcid
+ maxAmount: 2
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_security.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_security.yml
new file mode 100644
index 00000000000..b0f041eb7be
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_security.yml
@@ -0,0 +1,55 @@
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSecurityDonuts
+ suffix: donuts
+ components:
+ - type: Mail
+ contents:
+ - id: FoodBoxDonut
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSecurityFlashlight
+ suffix: seclite
+ components:
+ - type: Mail
+ contents:
+ - id: FlashlightSeclite
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSecurityNonlethalsKit
+ suffix: nonlethalskit
+ components:
+ - type: Mail
+ contents:
+ - id: Flash
+ maxAmount: 2
+ - id: GrenadeFlashBang
+ maxAmount: 2
+ - id: Handcuffs
+ maxAmount: 2
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailSecuritySpaceLaw
+ suffix: spacelaw
+ components:
+ - type: Mail
+ contents:
+ - id: HyperlinkBookSpaceLaw
+
+- type: entity
+ noSpawn: true
+ parent: BaseMail
+ id: MailWardenCrowdControl
+ suffix: crowdcontrol
+ components:
+ - type: Mail
+ contents:
+ - id: BoxBeanbag
+
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_specific_items.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_specific_items.yml
new file mode 100644
index 00000000000..b4d2b547798
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_specific_items.yml
@@ -0,0 +1,169 @@
+- type: entity
+ id: PaperMailCallForHelp1
+ noSpawn: true
+ suffix: "call for help 1"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Help! They're coming! Take this!
+
+- type: entity
+ id: PaperMailCallForHelp2
+ noSpawn: true
+ suffix: "call for help 2"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Check disposals!
+
+- type: entity
+ id: PaperMailCallForHelp3
+ noSpawn: true
+ suffix: "call for help 3"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ GET ME OUT!
+
+- type: entity
+ id: PaperMailCallForHelp4
+ noSpawn: true
+ suffix: "call for help 4"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Check maintenance!
+
+- type: entity
+ id: PaperMailCallForHelp5
+ noSpawn: true
+ suffix: "call for help 5"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Save me, please!
+
+- type: entity
+ id: PaperMailVagueThreat1
+ noSpawn: true
+ suffix: "vague mail threat 1"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ I know what you did. You don't know what I'm going to do to you.
+
+- type: entity
+ id: PaperMailVagueThreat2
+ noSpawn: true
+ suffix: "vague mail threat 2"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ I'm coming for you.
+
+- type: entity
+ id: PaperMailVagueThreat3
+ noSpawn: true
+ suffix: "vague mail threat 3"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ You're next.
+
+- type: entity
+ id: PaperMailVagueThreat4
+ noSpawn: true
+ suffix: "vague mail threat 4"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ We see you.
+
+- type: entity
+ id: PaperMailVagueThreat5
+ noSpawn: true
+ suffix: "vague mail threat 5"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ I hope your affairs are in order.
+
+- type: entity
+ id: PaperMailVagueThreat6
+ noSpawn: true
+ suffix: "vague mail threat 6"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ It's only a matter of time. Enjoy it while it lasts.
+
+- type: entity
+ id: PaperMailVagueThreat7
+ noSpawn: true
+ suffix: "vague mail threat 7"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Who should we mail your pieces to?
+
+- type: entity
+ id: PaperMailVagueThreat8
+ noSpawn: true
+ suffix: "vague mail threat 8"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Do you prefer to die slowly or quickly? Just kidding. We don't care what you think.
+
+- type: entity
+ id: PaperMailVagueThreat9
+ noSpawn: true
+ suffix: "vague mail threat 9"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ I think your head would look nice on my mantel.
+
+- type: entity
+ id: PaperMailVagueThreat10
+ noSpawn: true
+ suffix: "vague mail threat 10"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ You should have paid up. It's too late now.
+
+- type: entity
+ id: PaperMailVagueThreat11
+ noSpawn: true
+ suffix: "vague mail threat 11"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ Your family will miss you, but don't worry. We'll take care of them too.
+
+- type: entity
+ id: PaperMailVagueThreat12
+ noSpawn: true
+ suffix: "vague mail threat 12"
+ parent: Paper
+ components:
+ - type: Paper
+ content: |
+ I have a bet that you're going to die today. I'm not afraid of cheating.
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Stations/mail.yml b/Resources/Prototypes/Nyanotrasen/Entities/Stations/mail.yml
new file mode 100644
index 00000000000..ceb87bbaa1b
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Stations/mail.yml
@@ -0,0 +1,5 @@
+- type: entity
+ id: BaseStationMail
+ abstract: true
+ components:
+ - type: StationMailRouter
diff --git a/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml
new file mode 100644
index 00000000000..9bcaef52bbf
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/Entities/Structures/Machines/mailTeleporter.yml
@@ -0,0 +1,58 @@
+- type: entity
+ id: MailTeleporter
+ parent: BaseStructureDynamic
+ name: mail teleporter
+ description: Teleports mail addressed to the crew of this station.
+ components:
+ - type: MailTeleporter
+ - type: InteractionOutline
+ - type: Physics
+ bodyType: Static
+ - type: Transform
+ anchored: true
+ noRot: true
+ - type: Fixtures
+ fixtures:
+ fix1:
+ shape:
+ !type:PhysShapeAabb
+ bounds: "-0.45,-0.45,0.45,0.00"
+ density: 120
+ mask:
+ - HighImpassable
+ - type: Sprite
+ sprite: Nyanotrasen/Structures/mailbox.rsi
+ scale: 0.5, 0.5
+ layers:
+ - state: icon
+ - state: unlit
+ shader: unshaded
+ map: ["enum.PowerDeviceVisualLayers.Powered"]
+ - type: Damageable
+ damageContainer: Inorganic
+ damageModifierSet: Metallic
+ - type: Destructible
+ thresholds:
+ - trigger:
+ !type:DamageTrigger
+ damage: 75
+ behaviors:
+ - !type:SpawnEntitiesBehavior
+ spawn:
+ SheetSteel1:
+ min: 1
+ max: 1
+ - !type:DoActsBehavior
+ acts: ["Destruction"]
+ - type: ApcPowerReceiver
+ powerLoad: 1000 # TODO if we keep this make it spike power draw when teleporting
+ powerDisabled: true
+ - type: ExtensionCableReceiver
+ - type: Appearance
+ - type: GenericVisualizer
+ visuals:
+ enum.PowerDeviceVisuals.Powered:
+ enum.PowerDeviceVisualLayers.Powered:
+ True: {visible: true}
+ False: {visible: false}
+ - type: PowerSwitch
diff --git a/Resources/Prototypes/Nyanotrasen/mailDeliveries.yml b/Resources/Prototypes/Nyanotrasen/mailDeliveries.yml
new file mode 100644
index 00000000000..abc768f3637
--- /dev/null
+++ b/Resources/Prototypes/Nyanotrasen/mailDeliveries.yml
@@ -0,0 +1,118 @@
+- type: mailDeliveryPool
+ id: RandomMailDeliveryPool
+ everyone:
+ MailAlcohol: 0.5
+ MailSake: 0.5
+ MailBible: 1
+ MailBikeHorn: 0.5
+ MailBlockGameDIY: 1
+ MailCake: 1
+ MailCallForHelp: 0.6
+ MailCheese: 1
+ MailChocolate: 1
+ MailCigarettes: 0.5
+ MailCigars: 0.5
+ MailCookies: 1.1
+ MailCosplayArc: 0.5
+ MailCosplayGeisha: 0.5
+ MailCosplayMaid: 0.5
+ MailCosplayNurse: 0.5
+ MailCosplaySchoolgirl: 0.5
+ MailCosplayWizard: 0.5
+ MailCrayon: 1
+ MailFigurine: 1
+ MailFishingCap: 0.5
+ MailFlashlight: 1
+ MailFlowers: 1
+ MailHighlander: 0.12
+ MailHighlanderDulled: 1
+ MailHoneyBuns: 1
+ MailJunkFood: 1
+ MailKatana: 1
+ MailKnife: 1
+ MailMoney: 1
+ MailMuffins: 1.1
+ MailMoffins: 0.5
+ MailNoir: 0.5
+ MailPAI: 1
+ MailPlushie: 1
+ MailRestraints: 1
+ # MailSixPack: 0.5
+ MailSkub: 0.5
+ MailSoda: 1
+ MailSpaceVillainDIY: 1
+ MailSunglasses: 1
+ MailVagueThreat: 0.4
+ # This is mainly for Glacier.
+ MailWinterCoat: 1.5
+
+ # Department and job-specific mail can have slightly higher weights,
+ # since they'll be merged with the everyone pool.
+ departments:
+ Medical:
+ MailMedicalBasicSupplies: 2
+ MailMedicalChemistrySupplement: 2
+ MailMedicalEmergencyPens: 3
+ MailMedicalMedicinePills: 2
+ MailMedicalSheetPlasma: 1
+ MailMedicalSpaceacillin: 1
+ MailMedicalStabilizers: 2
+ Engineering:
+ MailAMEGuide: 1
+ MailEngineeringCables: 2
+ MailEngineeringKudzuDeterrent: 2
+ MailEngineeringSheetGlass: 2
+ MailEngineeringWelderReplacement: 2
+ Security:
+ MailSecurityDonuts: 3
+ MailSecurityFlashlight: 2
+ MailSecurityNonlethalsKit: 2
+ MailSecuritySpaceLaw: 1
+ Epistemology:
+ MailBooks: 1
+ MailEpistemologyBluespace: 1
+ MailEpistemologyIngotGold: 2
+ MailEpistemologyResearchDisk: 1
+ MailEpistemologyTinfoilHat: 1
+ MailSignallerKit: 1
+ # All heads of staff are in Command and not their departments, technically.
+ # So any items from the departments above that should also be sent to the
+ # respective department heads should be duplicated below.
+ Command:
+ MailCommandPinpointerNuclear: 0.5
+
+ jobs:
+ Botanist:
+ MailBotanistChemicalBottles: 2
+ MailBotanistMutagen: 1.5
+ MailBotanistSeeds: 1
+ ChiefEngineer:
+ MailEngineeringKudzuDeterrent: 2
+ ChiefMedicalOfficer:
+ MailMedicalEmergencyPens: 2
+ MailMedicalMedicinePills: 3
+ MailMedicalSheetPlasma: 2
+ Clown:
+ MailClownGildedBikeHorn: 0.5
+ MailClownHonkSupplement: 3
+ ForensicMantis:
+ MailForensicMantisForensicSupplement: 2
+ HeadOfPersonnel:
+ MailHoPBureaucracy: 2
+ MailHoPSupplement: 3
+ HeadOfSecurity:
+ MailSecurityNonlethalsKit: 2
+ Lawyer:
+ MailSecuritySpaceLaw: 2
+ Mime:
+ MailMimeArtsCrafts: 3
+ MailMimeBlankBook: 2
+ MailMimeBottleOfNothing: 1
+ Mystagogue:
+ MailEpistemologyIngotGold: 2
+ Musician:
+ MailMusicianInstrumentSmall: 1
+ Passenger:
+ MailPassengerMoney: 3
+ Warden:
+ MailWardenCrowdControl: 2
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/broken.png b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/broken.png
new file mode 100644
index 0000000000000000000000000000000000000000..afce59853e425ba1f3c17b4a637a359ee98b2779
GIT binary patch
literal 199
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnH3?%tPCZz)@mUKs7M+SzC{oH>NS%G}c0G|+7
zAl=i|HD$__?r*0PfE>n>AirP+hi5m^fSedl7sn6@N!EiKd0PT_SPj^1RZfYRAJCUn
zW@SF$Hl?T545)~K!9n=~he^CbD(k5O|E6)zDakdLe#UZ&o1v}o5YRaS4ow^E7xR5s
j{hA?yfxCrmAtP8*eU~}wj`GDD-h+6au6{1-oD!MNS%G|^0G|+7
zAl=F!Kd-WJo4Uu|3A0~r+TUjvl?D`LED7=pW^j0R1H{qtba4#PIG>y#!OG0Z;^@HO
w>0kl`E{vWIM{XRsaTCb*;N%c_@x+ed{c7fuuJe5hfw~wxUHx3vIVCg!0Hxq7Hvj+t
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/icon.png b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..bcfe67ceda767e67d6b7c9eb6a90cb5c9082c80b
GIT binary patch
literal 277
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv&H|6fVg?4jBOuH;Rhv&5D5w?S
z6XFV_je#KTjIXb6+RQUErcGP0V8QC;%a0#FekSez)2B~Aefsp{$B+O2|4&xDA_G*-
zSQ6wH%;50sMjDXQ;_2cTqH#VsVF7=Fw=)x`LgTv7f(=YM7dA?>m6bNI?n*yb7w@Ep9rV0BcQfouMPI_8t|
RYC!iec)I$ztaD0e0sz(CWmEtF
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/locked.png b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/locked.png
new file mode 100644
index 0000000000000000000000000000000000000000..8292644fb14b8da0cafa9f6dcc3a81a7e820f6d5
GIT binary patch
literal 144
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnH3?%tPCZz)@mUKs7M+SzC{oH>NS%G}c0G|+7
zAPoeTo|f0ry=Mbij3q&S!3+-1ZlnP@s-7;6ArhC96BNV?Oamq#+>&*5)0)uLQVu;+
iS1;h{;1GSQB+I~HY9c1{pEq+S$RJNwKbLh*2~7ZQk0dbw
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/meta.json b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/meta.json
new file mode 100644
index 00000000000..49771ce1a00
--- /dev/null
+++ b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/meta.json
@@ -0,0 +1,35 @@
+{
+ "version": 1,
+ "license": "CC-BY-SA-3.0",
+ "copyright": "Taken from tgstation at https://github.com/tgstation/tgstation/commit/40d89d11ea4a5cb81d61dc1018b46f4e7d32c62a",
+ "size": {
+ "x": 32,
+ "y": 32
+ },
+ "states": [
+ {
+ "name": "icon"
+ },
+ {
+ "name": "locked"
+ },
+ {
+ "name": "trash"
+ },
+ {
+ "name": "fragile"
+ },
+ {
+ "name": "priority"
+ },
+ {
+ "name": "priority_inactive"
+ },
+ {
+ "name": "broken"
+ },
+ {
+ "name": "postmark"
+ }
+ ]
+}
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/postmark.png b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/postmark.png
new file mode 100644
index 0000000000000000000000000000000000000000..5d55ab0b15ef1f8a0348ee9f6f2685f6548d0300
GIT binary patch
literal 133
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnL3?x0byx0z;SkfJR9T^xl_H+M9WCils0(?ST
zfwVEh8NnGjbAc?Tk|4iehV=%&ih=4FWISCQLpZJ{CoE8lc*>yCNS%G}c0G|+7
zAicYj<^OHPfaB#$fgHw?AirP+hi5m^fE*1^7sn8Z%gG4};v1|MbZ-b$IHaMWp`f9`
lk)olg2}FvW0U1WA3=F=!EWCNS%G}c0G|+7
zARXhQ)LUnmHbY+l$YCrA@(X5gcy=QV$kFh0aSV~ToSdK_zQJli_l7`)LmC@XJAOZjY
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/trash.png b/Resources/Textures/Nyanotrasen/Objects/Specific/Mail/mail.rsi/trash.png
new file mode 100644
index 0000000000000000000000000000000000000000..1faadffe1de09d1473167a28753bc127a2481b59
GIT binary patch
literal 305
zcmeAS@N?(olHy`uVBq!ia0vp^3LwnF3?v&v(vJfv&H|6fVg?4jBOuH;Rhv&5D5w?S
z6XFV_je#KTjIXb6+RQUErcGP0V8QC;%a0#FekSez)2B~Aefsp{$B+O2|4&xDA_G*-
zSQ6wH%;50sMjDW_z|+MsMB{vN!UDboX2Hx%9v;qs#y4A7%(mU#efiQhN6|O;_Uc+!
zn+xPLYn)`-RF*!Uu_^ZS0j^!;hYzfi-T(f++UoE3
wtQ~X{8ow1;tYF}7adM1`ippSMu4Q1j$hiFF-eupsfNo{*boFyt=akR{08<8cEdT%j
literal 0
HcmV?d00001
diff --git a/Resources/Textures/Nyanotrasen/Structures/mailbox.rsi/icon.png b/Resources/Textures/Nyanotrasen/Structures/mailbox.rsi/icon.png
new file mode 100644
index 0000000000000000000000000000000000000000..1bc406ee38da4b8ed5a9a0e89da1b3467f43a36d
GIT binary patch
literal 2212
zcmV;V2wV4wP)Px-UP(kjRCr$PT3cuw*BSoK%S%?E0p-mTcE>10Y*3JUi0MwoCDkeIL$*7s6VZC
zSt_P#GZ+DmJgP{e2>iO(j!ruVKqzd%Zfos!_SrZ#)DJWRz_J{bP!ZE-3++tYc>r9e
zo%P!vYnVVY05pra$Hva7vB7VBLc2fyzfTtH%r}00w~4a@_~eYc)TREdZ}o*1KfK7{G=`EMBl0
z0Z@u(@g(?CdcAuRPBJiQkN8=g2
zqmwuexO7j$N)f^{zc&@8A--gS#DI~n0GW4zoyTM`Q`;pKKO{2=t%@KjhO}F!+gwRU
zf|IZUKq`hbc#gzF*G~E~Juj~`7DN8j0%ePYPZy?<96zaqr~=g$sVb@jMP;|aOT6!4
zQZNFrJ_$&7RD%)#UYG$p^pvEqi20l&7?oL}TV>-1CQ!9)R2&D6`T-sWOiamnN?DNbUqAe1
z6ia24DrJ0lbw+8j#Zn&OYfq_Xz&TQhga9HG4B_&BuK)x>cv#M*O@PAR2I=1c0B8yc
z0!EKZn=j=6GsKVHdziid(M1j>Iv~myC=p5XtpsnzN6;S_-F`P_W@m^^K%a89+5|Qj
z|MgPqj5%ufnjN9CB{W0MuDmFyI)3kAT)6T{gGlFo_kEl?^9J5}@3I7j>_Gj!_}AxI
zYTFRvu`t--XX@{9j$~pB(&?+xx5QJ@QEC7-=>0nk001h7Dcr5h;mDWnL#a~1#cS6Z
z04SXMDHiiZJpIxi`9IEms2M2Kye0quwhRuSFAzZ5x&lP+23%W~!=IxS)_DLTaZB|4
zigP$Pc0}5J(?tM~hG*w;nEKJ5`0H=~Te9F50MPSo!$X+QTm@p=0mmjMnDp5Gaywlh
z@3uN*0>pqpOLS|B8I0|lSfzbhR0BYt)AM=!;N{o(@BjXu*u%xYoI*Gm2R%}VJU0Fgmy0>w1;>^_L;w5I{0Y~2e0ZDcY38BU#k1Hbz7
zKW_=ZaDNQh8?zDsV!-v!wr->gd`AG-J$eA2d^)$L3D~v^(`3T`<(5|P!Y}`Zryslj
zrVJQ}L{P}46$5Nwd43SnXE#j!+u#H03>e+HA2W0FUQaH(y=K7M?|+O)IEecW?i3(6
zj*Gd4g76QkqJWwu9tmS9o2fJ4{A^#lNzW>c)LSpD9<{i4vx5N^W97M
z$#37}&rcp>_aFIU4FtoGtY`>>lu~n!cr@Asz|Y$=-!(*RZ<4Xe5x92icSIB*AMhz|
z#Q*}}!6(n)(B2XJ?Ab@~&95G)RasF-xwDl2mmNuuAMEq
zh{yV|v|xe7wt-WT^{-d_0Dxe8G=x2&Fp9BI4G6j&45mf-TUrD~fl{@K^a7>D^~>u6
z9wY#2y#s(ic<%8B*oosu5j^@muLDIq8i8q;$Sur*MH1k|j`OGe0zmA|04BrOkqyHg
zHUtnxA|p(ok`;Y`RB{)rg$0lJtd$-4UF65Pc(E7=-$N>Zn@@P!Z-U!OW5N}6_Ugh1I{v5ODNR3@0FU)43Z>fB>lbb(O&>KqxwhF1hzypvi}ku#AI~841Fumbl0T
zR14jEF3@!h7@y*l1mW_l9r4bM9@Fz}-wBRLq6nhE&A8
zoqCb?uz?6uu4eF)<-hx{&i4^K8T;s^%h6N@=LZ|UYyB_#`Cp%TLc=GPIS!T#aZP>)
zIH#yTU^&J3L9~F$Z-EV?dH`R;`Uc?wrl*`1jYSM?agEQ5ul@b&JuA&;{_8b=&&^>~
zwoKkNk737F&z+J?YO)8@B^hKY=FDL^63Qe!iNWH7g(YXlY8EpU#)1chMM47MoH-s$
z4-PzjqN1=)=*9$=1C7T|dOF0YZcyZGVD6thxnYM_g0m03F4lwK>p}44$rjF6*2UngHb3X&nFn
literal 0
HcmV?d00001