* Fix usages of TryIndex()
Most usages of TryIndex() were using it incorrectly. Checking whether prototype IDs specified in prototypes actually existed before using them. This is not appropriate as it's just hiding bugs that should be getting caught by the YAML linter and other tools. (#39115)
This then resulted in TryIndex() getting modified to log errors (94f98073b0), which is incorrect as it causes false-positive errors in proper uses of the API: external data validation. (#39098)
This commit goes through and checks every call site of TryIndex() to see whether they were correct. Most call sites were replaced with the new Resolve(), which is suitable for these "defensive programming" use cases.
Fixes #39115
Breaking change: while doing this I noticed IdCardComponent and related systems were erroneously using ProtoId<AccessLevelPrototype> for job prototypes. This has been corrected.
* fix tests
---------
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
543 lines
19 KiB
C#
543 lines
19 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
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;
|
|
|
|
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!;
|
|
|
|
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<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);
|
|
}
|
|
|
|
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.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
|
|
msg.PushNewline();
|
|
msg.AddText(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();
|
|
}
|
|
}
|
|
}
|