if (message is not CargoBountyConsoleState state)
return;
- _menu?.UpdateEntries(state.Bounties, state.UntilNextSkip);
+ _menu?.UpdateEntries(state.Bounties, state.History, state.UntilNextSkip);
}
}
--- /dev/null
+<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>
--- /dev/null
+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 ?? "")));
+ }
+ }
+}
<PanelContainer.PanelOverride>
<gfx:StyleBoxFlat BackgroundColor="#1B1B1E" />
</PanelContainer.PanelOverride>
- <ScrollContainer HScrollEnabled="False"
- HorizontalExpand="True"
- VerticalExpand="True">
- <BoxContainer Name="BountyEntriesContainer"
- Orientation="Vertical"
- VerticalExpand="True"
- HorizontalExpand="True">
- </BoxContainer>
- </ScrollContainer>
+ <TabContainer Name="MasterTabContainer" VerticalExpand="True" HorizontalExpand="True">
+ <ScrollContainer HScrollEnabled="False"
+ HorizontalExpand="True"
+ VerticalExpand="True">
+ <BoxContainer Name="BountyEntriesContainer"
+ Orientation="Vertical"
+ VerticalExpand="True"
+ 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">
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)
{
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]));
+ }
+ }
}
}
/// <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>
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;
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;
[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";
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)
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);
}
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");
}
}
[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;
}
}
}
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));
}
}
--- /dev/null
+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,
+ }
+}
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;
}
}
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}