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
This commit is contained in:
BarryNorfolk
2025-01-30 18:27:36 +01:00
committed by GitHub
parent 5be5ee9832
commit 18592148f7
10 changed files with 235 additions and 27 deletions

View File

@@ -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);
}
}

View File

@@ -0,0 +1,22 @@
<BoxContainer xmlns="https://spacestation14.io"
xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
Margin="10 10 10 0"
HorizontalExpand="True">
<PanelContainer StyleClasses="AngleRect" HorizontalExpand="True">
<BoxContainer Orientation="Vertical"
HorizontalExpand="True">
<BoxContainer Orientation="Horizontal">
<BoxContainer Orientation="Vertical" HorizontalExpand="True">
<RichTextLabel Name="RewardLabel"/>
<RichTextLabel Name="ManifestLabel"/>
</BoxContainer>
<BoxContainer Orientation="Vertical" MinWidth="120" Margin="0 0 10 0">
<RichTextLabel Name="TimestampLabel" HorizontalAlignment="Right" />
<RichTextLabel Name="IdLabel" HorizontalAlignment="Right" />
</BoxContainer>
</BoxContainer>
<customControls:HSeparator Margin="5 10 5 10"/>
<RichTextLabel Name="NoticeLabel" />
</BoxContainer>
</PanelContainer>
</BoxContainer>

View File

@@ -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<string>();
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 ?? "")));
}
}
}

View File

@@ -11,15 +11,28 @@
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
<TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True">
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<BoxContainer Name="BountyEntriesContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True">
</BoxContainer>
HorizontalExpand="True" />
</ScrollContainer>
<ScrollContainer HScrollEnabled="False"
HorizontalExpand="True"
VerticalExpand="True">
<Label Name="NoHistoryLabel"
Text="{Loc 'bounty-console-history-empty-label'}"
Visible="False"
Align="Center" />
<BoxContainer Name="BountyHistoryContainer"
Orientation="Vertical"
VerticalExpand="True"
HorizontalExpand="True" />
</ScrollContainer>
</TabContainer>
</PanelContainer>
<!-- Footer -->
<BoxContainer Orientation="Vertical">

View File

@@ -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<CargoBountyData> bounties, TimeSpan untilNextSkip)
public void UpdateEntries(List<CargoBountyData> bounties, List<CargoBountyHistoryData> 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]));
}
}
}
}

View File

@@ -12,15 +12,22 @@ public sealed partial class StationCargoBountyDatabaseComponent : Component
/// <summary>
/// Maximum amount of bounties a station can have.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public int MaxBounties = 6;
/// <summary>
/// A list of all the bounties currently active for a station.
/// </summary>
[DataField, ViewVariables(VVAccess.ReadWrite)]
[DataField]
public List<CargoBountyData> Bounties = new();
/// <summary>
/// A list of all the bounties that have been completed or
/// skipped for a station.
/// </summary>
[DataField]
public List<CargoBountyHistoryData> History = new();
/// <summary>
/// Used to determine unique order IDs
/// </summary>

View File

@@ -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<NameIdentifierGroupPrototype>]
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<StationCargoBountyDatabaseComponent?> 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<StationCargoBountyDatabaseComponent?> 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));
}
}

View File

@@ -0,0 +1,67 @@
using Content.Shared.Cargo.Prototypes;
using Robust.Shared.Prototypes;
using Robust.Shared.Serialization;
namespace Content.Shared.Cargo;
/// <summary>
/// A data structure for storing historical information about bounties.
/// </summary>
[DataDefinition, NetSerializable, Serializable]
public readonly partial record struct CargoBountyHistoryData
{
/// <summary>
/// A unique id used to identify the bounty
/// </summary>
[DataField]
public string Id { get; init; } = string.Empty;
/// <summary>
/// Whether this bounty was completed or skipped.
/// </summary>
[DataField]
public BountyResult Result { get; init; } = BountyResult.Completed;
/// <summary>
/// Optional name of the actor that completed/skipped the bounty.
/// </summary>
[DataField]
public string? ActorName { get; init; } = default;
/// <summary>
/// Time when this bounty was completed or skipped
/// </summary>
[DataField]
public TimeSpan Timestamp { get; init; } = TimeSpan.MinValue;
/// <summary>
/// The prototype containing information about the bounty.
/// </summary>
[DataField(required: true)]
public ProtoId<CargoBountyPrototype> 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;
}
/// <summary>
/// Covers how a bounty was actually finished.
/// </summary>
public enum BountyResult
{
/// <summary>
/// Bounty was actually fulfilled and the goods sold
/// </summary>
Completed = 0,
/// <summary>
/// Bounty was explicitly skipped by some actor
/// </summary>
Skipped = 1,
}
}

View File

@@ -50,11 +50,13 @@ public sealed partial class CargoBountyConsoleComponent : Component
public sealed class CargoBountyConsoleState : BoundUserInterfaceState
{
public List<CargoBountyData> Bounties;
public List<CargoBountyHistoryData> History;
public TimeSpan UntilNextSkip;
public CargoBountyConsoleState(List<CargoBountyData> bounties, TimeSpan untilNextSkip)
public CargoBountyConsoleState(List<CargoBountyData> bounties, List<CargoBountyHistoryData> history, TimeSpan untilNextSkip)
{
Bounties = bounties;
History = history;
UntilNextSkip = untilNextSkip;
}
}

View File

@@ -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}