using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Labels; using Content.Server.NameIdentifier; using Content.Server.Paper; 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.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] private const string BountyNameIdentifierGroup = "Bounty"; private EntityQuery _stackQuery; private EntityQuery _containerQuery; private EntityQuery _bountyLabelQuery; private void InitializeBounty() { SubscribeLocalEvent(OnBountyConsoleOpened); SubscribeLocalEvent(OnPrintLabelMessage); SubscribeLocalEvent(OnSkipBountyMessage); SubscribeLocalEvent(OnGetBountyPrice); SubscribeLocalEvent(OnSold); SubscribeLocalEvent(OnMapInit); _stackQuery = GetEntityQuery(); _containerQuery = GetEntityQuery(); _bountyLabelQuery = GetEntityQuery(); } private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args) { if (_station.GetOwningStation(uid) is not { } station || !TryComp(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(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(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(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.AddMarkup($"- {Loc.GetString("bounty-console-manifest-entry", ("amount", entry.Amount), ("item", Loc.GetString(entry.Name)))}"); msg.PushNewline(); } _paperSystem.SetContent(uid, msg.ToMarkup(), paper); } /// /// Bounties do not sell for any currency. The reward for a bounty is /// calculated after it is sold separately from the selling system. /// 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, out var container) || container.ID != LabelSystem.ContainerName) return; if (component.AssociatedStationId is not { } station || !TryComp(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); } /// /// Fills up the bounty database with random bounties. /// 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 entity) { if (!Resolve(entity, ref entity.Comp)) return; entity.Comp.Bounties.Clear(); FillBountyDatabase(entity); } public bool IsBountyComplete(EntityUid container, out HashSet 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 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(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 entries) { return IsBountyComplete(container, entries, out _); } public bool IsBountyComplete(EntityUid container, IEnumerable entries, out HashSet bountyEntities) { return IsBountyComplete(GetBountyEntities(container), entries, out bountyEntities); } public bool IsBountyComplete(HashSet entities, IEnumerable entries, out HashSet 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(); foreach (var entity in entities) { if (!_whitelistSys.IsValid(entry.Whitelist, entity) || (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))) 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 GetBountyEntities(EntityUid uid) { var entities = new HashSet { uid }; if (!TryComp(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().ToList(); var filteredBounties = new List(); 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(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); 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(); while (query.MoveNext(out var uid, out _, out var ui)) { if (_station.GetOwningStation(uid) is not { } station || !TryComp(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(); while (query.MoveNext(out var bountyDatabase)) { bountyDatabase.CheckedBounties.Clear(); } } }