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:
parent
1835d7678f
commit
5c527dc6a2
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
using Content.Shared._DV.Mail;
|
||||
|
||||
namespace Content.Client._DV.Mail
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class MailComponent : SharedMailComponent
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +0,0 @@
|
|||
namespace Content.Server._DV.Mail.Components
|
||||
{
|
||||
[RegisterComponent]
|
||||
public sealed partial class MailReceiverComponent : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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"));
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
namespace Content.Shared._DV.Mail
|
||||
{
|
||||
public abstract partial class SharedMailComponent : Component
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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*.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue