From 3d83d666e3c7f823231b5c12151065afe3ba5853 Mon Sep 17 00:00:00 2001 From: Debug <49997488+DebugOk@users.noreply.github.com> Date: Sun, 1 Oct 2023 18:25:32 +0200 Subject: [PATCH] Mail! (#16) * Add Mail!!111!! * Update MailSystem.cs * Make mailbag mail only * Remove invalid mail types * Update MailSystem.cs * Enable wizard cosplay mail --- .../Nyanotrasen/Mail/MailComponent.cs | 8 + Content.Client/Nyanotrasen/Mail/MailSystem.cs | 59 ++ .../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 | 743 +++++++++++++++++ .../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 | 59 ++ .../Objects/Specific/Mail/mail_medical.yml | 93 +++ .../Objects/Specific/Mail/mail_security.yml | 56 ++ .../Specific/Mail/mail_specific_items.yml | 169 ++++ .../Entities/Objects/Specific/Mail/tools.yml | 6 +- .../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 38 files changed, 2975 insertions(+), 3 deletions(-) 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..fd03524934d --- /dev/null +++ b/Content.Client/Nyanotrasen/Mail/MailSystem.cs @@ -0,0 +1,59 @@ +using Robust.Client.GameObjects; +using Content.Shared.Mail; +using Content.Shared.StatusIcon; +using Content.Shared.StatusIcon.Components; +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..3a3470a44a5 --- /dev/null +++ b/Content.Server/Nyanotrasen/Mail/MailSystem.cs @@ -0,0 +1,743 @@ +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.Nutrition.EntitySystems; +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 OpenableSystem _openable = 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 var openable) + && !_openable.IsClosed(uid, null, openable) + && _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..73d0af4ad2a --- /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: PlushieMothRandom + orGroup: Plushie + - id: PlushieMoth + orGroup: Plushie + - id: PlushieMothMusician + prob: 0.5 + orGroup: Plushie + - id: PlushieMothBartender + 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..bdf50f5edf8 --- /dev/null +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_epistemology.yml @@ -0,0 +1,59 @@ +#- type: entity +# noSpawn: true +# parent: BaseMail +# id: MailEpistemologyBluespace +# suffix: bluespace +# components: +# - type: Mail +# contents: +# - id: MaterialBluespace1 +# Awaiting bluespace to be implemented + +- 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..4e797272e5e --- /dev/null +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_medical.yml @@ -0,0 +1,93 @@ +- 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 +# Awaiting diseases + +- 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..b47d5af56e3 --- /dev/null +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/mail_security.yml @@ -0,0 +1,56 @@ +- 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 +# Will we ever readd hyperlinks books? Who knows! + +- 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/Objects/Specific/Mail/tools.yml b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/tools.yml index c934f181c5c..b628ed4d526 100644 --- a/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/tools.yml +++ b/Resources/Prototypes/Nyanotrasen/Entities/Objects/Specific/Mail/tools.yml @@ -17,6 +17,6 @@ - type: Storage capacity: 120 quickInsert: true -# whitelist: -# components: -# - Mail # Awaiting actual Mail stuff + whitelist: + components: + - Mail 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..b67a52079df --- /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