From 18592148f7ef3e9de6e67e1fe2c4029dc392f7a8 Mon Sep 17 00:00:00 2001 From: BarryNorfolk Date: Thu, 30 Jan 2025 18:27:36 +0100 Subject: [PATCH] Add history tab to bounty console (#33932) * Add struct for holding historical data on cargo bounties * Add localisation strings for bounty history * Add new XAML entry for display bounty history * Expand cargo bounty menu to include tabs * Ensure station databases hold historical bounty data * Add to the bounty history when removing one from active * Feed bounty history into cargo's bounty system * Move tab title setting to constructor * Remove redundant access specifications * Remove un-needed override * Fixup BountyHistoryEntry backing code * Fix formatting in CargoBountyMenu * Reformat BountyHistoryData * Rework TryRemoveBounty to use new Entity type * Add Enum for showing bounty results * Rework look and feel of History tab * Add visible text when no bounties have been completed yet * Remove control * Swap default to null * Reverse ordering of bounties so last entry comes first * Remove redundant Visible * Move enum docs into the enum --- .../CargoBountyConsoleBoundUserInterface.cs | 2 +- .../Cargo/UI/BountyHistoryEntry.xaml | 22 ++++++ .../Cargo/UI/BountyHistoryEntry.xaml.cs | 49 ++++++++++++++ Content.Client/Cargo/UI/CargoBountyMenu.xaml | 31 ++++++--- .../Cargo/UI/CargoBountyMenu.xaml.cs | 21 +++++- .../StationCargoBountyDatabaseComponent.cs | 11 ++- .../Cargo/Systems/CargoSystem.Bounty.cs | 49 ++++++++++---- .../Cargo/CargoBountyHistoryData.cs | 67 +++++++++++++++++++ .../Components/CargoBountyConsoleComponent.cs | 4 +- .../en-US/cargo/cargo-bounty-console.ftl | 6 ++ 10 files changed, 235 insertions(+), 27 deletions(-) create mode 100644 Content.Client/Cargo/UI/BountyHistoryEntry.xaml create mode 100644 Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs create mode 100644 Content.Shared/Cargo/CargoBountyHistoryData.cs diff --git a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs index 44c40143d8..04075000f5 100644 --- a/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs +++ b/Content.Client/Cargo/BUI/CargoBountyConsoleBoundUserInterface.cs @@ -39,6 +39,6 @@ public sealed class CargoBountyConsoleBoundUserInterface : BoundUserInterface if (message is not CargoBountyConsoleState state) return; - _menu?.UpdateEntries(state.Bounties, state.UntilNextSkip); + _menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip); } } diff --git a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml new file mode 100644 index 0000000000..905cf020ed --- /dev/null +++ b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs new file mode 100644 index 0000000000..54804be641 --- /dev/null +++ b/Content.Client/Cargo/UI/BountyHistoryEntry.xaml.cs @@ -0,0 +1,49 @@ +using Content.Client.Message; +using Content.Shared.Cargo; +using Content.Shared.Cargo.Prototypes; +using Robust.Client.AutoGenerated; +using Robust.Client.UserInterface.Controls; +using Robust.Client.UserInterface.XAML; +using Robust.Shared.Prototypes; +using Robust.Shared.Timing; + +namespace Content.Client.Cargo.UI; + +[GenerateTypedNameReferences] +public sealed partial class BountyHistoryEntry : BoxContainer +{ + [Dependency] private readonly IPrototypeManager _prototype = default!; + + public BountyHistoryEntry(CargoBountyHistoryData bounty) + { + RobustXamlLoader.Load(this); + IoCManager.InjectDependencies(this); + + if (!_prototype.TryIndex(bounty.Bounty, out var bountyPrototype)) + return; + + var items = new List(); + foreach (var entry in bountyPrototype.Entries) + { + items.Add(Loc.GetString("bounty-console-manifest-entry", + ("amount", entry.Amount), + ("item", Loc.GetString(entry.Name)))); + } + + ManifestLabel.SetMarkup(Loc.GetString("bounty-console-manifest-label", ("item", string.Join(", ", items)))); + RewardLabel.SetMarkup(Loc.GetString("bounty-console-reward-label", ("reward", bountyPrototype.Reward))); + IdLabel.SetMarkup(Loc.GetString("bounty-console-id-label", ("id", bounty.Id))); + + TimestampLabel.SetMarkup(bounty.Timestamp.ToString(@"hh\:mm\:ss")); + + if (bounty.Result == CargoBountyHistoryData.BountyResult.Completed) + { + NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-completed-label")); + } + else + { + NoticeLabel.SetMarkup(Loc.GetString("bounty-console-history-notice-skipped-label", + ("id", bounty.ActorName ?? ""))); + } + } +} diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml b/Content.Client/Cargo/UI/CargoBountyMenu.xaml index bb263ff6c4..526ba69129 100644 --- a/Content.Client/Cargo/UI/CargoBountyMenu.xaml +++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml @@ -11,15 +11,28 @@ - - - - + + + + + + + diff --git a/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs index 3767b45e4b..c289fb6ed8 100644 --- a/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs +++ b/Content.Client/Cargo/UI/CargoBountyMenu.xaml.cs @@ -15,9 +15,12 @@ public sealed partial class CargoBountyMenu : FancyWindow public CargoBountyMenu() { RobustXamlLoader.Load(this); + + MasterTabContainer.SetTabTitle(0, Loc.GetString("bounty-console-tab-available-label")); + MasterTabContainer.SetTabTitle(1, Loc.GetString("bounty-console-tab-history-label")); } - public void UpdateEntries(List bounties, TimeSpan untilNextSkip) + public void UpdateEntries(List bounties, List history, TimeSpan untilNextSkip) { BountyEntriesContainer.Children.Clear(); foreach (var b in bounties) @@ -32,5 +35,21 @@ public sealed partial class CargoBountyMenu : FancyWindow { MinHeight = 10 }); + + BountyHistoryContainer.Children.Clear(); + if (history.Count == 0) + { + NoHistoryLabel.Visible = true; + } + else + { + NoHistoryLabel.Visible = false; + + // Show the history in reverse, so last entry is first in the list + for (var i = history.Count - 1; i >= 0; i--) + { + BountyHistoryContainer.AddChild(new BountyHistoryEntry(history[i])); + } + } } } diff --git a/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs b/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs index a7735787cb..c650438b28 100644 --- a/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs +++ b/Content.Server/Cargo/Components/StationCargoBountyDatabaseComponent.cs @@ -12,15 +12,22 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component /// /// Maximum amount of bounties a station can have. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public int MaxBounties = 6; /// /// A list of all the bounties currently active for a station. /// - [DataField, ViewVariables(VVAccess.ReadWrite)] + [DataField] public List Bounties = new(); + /// + /// A list of all the bounties that have been completed or + /// skipped for a station. + /// + [DataField] + public List History = new(); + /// /// Used to determine unique order IDs /// diff --git a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs index 373e8e243b..7f74fe269d 100644 --- a/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs +++ b/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs @@ -8,6 +8,7 @@ using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; using Content.Shared.Database; +using Content.Shared.IdentityManagement; using Content.Shared.NameIdentifier; using Content.Shared.Paper; using Content.Shared.Stacks; @@ -16,6 +17,7 @@ using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Shared.Containers; using Robust.Shared.Random; +using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Content.Server.Cargo.Systems; @@ -25,6 +27,7 @@ public sealed partial class CargoSystem [Dependency] private readonly ContainerSystem _container = default!; [Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!; [Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; [ValidatePrototypeId] private const string BountyNameIdentifierGroup = "Bounty"; @@ -54,7 +57,7 @@ public sealed partial class CargoSystem return; var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime; - _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, untilNextSkip)); + _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip)); } private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args) @@ -95,13 +98,13 @@ public sealed partial class CargoSystem return; } - if (!TryRemoveBounty(station, bounty.Value)) + if (!TryRemoveBounty(station, bounty.Value, true, args.Actor)) return; FillBountyDatabase(station); db.NextSkipTime = _timing.CurTime + db.SkipDelay; var untilNextSkip = db.NextSkipTime - _timing.CurTime; - _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip)); + _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip)); _audio.PlayPvs(component.SkipSound, uid); } @@ -179,7 +182,7 @@ public sealed partial class CargoSystem continue; } - TryRemoveBounty(station, bounty.Value); + TryRemoveBounty(station, bounty.Value, false); FillBountyDatabase(station); _adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled"); } @@ -434,24 +437,44 @@ public sealed partial class CargoSystem } [PublicAPI] - public bool TryRemoveBounty(EntityUid uid, string dataId, StationCargoBountyDatabaseComponent? component = null) + public bool TryRemoveBounty(Entity ent, + string dataId, + bool skipped, + EntityUid? actor = null) { - if (!TryGetBountyFromId(uid, dataId, out var data, component)) + if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp)) return false; - return TryRemoveBounty(uid, data.Value, component); + return TryRemoveBounty(ent, data.Value, skipped, actor); } - public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null) + public bool TryRemoveBounty(Entity ent, + CargoBountyData data, + bool skipped, + EntityUid? actor = null) { - if (!Resolve(uid, ref component)) + if (!Resolve(ent, ref ent.Comp)) return false; - for (var i = 0; i < component.Bounties.Count; i++) + for (var i = 0; i < ent.Comp.Bounties.Count; i++) { - if (component.Bounties[i].Id == data.Id) + if (ent.Comp.Bounties[i].Id == data.Id) { - component.Bounties.RemoveAt(i); + string? actorName = null; + if (actor != null) + { + var getIdentityEvent = new TryGetIdentityShortInfoEvent(ent.Owner, actor.Value); + RaiseLocalEvent(getIdentityEvent); + actorName = getIdentityEvent.Title; + } + + ent.Comp.History.Add(new CargoBountyHistoryData(data, + skipped + ? CargoBountyHistoryData.BountyResult.Skipped + : CargoBountyHistoryData.BountyResult.Completed, + _gameTiming.CurTime, + actorName)); + ent.Comp.Bounties.RemoveAt(i); return true; } } @@ -492,7 +515,7 @@ public sealed partial class CargoSystem } var untilNextSkip = db.NextSkipTime - _timing.CurTime; - _uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, untilNextSkip)); + _uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip)); } } diff --git a/Content.Shared/Cargo/CargoBountyHistoryData.cs b/Content.Shared/Cargo/CargoBountyHistoryData.cs new file mode 100644 index 0000000000..eb46bc1944 --- /dev/null +++ b/Content.Shared/Cargo/CargoBountyHistoryData.cs @@ -0,0 +1,67 @@ +using Content.Shared.Cargo.Prototypes; +using Robust.Shared.Prototypes; +using Robust.Shared.Serialization; + +namespace Content.Shared.Cargo; + +/// +/// A data structure for storing historical information about bounties. +/// +[DataDefinition, NetSerializable, Serializable] +public readonly partial record struct CargoBountyHistoryData +{ + /// + /// A unique id used to identify the bounty + /// + [DataField] + public string Id { get; init; } = string.Empty; + + /// + /// Whether this bounty was completed or skipped. + /// + [DataField] + public BountyResult Result { get; init; } = BountyResult.Completed; + + /// + /// Optional name of the actor that completed/skipped the bounty. + /// + [DataField] + public string? ActorName { get; init; } = default; + + /// + /// Time when this bounty was completed or skipped + /// + [DataField] + public TimeSpan Timestamp { get; init; } = TimeSpan.MinValue; + + /// + /// The prototype containing information about the bounty. + /// + [DataField(required: true)] + public ProtoId Bounty { get; init; } = string.Empty; + + public CargoBountyHistoryData(CargoBountyData bounty, BountyResult result, TimeSpan timestamp, string? actorName) + { + Bounty = bounty.Bounty; + Result = result; + Id = bounty.Id; + ActorName = actorName; + Timestamp = timestamp; + } + + /// + /// Covers how a bounty was actually finished. + /// + public enum BountyResult + { + /// + /// Bounty was actually fulfilled and the goods sold + /// + Completed = 0, + + /// + /// Bounty was explicitly skipped by some actor + /// + Skipped = 1, + } +} diff --git a/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs b/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs index bf82a08127..8c78312be1 100644 --- a/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs +++ b/Content.Shared/Cargo/Components/CargoBountyConsoleComponent.cs @@ -50,11 +50,13 @@ public sealed partial class CargoBountyConsoleComponent : Component public sealed class CargoBountyConsoleState : BoundUserInterfaceState { public List Bounties; + public List History; public TimeSpan UntilNextSkip; - public CargoBountyConsoleState(List bounties, TimeSpan untilNextSkip) + public CargoBountyConsoleState(List bounties, List history, TimeSpan untilNextSkip) { Bounties = bounties; + History = history; UntilNextSkip = untilNextSkip; } } diff --git a/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl b/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl index 4314cbf449..d7a7025927 100644 --- a/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl +++ b/Resources/Locale/en-US/cargo/cargo-bounty-console.ftl @@ -18,3 +18,9 @@ bounty-console-flavor-right = v1.4 bounty-manifest-header = [font size=14][bold]Official cargo bounty manifest[/bold] (ID#{$id})[/font] bounty-manifest-list-start = Item manifest: + +bounty-console-tab-available-label = Available +bounty-console-tab-history-label = History +bounty-console-history-empty-label = No bounty history found +bounty-console-history-notice-completed-label = [color=limegreen]Completed[/color] +bounty-console-history-notice-skipped-label = [color=red]Skipped[/color] by {$id} -- 2.51.2