mail refactor + predicted mail (#5031)

* scope? i don't know her

* more cleanup, Entity<T>

* priority mail timer, shared dependencies

* MailSystem file-scoped namespace

* change FTL text

* TODOs

* mailteleporter file-scoped namespace

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

* remove the evil nyano mail

* Update Content.Shared/_DV/Mail/MailTeleporterComponent.cs

Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
Signed-off-by: Milon <plmilonpl@gmail.com>

---------

Signed-off-by: Milon <plmilonpl@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Vanessa <908648+ShepardToTheStars@users.noreply.github.com>
This commit is contained in:
Milon 2025-12-27 14:27:24 +01:00 committed by GitHub
parent 1835d7678f
commit 5c527dc6a2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1265 additions and 1293 deletions

View File

@ -1,6 +1,6 @@
using Robust.Client.UserInterface;
using Content.Client.UserInterface.Fragments;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared._DV.CartridgeLoader.Cartridges;
namespace Content.Client._DV.CartridgeLoader.Cartridges;

View File

@ -1,6 +1,5 @@
<cartridges:MailMetricUiFragment
xmlns:cartridges="clr-namespace:Content.Client._DV.CartridgeLoader.Cartridges"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
xmlns:controls="clr-namespace:Content.Client.UserInterface.Controls"
xmlns="https://spacestation14.io"
Margin="5"

View File

@ -1,4 +1,4 @@
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Robust.Client.AutoGenerated;
using Robust.Client.UserInterface.Controls;
using Robust.Client.UserInterface.XAML;
@ -96,7 +96,7 @@ public sealed partial class MailMetricUiFragment : BoxContainer
}
}
enum OpenedMailPercentGrade
enum OpenedMailPercentGrade : byte
{
Good,
Average,

View File

@ -1,9 +0,0 @@
using Content.Shared._DV.Mail;
namespace Content.Client._DV.Mail
{
[RegisterComponent]
public sealed partial class MailComponent : SharedMailComponent
{
}
}

View File

@ -0,0 +1,61 @@
using Content.Shared._DV.Mail;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Mail;
/// <summary>
/// Display a cool stamp on the parcel based on the job of the recipient.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public sealed class MailJobVisualizerSystem : VisualizerSystem<MailComponent>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
private static readonly ProtoId<JobIconPrototype> UnknownJobIcon = "JobIconUnknown";
protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
_appearance.TryGetData(uid, MailVisuals.JobIcon, out string job, args.Component);
ProtoId<JobIconPrototype> jobProtoId = string.IsNullOrEmpty(job) ? UnknownJobIcon : job;
if (!_prototypeManager.TryIndex(jobProtoId, out var icon))
{
_spriteSystem.LayerSetTexture((uid, args.Sprite), MailVisualLayers.JobStamp, _spriteSystem.Frame0(_prototypeManager.Index(UnknownJobIcon).Icon));
return;
}
_spriteSystem.LayerSetTexture((uid, args.Sprite), MailVisualLayers.JobStamp, _spriteSystem.Frame0(icon.Icon));
}
}
public enum MailVisualLayers : byte
{
Icon,
Lock,
FragileStamp,
JobStamp,
PriorityTape,
Breakage
}

View File

@ -1,60 +1,5 @@
using Content.Shared._DV.Mail;
using Content.Shared.StatusIcon;
using Robust.Client.GameObjects;
using Robust.Shared.Prototypes;
namespace Content.Client._DV.Mail;
/// <summary>
/// Display a cool stamp on the parcel based on the job of the recipient.
/// </summary>
/// <remarks>
/// 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
/// </remarks>
public sealed class MailJobVisualizerSystem : VisualizerSystem<MailComponent>
{
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SpriteSystem _spriteSystem = default!;
protected override void OnAppearanceChange(EntityUid uid, MailComponent component, ref AppearanceChangeEvent args)
{
if (args.Sprite == null)
return;
_appearance.TryGetData(uid, MailVisuals.JobIcon, out string job, args.Component);
if (string.IsNullOrEmpty(job))
job = "JobIconUnknown";
if (!_prototypeManager.TryIndex<JobIconPrototype>(job, out var icon))
{
args.Sprite.LayerSetTexture(MailVisualLayers.JobStamp, _spriteSystem.Frame0(_prototypeManager.Index("JobIconUnknown")));
return;
}
args.Sprite.LayerSetTexture(MailVisualLayers.JobStamp, _spriteSystem.Frame0(icon.Icon));
}
}
public enum MailVisualLayers : byte
{
Icon,
Lock,
FragileStamp,
JobStamp,
PriorityTape,
Breakage
}
public sealed class MailSystem : SharedMailSystem;

View File

@ -1,11 +1,7 @@
namespace Content.Server._DV.CartridgeLoader.Cartridges;
/// <summary>
/// Only used for tracking the MailMetrics PDAs.
/// </summary>
[RegisterComponent, Access(typeof(MailMetricsCartridgeSystem))]
public sealed partial class MailMetricsCartridgeComponent : Component
{
/// <summary>
/// Station entity keeping track of logistics stats
/// </summary>
[DataField]
public EntityUid? Station;
}
public sealed partial class MailMetricsCartridgeComponent : Component;

View File

@ -1,10 +1,11 @@
using Content.Server._DV.Cargo.Components;
using Content.Server._DV.Cargo.Systems;
using Content.Server.Station.Systems;
using Content.Server.CartridgeLoader;
using Content.Shared._DV.CartridgeLoader.Cartridges;
using Content.Shared._DV.Cargo.Components;
using Content.Shared._DV.Cargo.Systems;
using Content.Shared._DV.Mail;
using Content.Shared.CartridgeLoader;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Server._DV.Mail.Components;
using Content.Shared.Station.Components;
namespace Content.Server._DV.CartridgeLoader.Cartridges;
@ -27,7 +28,7 @@ public sealed class MailMetricsCartridgeSystem : EntitySystem
UpdateUI(ent, args.Loader);
}
private void OnLogisticsStatsUpdated(LogisticStatsUpdatedEvent args)
private void OnLogisticsStatsUpdated(ref LogisticStatsUpdatedEvent args)
{
UpdateAllCartridges(args.Station);
}
@ -40,25 +41,25 @@ public sealed class MailMetricsCartridgeSystem : EntitySystem
private void UpdateAllCartridges(EntityUid station)
{
var query = EntityQueryEnumerator<MailMetricsCartridgeComponent, CartridgeComponent>();
while (query.MoveNext(out var uid, out var comp, out var cartridge))
var query = EntityQueryEnumerator<MailMetricsCartridgeComponent, CartridgeComponent, StationTrackerComponent>();
while (query.MoveNext(out var uid, out var comp, out var cartridge, out var stationTracker))
{
if (cartridge.LoaderUid is not { } loader || comp.Station != station)
if (cartridge.LoaderUid is not { } loader || stationTracker.Station != station)
continue;
UpdateUI((uid, comp), loader);
UpdateUI((uid, comp, stationTracker), loader);
}
}
private void UpdateUI(Entity<MailMetricsCartridgeComponent> ent, EntityUid loader)
private void UpdateUI(Entity<MailMetricsCartridgeComponent, StationTrackerComponent?> ent, EntityUid loader)
{
if (_station.GetOwningStation(loader) is { } station)
ent.Comp.Station = station;
if (!Resolve(ent, ref ent.Comp2))
return;
if (!TryComp<StationLogisticStatsComponent>(ent.Comp.Station, out var logiStats))
if (!TryComp<StationLogisticStatsComponent>(ent.Comp2.Station, out var logiStats))
return;
// Get station's logistic stats
var unopenedMailCount = GetUnopenedMailCount(ent.Comp.Station);
var unopenedMailCount = GetUnopenedMailCount(ent.Comp2.Station);
// Send logistic stats to cartridge client
var state = new MailMetricUiState(logiStats.Metrics, unopenedMailCount);

View File

@ -1,113 +0,0 @@
using System.Threading;
using Robust.Shared.Audio;
using Content.Shared.Storage;
using Content.Shared._DV.Mail;
namespace Content.Server._DV.Mail.Components
{
[RegisterComponent]
public sealed partial class MailComponent : SharedMailComponent
{
[DataField]
public string Recipient = "None";
[DataField]
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.
[DataField]
public bool IsLocked = true;
/// <summary>
/// Is this parcel profitable to deliver for the station?
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public bool IsProfitable = true;
/// <summary>
/// Is this package considered fragile?
/// </summary>
/// <remarks>
/// This can be set to true in the YAML files for a mail delivery to
/// always be Fragile, despite its contents.
/// </remarks>
[DataField]
public bool IsFragile = false;
/// <summary>
/// Is this package considered priority mail?
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public bool IsPriority = false;
// Frontier Mail Port: large mail
/// <summary>
/// Whether this parcel is large.
/// </summary>
[DataField]
public bool IsLarge = false;
// End Frontier: large mail
/// <summary>
/// What will be packaged when the mail is spawned.
/// </summary>
[DataField]
public List<EntitySpawnEntry> Contents = new();
/// <summary>
/// The amount that cargo will be awarded for delivering this mail.
/// </summary>
[DataField]
public int Bounty = 750;
/// <summary>
/// Penalty if the mail is destroyed.
/// </summary>
[DataField]
public int Penalty = -250;
/// <summary>
/// The sound that's played when the mail's lock is broken.
/// </summary>
[DataField]
public SoundSpecifier PenaltySound = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The sound that's played when the mail's opened.
/// </summary>
[DataField]
public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/packetrip.ogg");
/// <summary>
/// The sound that's played when the mail's lock has been emagged.
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
/// <summary>
/// Whether this component is enabled.
/// Removed when it becomes trash.
/// </summary>
public bool IsEnabled = true;
public CancellationTokenSource? PriorityCancelToken;
}
}

View File

@ -1,7 +0,0 @@
namespace Content.Server._DV.Mail.Components
{
[RegisterComponent]
public sealed partial class MailReceiverComponent : Component
{
}
}

View File

@ -1,136 +0,0 @@
using Content.Shared.Radio;
using Robust.Shared.Audio;
using Robust.Shared.Prototypes;
namespace Content.Server._DV.Mail.Components
{
/// <summary>
/// This is for the mail teleporter.
/// Random mail will be teleported to this every few minutes.
/// </summary>
[RegisterComponent]
public sealed partial class MailTeleporterComponent : Component
{
// Not starting accumulator at 0 so mail carriers have some deliveries to make shortly after roundstart.
[DataField]
public float Accumulator = 285f;
[DataField]
public TimeSpan TeleportInterval = TimeSpan.FromMinutes(5);
/// <summary>
/// The sound that's played when new mail arrives.
/// </summary>
[DataField]
public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
/// <summary>
/// The MailDeliveryPoolPrototype that's used to select what mail this
/// teleporter can deliver.
/// </summary>
[DataField]
public string MailPool = "RandomDeltaVMailDeliveryPool"; // Frontier / DeltaV: Mail rework
/// <summary>
/// Imp. Whether or not the telepad should output a message upon recieving mail.
/// </summary>
[DataField]
public bool RadioNotification = false;
[DataField]
public LocId ShipmentReceivedMessage = "mail-received-message";
[DataField]
public ProtoId<RadioChannelPrototype> RadioChannel = "Supply";
/// <summary>
/// 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.
/// </summary>
[DataField]
public int CandidatesPerDelivery = 8;
[DataField]
public int MinimumDeliveriesPerTeleport = 1;
/// <summary>
/// Do not teleport any more mail in, if there are at least this many
/// undelivered parcels.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public int MaximumUndeliveredParcels = 5;
/// <summary>
/// Any item that breaks or is destroyed in less than this amount of
/// damage is one of the types of items considered fragile.
/// </summary>
[DataField]
public int FragileDamageThreshold = 10;
/// <summary>
/// What's the bonus for delivering a fragile package intact?
/// </summary>
[DataField]
public int FragileBonus = 100;
/// <summary>
/// What's the malus for failing to deliver a fragile package?
/// </summary>
[DataField]
public int FragileMalus = -100;
/// <summary>
/// What's the chance for any one delivery to be marked as priority mail?
/// </summary>
[DataField]
public float PriorityChance = 0.1f;
/// <summary>
/// How long until a priority delivery is considered as having failed
/// if not delivered?
/// </summary>
[DataField]
public TimeSpan PriorityDuration = TimeSpan.FromMinutes(5);
/// <summary>
/// What's the bonus for delivering a priority package on time?
/// </summary>
[DataField]
public int PriorityBonus = 250;
/// <summary>
/// What's the malus for failing to deliver a priority package?
/// </summary>
[DataField]
public int PriorityMalus = -250;
// Frontier: Large mail
/// <summary>
/// What's the bonus for delivering a large package intact?
/// </summary>
[DataField]
public int LargeBonus = 1500; // DeltaV; 5000 to 1500
/// <summary>
/// What's the malus for failing to deliver a large package?
/// </summary>
[DataField]
public int LargeMalus = -500; // DeltaV; -250 to -500
// End Frontier: Large mail
}
}

View File

@ -4,6 +4,4 @@ namespace Content.Server._DV.Mail.Components;
/// Designates a station as a place for sending and receiving mail.
/// </summary>
[RegisterComponent]
public sealed partial class StationMailRouterComponent : Component
{
}
public sealed partial class StationMailRouterComponent : Component;

File diff suppressed because it is too large Load Diff

View File

@ -4,8 +4,9 @@ using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Content.Shared.Administration;
using Content.Server.Administration;
using Content.Server._DV.Mail.Components;
using Content.Server._DV.Mail.EntitySystems;
using Content.Shared._DV.Mail;
using Robust.Shared.Timing;
namespace Content.Server._DV.Mail;
@ -19,6 +20,7 @@ public sealed class MailToCommand : IConsoleCommand
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
private const string BlankMailPrototype = "MailAdminFun";
private const string BlankLargeMailPrototype = "MailLargeAdminFun"; // Frontier: large mail
@ -124,7 +126,7 @@ public sealed class MailToCommand : IConsoleCommand
var teleporterQueue = containerSystem.EnsureContainer<Container>((EntityUid)teleporterUid, "queued");
containerSystem.Insert(mailUid, teleporterQueue);
shell.WriteLine(Loc.GetString("command-mailto-success", ("timeToTeleport", teleporterComponent.TeleportInterval.TotalSeconds - teleporterComponent.Accumulator)));
shell.WriteLine(Loc.GetString("command-mailto-success", ("timeToTeleport", teleporterComponent.NextDelivery - _timing.CurTime)));
}
}
@ -136,12 +138,13 @@ public sealed class MailNowCommand : IConsoleCommand
public string Help => Loc.GetString("command-mailnow-help", ("command", Command));
[Dependency] private readonly IEntityManager _entityManager = default!;
[Dependency] private readonly IGameTiming _timing = default!;
public async void Execute(IConsoleShell shell, string argStr, string[] args)
{
foreach (var mailTeleporter in _entityManager.EntityQuery<MailTeleporterComponent>())
{
mailTeleporter.Accumulator += (float) mailTeleporter.TeleportInterval.TotalSeconds - mailTeleporter.Accumulator;
mailTeleporter.NextDelivery = _timing.CurTime;
}
shell.WriteLine(Loc.GetString("command-mailnow-success"));

View File

@ -1,38 +0,0 @@
namespace Content.Server._DV.Mail
{
/// <summary>
/// A set of localized strings related to mail entities
/// </summary>
public struct MailEntityStrings
{
public string NameAddressed;
public string DescClose;
public string DescFar;
}
/// <summary>
/// Constants related to mail.
/// </summary>
public sealed class MailConstants : EntitySystem
{
/// <summary>
/// Locale strings related to small parcels.
/// </summary>
public static readonly MailEntityStrings Mail = new()
{
NameAddressed = "mail-item-name-addressed",
DescClose = "mail-desc-close",
DescFar = "mail-desc-far",
};
/// <summary>
/// Locale strings related to large packages.
/// </summary>
public static readonly MailEntityStrings MailLarge = new()
{
NameAddressed = "mail-large-item-name-addressed",
DescClose = "mail-large-desc-close",
DescFar = "mail-large-desc-far",
};
}
}

View File

@ -1,8 +1,7 @@
using Content.Server._DV.Cargo.Systems;
using Content.Shared.Cargo;
using Content.Shared.CartridgeLoader.Cartridges;
using Content.Shared._DV.Cargo.Systems;
using Content.Shared._DV.CartridgeLoader.Cartridges;
namespace Content.Server._DV.Cargo.Components;
namespace Content.Shared._DV.Cargo.Components;
/// <summary>
/// Added to the abstract representation of a station to track stats related to mail delivery and income
@ -11,5 +10,5 @@ namespace Content.Server._DV.Cargo.Components;
public sealed partial class StationLogisticStatsComponent : Component
{
[DataField]
public MailStats Metrics { get; set; }
public MailStats Metrics;
}

View File

@ -1,16 +1,10 @@
using Content.Server._DV.Cargo.Components;
using Content.Shared.Cargo;
using Content.Shared._DV.Cargo.Components;
using JetBrains.Annotations;
namespace Content.Server._DV.Cargo.Systems;
namespace Content.Shared._DV.Cargo.Systems;
public sealed partial class LogisticStatsSystem : EntitySystem
{
public override void Initialize()
{
base.Initialize();
}
[PublicAPI]
public void AddOpenedMailEarnings(EntityUid uid, StationLogisticStatsComponent component, int earnedMoney)
{
@ -55,14 +49,16 @@ public sealed partial class LogisticStatsSystem : EntitySystem
UpdateLogisticsStats(uid);
}
private void UpdateLogisticsStats(EntityUid uid) => RaiseLocalEvent(new LogisticStatsUpdatedEvent(uid));
}
public sealed class LogisticStatsUpdatedEvent : EntityEventArgs
{
public EntityUid Station;
public LogisticStatsUpdatedEvent(EntityUid station)
private void UpdateLogisticsStats(EntityUid uid)
{
Station = station;
var ev = new LogisticStatsUpdatedEvent(uid);
RaiseLocalEvent(uid, ref ev);
}
}
[ByRefEvent]
public record struct LogisticStatsUpdatedEvent(EntityUid Station)
{
public EntityUid Station = Station;
public bool Handled = false;
}

View File

@ -1,6 +1,6 @@
using Robust.Shared.Serialization;
namespace Content.Shared.CartridgeLoader.Cartridges;
namespace Content.Shared._DV.CartridgeLoader.Cartridges;
[Serializable, NetSerializable]
public sealed class MailMetricUiState : BoundUserInterfaceState

View File

@ -0,0 +1,112 @@
using System.Threading;
using Content.Shared.Storage;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Mail;
[RegisterComponent, NetworkedComponent, AutoGenerateComponentState] // TODO: Access & PublicAPI for Commands
public sealed partial class MailComponent : Component
{
[DataField, AutoNetworkedField]
public string Recipient = "None";
[DataField, AutoNetworkedField]
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.
[DataField, AutoNetworkedField]
public bool IsLocked = true;
/// <summary>
/// Is this parcel profitable to deliver for the station?
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, AutoNetworkedField]
public bool IsProfitable = true;
/// <summary>
/// Is this package considered fragile?
/// </summary>
/// <remarks>
/// This can be set to true in the YAML files for a mail delivery to
/// always be Fragile, despite its contents.
/// </remarks>
[DataField, AutoNetworkedField]
public bool IsFragile;
/// <summary>
/// Is this package considered priority mail?
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField, AutoNetworkedField]
public bool IsPriority;
/// <summary>
/// The time when this priority mail expires if Priority.
/// </summary>
[DataField, AutoNetworkedField]
public TimeSpan? ExpiryTime;
// Frontier Mail Port: large mail
/// <summary>
/// Whether this parcel is large.
/// </summary>
[DataField, AutoNetworkedField]
public bool IsLarge;
// End Frontier: large mail
/// <summary>
/// What will be packaged when the mail is spawned.
/// </summary>
[DataField]
public List<EntitySpawnEntry> Contents = new();
/// <summary>
/// The amount that cargo will be awarded for delivering this mail.
/// </summary>
[DataField, AutoNetworkedField]
public int Bounty = 750;
/// <summary>
/// Penalty if the mail is destroyed.
/// </summary>
[DataField, AutoNetworkedField]
public int Penalty = -250;
/// <summary>
/// The sound that's played when the mail's lock is broken.
/// </summary>
[DataField]
public SoundSpecifier PenaltySound = new SoundPathSpecifier("/Audio/Machines/Nuke/angry_beep.ogg");
/// <summary>
/// The sound that's played when the mail's opened.
/// </summary>
[DataField]
public SoundSpecifier OpenSound = new SoundPathSpecifier("/Audio/Effects/packetrip.ogg");
/// <summary>
/// The sound that's played when the mail's lock has been emagged.
/// </summary>
[DataField]
public SoundSpecifier EmagSound = new SoundCollectionSpecifier("sparks");
public CancellationTokenSource? PriorityCancelToken;
}

View File

@ -1,3 +1,4 @@
using Content.Shared.Roles;
using Robust.Shared.Prototypes;
namespace Content.Shared._DV.Mail;
@ -5,7 +6,7 @@ namespace Content.Shared._DV.Mail;
/// <summary>
/// Generic random weighting dataset to use.
/// </summary>
[Prototype("mailDeliveryPool")]
[Prototype]
public sealed class MailDeliveryPoolPrototype : IPrototype
{
[IdDataField] public string ID { get; } = default!;
@ -13,18 +14,18 @@ public sealed class MailDeliveryPoolPrototype : IPrototype
/// <summary>
/// Mail that can be sent to everyone.
/// </summary>
[DataField("everyone")]
public Dictionary<string, float> Everyone = new();
[DataField]
public Dictionary<EntProtoId, float> Everyone = new();
/// <summary>
/// Mail that can be sent only to specific jobs.
/// </summary>
[DataField("jobs")]
public Dictionary<string, Dictionary<string, float>> Jobs = new();
[DataField]
public Dictionary<ProtoId<JobPrototype>, Dictionary<EntProtoId, float>> Jobs = new();
/// <summary>
/// Mail that can be sent only to specific departments.
/// </summary>
[DataField("departments")]
public Dictionary<string, Dictionary<string, float>> Departments = new();
[DataField]
public Dictionary<ProtoId<DepartmentPrototype>, Dictionary<EntProtoId, float>> Departments = new();
}

View File

@ -0,0 +1,9 @@
using Robust.Shared.GameStates;
namespace Content.Shared._DV.Mail;
/// <summary>
/// Used to mark entities that can receive mail.
/// </summary>
[RegisterComponent, NetworkedComponent]
public sealed partial class MailReceiverComponent : Component;

View File

@ -0,0 +1,145 @@
using Content.Shared.Radio;
using Robust.Shared.Audio;
using Robust.Shared.GameStates;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization.TypeSerializers.Implementations.Custom;
namespace Content.Shared._DV.Mail;
/// <summary>
/// This is for the mail teleporter.
/// Random mail will be teleported to this every few minutes.
/// </summary>
[RegisterComponent, NetworkedComponent, AutoGenerateComponentPause] // TODO: Access & PublicAPI for Commands
public sealed partial class MailTeleporterComponent : Component
{
/// <summary>
/// The TimeSpan of next Delivery
/// </summary>
[DataField(customTypeSerializer: typeof(TimeOffsetSerializer)), AutoPausedField]
public TimeSpan NextDelivery = TimeSpan.Zero;
/// <summary>
/// The time between new deliveries.
/// </summary>
[DataField]
public TimeSpan TeleportInterval = TimeSpan.FromMinutes(5);
/// <summary>
/// The sound that's played when new mail arrives.
/// </summary>
[DataField]
public SoundSpecifier TeleportSound = new SoundPathSpecifier("/Audio/Effects/teleport_arrival.ogg");
/// <summary>
/// The MailDeliveryPoolPrototype that's used to select what mail this
/// teleporter can deliver.
/// </summary>
[DataField]
public ProtoId<MailDeliveryPoolPrototype> MailPool = "RandomDeltaVMailDeliveryPool";
/// <summary>
/// Whether the telepad should output a message upon spawning mail.
/// </summary>
[DataField]
public bool RadioNotification;
/// <summary>
/// <see cref="LocId"/> to send when spawning new mail.
/// </summary>
[DataField]
public LocId ShipmentReceivedMessage = "mail-received-message";
/// <summary>
/// <see cref="RadioChannelPrototype"/> to notify when spawning new mail.
/// </summary>
[DataField]
public ProtoId<RadioChannelPrototype> RadioChannel = "Supply";
/// <summary>
/// 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.
/// </summary>
[DataField]
public int CandidatesPerDelivery = 8;
[DataField]
public int MinimumDeliveriesPerTeleport = 1;
/// <summary>
/// Do not teleport any more mail in, if there are at least this many
/// undelivered parcels.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[DataField]
public int MaximumUndeliveredParcels = 5;
/// <summary>
/// Any item that breaks or is destroyed in less than this amount of
/// damage is one of the types of items considered fragile.
/// </summary>
[DataField]
public int FragileDamageThreshold = 10;
/// <summary>
/// What's the bonus for delivering a fragile package intact?
/// </summary>
[DataField]
public int FragileBonus = 100;
/// <summary>
/// What's the malus for failing to deliver a fragile package?
/// </summary>
[DataField]
public int FragileMalus = -100;
/// <summary>
/// What's the chance for any one delivery to be marked as priority mail?
/// </summary>
[DataField]
public float PriorityChance = 0.1f;
/// <summary>
/// How long until a priority delivery is considered as having failed
/// if not delivered?
/// </summary>
[DataField]
public TimeSpan PriorityDuration = TimeSpan.FromMinutes(5);
/// <summary>
/// What's the bonus for delivering a priority package on time?
/// </summary>
[DataField]
public int PriorityBonus = 250;
/// <summary>
/// What's the malus for failing to deliver a priority package?
/// </summary>
[DataField]
public int PriorityMalus = -250;
/// <summary>
/// What's the bonus for delivering a large package intact?
/// </summary>
[DataField]
public int LargeBonus = 1500;
/// <summary>
/// What's the malus for failing to deliver a large package?
/// </summary>
[DataField]
public int LargeMalus = -500;
}

View File

@ -1,19 +1,18 @@
using Robust.Shared.Serialization;
namespace Content.Shared._DV.Mail
namespace Content.Shared._DV.Mail;
/// <summary>
/// Stores the visuals for mail.
/// </summary>
[Serializable, NetSerializable]
public enum MailVisuals : byte
{
/// <summary>
/// Stores the visuals for mail.
/// </summary>
[Serializable, NetSerializable]
public enum MailVisuals : byte
{
IsLocked,
IsTrash,
IsBroken,
IsFragile,
IsPriority,
IsPriorityInactive,
JobIcon,
}
IsLocked,
IsTrash,
IsBroken,
IsFragile,
IsPriority,
IsPriorityInactive,
JobIcon,
}

View File

@ -1,6 +0,0 @@
namespace Content.Shared._DV.Mail
{
public abstract partial class SharedMailComponent : Component
{
}
}

View File

@ -0,0 +1,421 @@
using System.Linq;
using Content.Shared._DV.Cargo.Components;
using Content.Shared._DV.Cargo.Systems;
using Content.Shared.Access.Components;
using Content.Shared.Access.Systems;
using Content.Shared.Cargo;
using Content.Shared.Cargo.Components;
using Content.Shared.Cargo.Prototypes;
using Content.Shared.Damage;
using Content.Shared.Destructible;
using Content.Shared.Emag.Components;
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.Nutrition.EntitySystems;
using Content.Shared.Objectives.Components;
using Content.Shared.PDA;
using Content.Shared.Popups;
using Content.Shared.Station;
using Content.Shared.Tag;
using Robust.Shared.Audio;
using Robust.Shared.Audio.Systems;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Timing;
namespace Content.Shared._DV.Mail;
public abstract class SharedMailSystem : EntitySystem
{
[Dependency] protected readonly AccessReaderSystem Access = default!;
[Dependency] private readonly DamageableSystem _damageable = default!;
[Dependency] protected readonly IGameTiming Timing = default!;
[Dependency] protected readonly LogisticStatsSystem LogisticsStats = default!;
[Dependency] protected readonly SharedAppearanceSystem Appearance = default!;
[Dependency] protected readonly SharedAudioSystem Audio = default!;
[Dependency] private readonly SharedCargoSystem _cargo = default!;
[Dependency] private readonly SharedContainerSystem _container = default!;
[Dependency] private readonly SharedHandsSystem _hands = default!;
[Dependency] protected readonly SharedIdCardSystem IdCard = default!;
[Dependency] private readonly SharedPopupSystem _popup = default!;
[Dependency] protected readonly SharedStationSystem Station = default!;
[Dependency] protected readonly TagSystem Tag = default!;
private static readonly ProtoId<TagPrototype> RecyclableTag = "Recyclable";
private static readonly ProtoId<TagPrototype> TrashTag = "Trash";
public override void Initialize()
{
base.Initialize();
SubscribeLocalEvent<MailComponent, AfterInteractUsingEvent>(OnAfterInteractUsing);
SubscribeLocalEvent<MailComponent, BreakageEventArgs>(OnBreak);
SubscribeLocalEvent<MailComponent, ComponentShutdown>(OnShutdown);
SubscribeLocalEvent<MailComponent, DamageChangedEvent>(OnDamageChanged);
SubscribeLocalEvent<MailComponent, DestructionEventArgs>(OnDestruction);
SubscribeLocalEvent<MailComponent, ExaminedEvent>(OnExamined);
SubscribeLocalEvent<MailComponent, GotEmaggedEvent>(OnEmagged);
SubscribeLocalEvent<MailComponent, UseInHandEvent>(OnUseInHand, before: new[] { typeof(FoodSystem), typeof(IngestionSystem) });
}
/// <summary>
/// Handle the <see cref="ComponentShutdown"/> and cancel any pending CancellationTokenSources for priority mail.
/// </summary>
private static void OnShutdown(Entity<MailComponent> ent, ref ComponentShutdown args)
{
ent.Comp.PriorityCancelToken?.Cancel();
}
/// <summary>
/// Handle the <see cref="AfterInteractEvent"/> by checking the ID against the mail.
/// </summary>
private void OnAfterInteractUsing(Entity<MailComponent> ent, ref AfterInteractUsingEvent args)
{
if (!args.CanReach || !ent.Comp.IsLocked)
return;
if (!HasComp<AccessReaderComponent>(ent))
return;
IdCardComponent? idCard = null; // We need an ID card.
if (HasComp<PdaComponent>(args.Used)) // Can we find it in a PDA if the user is using that?
{
IdCard.TryGetIdCard(args.Used, out var pdaId);
idCard = pdaId;
}
if (idCard == null &&
HasComp<IdCardComponent>(args.Used)) // If we still don't have an ID, check if the item itself is one
idCard = Comp<IdCardComponent>(args.Used);
if (idCard == null) // Return if we still haven't found an id card.
return;
if (!HasComp<EmaggedComponent>(ent))
{
if (idCard.FullName != ent.Comp.Recipient || idCard.LocalizedJobTitle != ent.Comp.RecipientJob)
{
_popup.PopupPredicted(Loc.GetString("mail-recipient-mismatch"), ent, args.User);
return;
}
if (!Access.IsAllowed(ent, args.User))
{
_popup.PopupPredicted(Loc.GetString("mail-invalid-access"), ent, args.User);
return;
}
}
// DeltaV - Add earnings to logistic stats
ExecuteForEachLogisticsStats(ent,
(station, logisticStats) =>
{
LogisticsStats.AddOpenedMailEarnings(station,
logisticStats,
ent.Comp.IsProfitable ? ent.Comp.Bounty : 0);
});
UnlockMail(ent);
if (!ent.Comp.IsProfitable)
{
_popup.PopupPredicted(Loc.GetString("mail-unlocked"), ent, args.User);
return;
}
_popup.PopupPredicted(Loc.GetString("mail-unlocked-reward", ("bounty", ent.Comp.Bounty)), ent, args.User);
ent.Comp.IsProfitable = false;
var query = EntityQueryEnumerator<StationBankAccountComponent>();
while (query.MoveNext(out var station, out var account))
{
if (Station.GetOwningStation(ent) != station)
continue;
UpdateBankAccount(
(station, account),
ent.Comp.Bounty,
_cargo.CreateAccountDistribution((station, account)));
}
Dirty(ent);
}
/// <summary>
/// Handle the <see cref="DamageChangedEvent"/> and transfer damage to the contents.
/// </summary>
private void OnDamageChanged(Entity<MailComponent> ent, ref DamageChangedEvent args)
{
if (args.DamageDelta == null)
return;
if (!_container.TryGetContainer(ent, "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())
{
_damageable.TryChangeDamage(entity, args.DamageDelta);
}
}
/// <summary>
/// Handle the <see cref="DestructionEventArgs"/>.
/// </summary>
private void OnDestruction(Entity<MailComponent> ent, ref DestructionEventArgs args)
{
if (ent.Comp.IsLocked)
{
ExecuteForEachLogisticsStats(ent,
(station, logisticStats) =>
{
LogisticsStats.AddTamperedMailLosses(station,
logisticStats,
ent.Comp.IsProfitable ? ent.Comp.Penalty : 0);
});
PenalizeStationFailedDelivery(ent, "mail-penalty-lock");
}
if (!Tag.HasTag(ent, TrashTag))
OpenMail(ent.AsNullable());
UpdateAntiTamperVisuals(ent, false);
}
/// <summary>
/// Handle the <see cref="BreakageEventArgs"/>.
/// </summary>
private void OnBreak(Entity<MailComponent> ent, ref BreakageEventArgs args)
{
Appearance.SetData(ent, MailVisuals.IsBroken, true);
if (!ent.Comp.IsFragile)
return;
ExecuteForEachLogisticsStats(ent,
(station, logisticStats) =>
{
LogisticsStats.AddDamagedMailLosses(station,
logisticStats,
ent.Comp.IsProfitable ? ent.Comp.Penalty : 0);
});
PenalizeStationFailedDelivery(ent, "mail-penalty-fragile");
}
/// <summary>
/// Handle the <see cref="ExaminedEvent"/>.
/// </summary>
private void OnExamined(Entity<MailComponent> ent, ref ExaminedEvent args)
{
var mailEntityStrings = ent.Comp.IsLarge ? MailConstants.MailLarge : MailConstants.Mail;
if (!args.IsInDetailsRange)
{
args.PushMarkup(Loc.GetString(mailEntityStrings.DescFar));
return;
}
args.PushMarkup(Loc.GetString(mailEntityStrings.DescClose,
("name", ent.Comp.Recipient),
("job", ent.Comp.RecipientJob)));
if (ent.Comp.IsFragile)
args.PushMarkup(Loc.GetString("mail-desc-fragile"));
if (ent.Comp.IsPriority)
{
if (ent.Comp.ExpiryTime != null && ent.Comp.IsProfitable)
{
var timeLeft = ent.Comp.ExpiryTime.Value - Timing.CurTime;
if (timeLeft > TimeSpan.Zero)
{
args.PushMarkup(Loc.GetString("mail-desc-priority-timer",
("time", timeLeft.ToString(@"mm\:ss"))));
}
else
{
args.PushMarkup(Loc.GetString("mail-desc-priority-inactive"));
}
}
else
{
// Handle the weird case of the timer not being set but mail being priority, if that ever happens
args.PushMarkup(Loc.GetString(ent.Comp.IsProfitable ? "mail-desc-priority" : "mail-desc-priority-inactive"));
}
}
}
/// <summary>
/// Handle the <see cref="GotEmaggedEvent"/> by unlocking the mail without giving cargo money.
/// </summary>
private void OnEmagged(Entity<MailComponent> ent, ref GotEmaggedEvent args)
{
if (!ent.Comp.IsLocked)
return;
UnlockMail(ent);
_popup.PopupPredicted(Loc.GetString("mail-unlocked-by-emag"), ent, args.UserUid);
Audio.PlayPredicted(ent.Comp.EmagSound, ent, args.UserUid, AudioParams.Default.WithVolume(4));
ent.Comp.IsProfitable = false;
args.Handled = true;
Dirty(ent);
}
/// <summary>
/// Handle the <see cref="UseInHandEvent"/> and try to open the mail.
/// </summary>
private void OnUseInHand(Entity<MailComponent> ent, ref UseInHandEvent args)
{
if (Tag.HasTag(ent, TrashTag))
return;
if (ent.Comp.IsLocked)
{
_popup.PopupPredicted(Loc.GetString("mail-locked"), ent, args.User);
args.Handled = true;
return;
}
args.Handled = true;
OpenMail(ent.AsNullable(), args.User);
}
/// <summary>
/// Helper method for actually opening the mail.
/// </summary>
private void OpenMail(Entity<MailComponent?> ent, EntityUid? user = null)
{
if (!Resolve(ent, ref ent.Comp))
return;
Audio.PlayPredicted(ent.Comp.OpenSound, ent, user);
if (user != null)
_hands.TryDrop((EntityUid)user);
if (!_container.TryGetContainer(ent, "contents", out var contents))
return;
foreach (var entity in contents.ContainedEntities.ToArray())
{
_hands.PickupOrDrop(user, entity);
}
Tag.AddTag(ent, TrashTag);
Tag.AddTag(ent, RecyclableTag);
UpdateMailTrashState(ent, true);
}
/// <summary>
/// Handle logic similar between a normal mail unlock and an emag
/// frying out the lock.
/// </summary>
private void UnlockMail(Entity<MailComponent> ent)
{
ent.Comp.IsLocked = false;
UpdateAntiTamperVisuals(ent, false);
if (!ent.Comp.IsPriority)
return;
// This is a successful delivery. Keep the failure timer from triggering.
ent.Comp.PriorityCancelToken?.Cancel();
// The priority tape is visually considered to be a part of the
// anti-tamper lock, so remove that too.
Appearance.SetData(ent, MailVisuals.IsPriority, false);
// The examination code depends on this being false to not show
// the priority tape description anymore.
ent.Comp.IsPriority = false;
Dirty(ent);
RemComp<StealTargetComponent>(ent);
}
private void UpdateAntiTamperVisuals(EntityUid uid, bool isLocked)
{
Appearance.SetData(uid, MailVisuals.IsLocked, isLocked);
}
private void UpdateMailTrashState(EntityUid uid, bool isTrash)
{
Appearance.SetData(uid, MailVisuals.IsTrash, isTrash);
}
/// <summary>
/// Implemented on the Server, this is a fancy wrapper for the Cargo API since it's not in Shared yet.
/// </summary>
protected virtual void UpdateBankAccount(
Entity<StationBankAccountComponent?> ent,
int balanceAdded,
Dictionary<ProtoId<CargoAccountPrototype>, double> accountDistribution)
{
}
/// <summary>
/// Implemented on the Server side.
/// </summary>
protected virtual void PenalizeStationFailedDelivery(Entity<MailComponent> ent, string localizationString)
{
}
protected void ExecuteForEachLogisticsStats(EntityUid uid,
Action<EntityUid, StationLogisticStatsComponent> action)
{
var query = EntityQueryEnumerator<StationLogisticStatsComponent>();
while (query.MoveNext(out var station, out var logisticStats))
{
if (Station.GetOwningStation(uid) != station)
continue;
action(station, logisticStats);
}
}
}
/// <summary>
/// Constants related to mail.
/// </summary>
public static class MailConstants
{
/// <summary>
/// Locale strings related to small parcels.
/// </summary>
public static readonly MailEntityStrings Mail = new()
{
NameAddressed = "mail-item-name-addressed",
DescClose = "mail-desc-close",
DescFar = "mail-desc-far",
};
/// <summary>
/// Locale strings related to large packages.
/// </summary>
public static readonly MailEntityStrings MailLarge = new()
{
NameAddressed = "mail-large-item-name-addressed",
DescClose = "mail-large-desc-close",
DescFar = "mail-large-desc-far",
};
}
/// <summary>
/// A set of localized strings related to mail entities
/// </summary>
public struct MailEntityStrings
{
public string NameAddressed;
public string DescClose;
public string DescFar;
}

View File

@ -5,6 +5,7 @@ mail-desc-far = A parcel of mail. You can't make out who it's addressed to from
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-timer = The anti-tamper lock's [color=yellow]yellow priority tape[/color] is active. Expires in: [color=yellow]{$time}[/color]
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*.

View File

@ -1,120 +0,0 @@
- 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
MailPumpkinPie: 1 # DeltaV - Pumpkie pie mail
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
MailBooksAll: 0.5 # DeltaV - All the other books not in MailBooks, see Resources/Prototypes/_DV/Entities/Objects/Specific/Mail/mail.yml
# 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
Epistemics:
# 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
Detective: # Deltav - Detective is in charge of investigating crimes.
MailDetectiveForensicSupplement: 2 # Deltav - Detective is in charge of investigating crimes.
HeadOfPersonnel:
MailHoPBureaucracy: 2
MailHoPSupplement: 3
HeadOfSecurity:
MailSecurityNonlethalsKit: 2
Lawyer:
MailSecuritySpaceLaw: 2
Mime:
MailMimeArtsCrafts: 3
MailMimeBlankBook: 2
MailMimeBottleOfNothing: 1
ResearchDirector: # DeltaV - Epistemics Department replacing Science but keeping their IDs
MailEpistemologyIngotGold: 2
Musician:
MailMusicianInstrumentSmall: 1
Passenger:
MailPassengerMoney: 3
Warden:
MailWardenCrowdControl: 2

View File

@ -48,6 +48,7 @@
- type: Sprite
sprite: _DV/Objects/Devices/cartridge.rsi
state: cart-mail
- type: StationTracker
- type: Icon
sprite: _DV/Objects/Devices/cartridge.rsi
state: cart-mail

View File

@ -82,7 +82,6 @@
MailMedicalStabilizers: 2
MailNFMedkit: 2
Engineering:
MailAMEGuide: 1
MailEngineeringCables: 2
MailEngineeringKudzuDeterrent: 2
MailEngineeringSheetGlass: 2
@ -140,8 +139,6 @@
MailMimeBottleOfNothing: 1
ResearchDirector: # DeltaV - Epistemics Department replacing Science but keeping their IDs
MailEpistemologyIngotGold: 2
Musician:
MailMusicianInstrumentSmall: 1
Passenger:
MailPassengerMoney: 3
Warden: