* Make NameIdentifier Ids get refreshed after round restarts Before this commit the existing values would just get shuffled. This means that eventually the server would run out of ids to give to new entities for different groups. As a result everything would get id 0 * Comply with what seemingly is the convention for sawmills * Make it impossible to insert a bounty with a duplicate id * Reduce duplication * Remove unused sawmill * Fix rustbrain and skill issue * Aaaa * Apply suggestions from code review --------- Co-authored-by: Pieter-Jan Briers <pieterjan.briers@gmail.com>
508 lines
18 KiB
C#
508 lines
18 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using Content.Server.Cargo.Components;
|
|
using Content.Server.Labels;
|
|
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.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.Random;
|
|
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!;
|
|
|
|
[ValidatePrototypeId<NameIdentifierGroupPrototype>]
|
|
private const string 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, 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))
|
|
{
|
|
_audio.PlayPvs(component.DenySound, uid);
|
|
return;
|
|
}
|
|
|
|
if (!TryRemoveBounty(station, bounty.Value))
|
|
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));
|
|
_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.TryIndex<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.TryIndex(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);
|
|
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.TryIndex(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, 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>().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(EntityUid uid, string dataId, StationCargoBountyDatabaseComponent? component = null)
|
|
{
|
|
if (!TryGetBountyFromId(uid, dataId, out var data, component))
|
|
return false;
|
|
|
|
return TryRemoveBounty(uid, data.Value, component);
|
|
}
|
|
|
|
public bool TryRemoveBounty(EntityUid uid, CargoBountyData data, StationCargoBountyDatabaseComponent? component = null)
|
|
{
|
|
if (!Resolve(uid, ref component))
|
|
return false;
|
|
|
|
for (var i = 0; i < component.Bounties.Count; i++)
|
|
{
|
|
if (component.Bounties[i].Id == data.Id)
|
|
{
|
|
component.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, untilNextSkip));
|
|
}
|
|
}
|
|
|
|
private void UpdateBounty()
|
|
{
|
|
var query = EntityQueryEnumerator<StationCargoBountyDatabaseComponent>();
|
|
while (query.MoveNext(out var bountyDatabase))
|
|
{
|
|
bountyDatabase.CheckedBounties.Clear();
|
|
}
|
|
}
|
|
}
|