Files
Delta-v/Content.Server/Cargo/Systems/CargoSystem.Bounty.cs
T
dbe955ca17 Cargo bounty console improvements (with changes) (#6306)
* simple fix for "Approved by: Self" on all cargo invoices

* port of ruddygreat's bounty console improvements to DeltaV

* modified tooltips and fixed a few importing errors

* StatusLabel in BountyEntry.xaml.cs

* final stupid tweak

* removed unrelated bugfix PR that snuck into this branch somehow

* Cargo bounty fix requests

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

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

---------

Co-authored-by: cargomouse <dboozlebeep@gmail.com>
Co-authored-by: Jessica Harvey <itsjessagain23@gmail.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2026-07-20 18:23:28 -05:00

614 lines
22 KiB
C#

using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Content.Server.Access.Systems; // DeltaV
using Content.Server.Cargo.Components;
using Content.Server.NameIdentifier;
using Content.Shared.Access.Components;
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.Labels.EntitySystems;
using Content.Shared.NameIdentifier;
using Content.Shared.Paper;
using Content.Shared.Stacks;
using Content.Shared.Whitelist;
using JetBrains.Annotations;
using Robust.Server.Containers;
using Robust.Shared.Containers;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Content.Shared._DV.Cargo.Components; // DeltaV: Bounty claim messages
namespace Content.Server.Cargo.Systems;
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 IdCardSystem _idCard = default!; // DeltaV
private static readonly ProtoId<NameIdentifierGroupPrototype> BountyNameIdentifierGroup = "Bounty";
private EntityQuery<StackComponent> _stackQuery;
private EntityQuery<ContainerManagerComponent> _containerQuery;
private EntityQuery<CargoBountyLabelComponent> _bountyLabelQuery;
private void InitializeBounty()
{
SubscribeLocalEvent<CargoBountyConsoleComponent, BoundUIOpenedEvent>(OnBountyConsoleOpened);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountyPrintLabelMessage>(OnPrintLabelMessage);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountySkipMessage>(OnSkipBountyMessage);
SubscribeLocalEvent<CargoBountyConsoleComponent, BountyClaimedMessage>(OnBountyClaimedMessage); // DeltaV
SubscribeLocalEvent<CargoBountyConsoleComponent, BountySetStatusMessage>(OnSetBountyStatusMessage); // DeltaV
SubscribeLocalEvent<CargoBountyLabelComponent, PriceCalculationEvent>(OnGetBountyPrice);
SubscribeLocalEvent<EntitySoldEvent>(OnSold);
SubscribeLocalEvent<StationCargoBountyDatabaseComponent, MapInitEvent>(OnMapInit);
_stackQuery = GetEntityQuery<StackComponent>();
_containerQuery = GetEntityQuery<ContainerManagerComponent>();
_bountyLabelQuery = GetEntityQuery<CargoBountyLabelComponent>();
}
private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args)
{
if (_station.GetOwningStation(uid) is not { } station ||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDb))
return;
var untilNextSkip = bountyDb.NextSkipTime - Timing.CurTime;
_uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip));
}
private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
{
if (Timing.CurTime < component.NextPrintTime)
return;
if (_station.GetOwningStation(uid) is not { } station)
return;
if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
return;
var label = Spawn(component.BountyLabelId, Transform(uid).Coordinates);
component.NextPrintTime = Timing.CurTime + component.PrintDelay;
SetupBountyLabel(label, station, bounty.Value);
_audio.PlayPvs(component.PrintSound, uid);
}
private void OnSkipBountyMessage(EntityUid uid, CargoBountyConsoleComponent component, BountySkipMessage args)
{
if (_station.GetOwningStation(uid) is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
return;
if (Timing.CurTime < db.NextSkipTime)
return;
if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
return;
if (args.Actor is not { Valid: true } mob)
return;
if (TryComp<AccessReaderComponent>(uid, out var accessReaderComponent) &&
!_accessReaderSystem.IsAllowed(mob, uid, accessReaderComponent))
{
if (Timing.CurTime >= component.NextDenySoundTime)
{
component.NextDenySoundTime = Timing.CurTime + component.DenySoundDelay;
_audio.PlayPvs(component.DenySound, uid);
}
return;
}
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, db.History, untilNextSkip));
_audio.PlayPvs(component.SkipSound, uid);
}
// Begin DeltaV bounty claim system
private void OnSetBountyStatusMessage(Entity<CargoBountyConsoleComponent> ent, ref BountySetStatusMessage args)
{
if (_station.GetOwningStation(ent.Owner) is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDbComp))
return;
for (var i = 0; i < bountyDbComp.Bounties.Count; i++)
{
var bounty = bountyDbComp.Bounties[i];
if (bounty.Id != args.BountyId)
continue;
var newData = new CargoBountyData
{
Id = bounty.Id,
Bounty = bounty.Bounty,
ClaimedBy = bounty.ClaimedBy,
Status = (CargoBountyStatus)args.Status,
};
bountyDbComp.Bounties[i] = newData;
}
var untilNextSkip = bountyDbComp.NextSkipTime - Timing.CurTime;
_uiSystem.SetUiState(ent.Owner, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDbComp.Bounties, bountyDbComp.History, untilNextSkip));
}
private void OnBountyClaimedMessage(Entity<CargoBountyConsoleComponent> ent, ref BountyClaimedMessage args)
{
if (_station.GetOwningStation(ent.Owner) is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDbComp))
return;
for (var i = 0; i < bountyDbComp.Bounties.Count; i++)
{
var bounty = bountyDbComp.Bounties[i];
if (bounty.Id != args.BountyId)
continue;
string name;
if (_idCard.TryFindIdCard(args.Actor, out var idCard) && idCard.Comp.FullName != null)
{
name = idCard.Comp.FullName;
}
else
{
name = Loc.GetString("bounty-console-claimed-by-unknown");
}
var newData = new CargoBountyData
{
Id = bounty.Id,
Bounty = bounty.Bounty,
ClaimedBy = name.Equals(bounty.ClaimedBy) ? string.Empty : name,
Status = bounty.Status,
};
bountyDbComp.Bounties[i] = newData;
}
var untilNextSkip = bountyDbComp.NextSkipTime - Timing.CurTime;
_uiSystem.SetUiState(ent.Owner, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDbComp.Bounties, bountyDbComp.History, untilNextSkip));
}
// End DeltaV bounty claim system
public void SetupBountyLabel(EntityUid uid, EntityUid stationId, CargoBountyData bounty, PaperComponent? paper = null, CargoBountyLabelComponent? label = null)
{
if (!Resolve(uid, ref paper, ref label) || !_protoMan.Resolve<CargoBountyPrototype>(bounty.Bounty, out var prototype))
return;
label.Id = bounty.Id;
label.AssociatedStationId = stationId;
var msg = new FormattedMessage();
msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
msg.PushNewline();
msg.AddMarkupOrThrow(Loc.GetString("bounty-manifest-list-start"));
msg.PushNewline();
foreach (var entry in prototype.Entries)
{
msg.AddMarkupOrThrow($"- {Loc.GetString("bounty-console-manifest-entry",
("amount", entry.Amount),
("item", Loc.GetString(entry.Name)))}");
msg.PushNewline();
}
msg.AddMarkupOrThrow(Loc.GetString("bounty-console-manifest-reward", ("reward", prototype.Reward)));
_paperSystem.SetContent((uid, paper), msg.ToMarkup());
}
/// <summary>
/// Bounties do not sell for any currency. The reward for a bounty is
/// calculated after it is sold separately from the selling system.
/// </summary>
private void OnGetBountyPrice(EntityUid uid, CargoBountyLabelComponent component, ref PriceCalculationEvent args)
{
if (args.Handled || component.Calculating)
return;
// make sure this label was actually applied to a crate.
if (!_container.TryGetContainingContainer((uid, null, null), out var container) || container.ID != LabelSystem.ContainerName)
return;
if (component.AssociatedStationId is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var database))
return;
if (database.CheckedBounties.Contains(component.Id))
return;
if (!TryGetBountyFromId(station, component.Id, out var bounty, database))
return;
if (!_protoMan.Resolve(bounty.Value.Bounty, out var bountyPrototype) ||
!IsBountyComplete(container.Owner, bountyPrototype))
return;
database.CheckedBounties.Add(component.Id);
args.Handled = true;
component.Calculating = true;
args.Price = bountyPrototype.Reward - _pricing.GetPrice(container.Owner);
component.Calculating = false;
}
private void OnSold(ref EntitySoldEvent args)
{
foreach (var sold in args.Sold)
{
if (!TryGetBountyLabel(sold, out _, out var component))
continue;
if (component.AssociatedStationId is not { } station || !TryGetBountyFromId(station, component.Id, out var bounty))
{
continue;
}
if (!IsBountyComplete(sold, bounty.Value))
{
continue;
}
TryRemoveBounty(station, bounty.Value, false);
FillBountyDatabase(station);
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
}
}
private bool TryGetBountyLabel(EntityUid uid,
[NotNullWhen(true)] out EntityUid? labelEnt,
[NotNullWhen(true)] out CargoBountyLabelComponent? labelComp)
{
labelEnt = null;
labelComp = null;
if (!_containerQuery.TryGetComponent(uid, out var containerMan))
return false;
// make sure this label was actually applied to a crate.
if (!_container.TryGetContainer(uid, LabelSystem.ContainerName, out var container, containerMan))
return false;
if (container.ContainedEntities.FirstOrNull() is not { } label ||
!_bountyLabelQuery.TryGetComponent(label, out var component))
return false;
labelEnt = label;
labelComp = component;
return true;
}
private void OnMapInit(EntityUid uid, StationCargoBountyDatabaseComponent component, MapInitEvent args)
{
FillBountyDatabase(uid, component);
}
/// <summary>
/// Fills up the bounty database with random bounties.
/// </summary>
public void FillBountyDatabase(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return;
while (component.Bounties.Count < component.MaxBounties)
{
if (!TryAddBounty(uid, component))
break;
}
UpdateBountyConsoles();
}
public void RerollBountyDatabase(Entity<StationCargoBountyDatabaseComponent?> entity)
{
if (!Resolve(entity, ref entity.Comp))
return;
entity.Comp.Bounties.Clear();
FillBountyDatabase(entity);
}
public bool IsBountyComplete(EntityUid container, out HashSet<EntityUid> bountyEntities)
{
if (!TryGetBountyLabel(container, out _, out var component))
{
bountyEntities = new();
return false;
}
var station = component.AssociatedStationId;
if (station == null)
{
bountyEntities = new();
return false;
}
if (!TryGetBountyFromId(station.Value, component.Id, out var bounty))
{
bountyEntities = new();
return false;
}
return IsBountyComplete(container, bounty.Value, out bountyEntities);
}
public bool IsBountyComplete(EntityUid container, CargoBountyData data)
{
return IsBountyComplete(container, data, out _);
}
public bool IsBountyComplete(EntityUid container, CargoBountyData data, out HashSet<EntityUid> bountyEntities)
{
if (!_protoMan.Resolve(data.Bounty, out var proto))
{
bountyEntities = new();
return false;
}
return IsBountyComplete(container, proto.Entries, out bountyEntities);
}
public bool IsBountyComplete(EntityUid container, string id)
{
if (!_protoMan.TryIndex<CargoBountyPrototype>(id, out var proto))
return false;
return IsBountyComplete(container, proto.Entries);
}
public bool IsBountyComplete(EntityUid container, ProtoId<CargoBountyPrototype> prototypeId)
{
var prototype = _protoMan.Index(prototypeId);
return IsBountyComplete(container, prototype.Entries);
}
public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
{
return IsBountyComplete(container, prototype.Entries);
}
public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries)
{
return IsBountyComplete(container, entries, out _);
}
public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities)
{
return IsBountyComplete(GetBountyEntities(container), entries, out bountyEntities);
}
/// <summary>
/// Determines whether the <paramref name="entity"/> meets the criteria for the bounty <paramref name="entry"/>.
/// </summary>
/// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns>
public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry)
{
if (!_whitelistSys.IsValid(entry.Whitelist, entity))
return false;
if (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))
return false;
return true;
}
public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities)
{
bountyEntities = new();
foreach (var entry in entries)
{
var count = 0;
// store entities that already satisfied an
// entry so we don't double-count them.
var temp = new HashSet<EntityUid>();
foreach (var entity in entities)
{
if (!IsValidBountyEntry(entity, entry))
continue;
count += _stackQuery.CompOrNull(entity)?.Count ?? 1;
temp.Add(entity);
if (count >= entry.Amount)
break;
}
if (count < entry.Amount)
return false;
foreach (var ent in temp)
{
entities.Remove(ent);
bountyEntities.Add(ent);
}
}
return true;
}
private HashSet<EntityUid> GetBountyEntities(EntityUid uid)
{
var entities = new HashSet<EntityUid>
{
uid
};
if (!TryComp<ContainerManagerComponent>(uid, out var containers))
return entities;
foreach (var container in containers.Containers.Values)
{
foreach (var ent in container.ContainedEntities)
{
if (_bountyLabelQuery.HasComponent(ent))
continue;
var children = GetBountyEntities(ent);
foreach (var child in children)
{
entities.Add(child);
}
}
}
return entities;
}
[PublicAPI]
public bool TryAddBounty(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
// todo: consider making the cargo bounties weighted.
var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>()
.Where(p => p.Group == component.Group)
.ToList();
var filteredBounties = new List<CargoBountyPrototype>();
foreach (var proto in allBounties)
{
if (component.Bounties.Any(b => b.Bounty == proto.ID))
continue;
filteredBounties.Add(proto);
}
var pool = filteredBounties.Count == 0 ? allBounties : filteredBounties;
var bounty = _random.Pick(pool);
return TryAddBounty(uid, bounty, component);
}
[PublicAPI]
public bool TryAddBounty(EntityUid uid, string bountyId, StationCargoBountyDatabaseComponent? component = null)
{
if (!_protoMan.TryIndex<CargoBountyPrototype>(bountyId, out var bounty))
{
return false;
}
return TryAddBounty(uid, bounty, component);
}
public bool TryAddBounty(EntityUid uid, CargoBountyPrototype bounty, StationCargoBountyDatabaseComponent? component = null)
{
if (!Resolve(uid, ref component))
return false;
if (component.Bounties.Count >= component.MaxBounties)
return false;
_nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
var newBounty = new CargoBountyData(bounty, randomVal);
// This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
if (component.Bounties.Any(b => b.Id == newBounty.Id))
{
Log.Error("Failed to add bounty {ID} because another one with the same ID already existed!", newBounty.Id);
return false;
}
component.Bounties.Add(new CargoBountyData(bounty, randomVal));
_adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
component.TotalBounties++;
return true;
}
[PublicAPI]
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
string dataId,
bool skipped,
EntityUid? actor = null)
{
if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp))
return false;
return TryRemoveBounty(ent, data.Value, skipped, actor);
}
public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
CargoBountyData data,
bool skipped,
EntityUid? actor = null)
{
if (!Resolve(ent, ref ent.Comp))
return false;
for (var i = 0; i < ent.Comp.Bounties.Count; i++)
{
if (ent.Comp.Bounties[i].Id == data.Id)
{
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,
Timing.CurTime,
actorName));
ent.Comp.Bounties.RemoveAt(i);
return true;
}
}
return false;
}
public bool TryGetBountyFromId(
EntityUid uid,
string id,
[NotNullWhen(true)] out CargoBountyData? bounty,
StationCargoBountyDatabaseComponent? component = null)
{
bounty = null;
if (!Resolve(uid, ref component))
return false;
foreach (var bountyData in component.Bounties)
{
if (bountyData.Id != id)
continue;
bounty = bountyData;
break;
}
return bounty != null;
}
public void UpdateBountyConsoles()
{
var query = EntityQueryEnumerator<CargoBountyConsoleComponent, UserInterfaceComponent>();
while (query.MoveNext(out var uid, out _, out var ui))
{
if (_station.GetOwningStation(uid) is not { } station ||
!TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
{
continue;
}
var untilNextSkip = db.NextSkipTime - Timing.CurTime;
_uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
}
}
private void UpdateBounty()
{
var query = EntityQueryEnumerator<StationCargoBountyDatabaseComponent>();
while (query.MoveNext(out var bountyDatabase))
{
bountyDatabase.CheckedBounties.Clear();
}
}
}