using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server.Cargo.Components; using Content.Server.Labels; using Content.Server.Paper; using Content.Shared.Cargo; using Content.Shared.Cargo.Components; using Content.Shared.Cargo.Prototypes; using Content.Shared.Database; using JetBrains.Annotations; using Robust.Server.Containers; using Robust.Shared.Collections; 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!; private void InitializeBounty() { SubscribeLocalEvent(OnBountyConsoleOpened); SubscribeLocalEvent(OnPrintLabelMessage); SubscribeLocalEvent(OnGetBountyPrice); SubscribeLocalEvent(OnSold); SubscribeLocalEvent(OnMapInit); } private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args) { if (_station.GetOwningStation(uid) is not { } station || !TryComp(station, out var bountyDb)) return; _uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties)); } 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, bounty.Value); _audio.PlayPvs(component.PrintSound, uid); } public void SetupBountyLabel(EntityUid uid, 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; 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 (_station.GetOwningStation(uid) is not { } station) return; if (!TryGetBountyFromId(station, component.Id, out var bounty)) return; if (!_protoMan.TryIndex(bounty.Value.Bounty, out var bountyProtoype) ||!IsBountyComplete(container.Owner, bountyProtoype)) return; args.Handled = true; component.Calculating = true; args.Price = bountyProtoype.Reward - _pricing.GetPrice(container.Owner); component.Calculating = false; } private void OnSold(ref EntitySoldEvent args) { var containerQuery = GetEntityQuery(); var labelQuery = GetEntityQuery(); foreach (var sold in args.Sold) { if (!containerQuery.TryGetComponent(sold, out var containerMan)) continue; // make sure this label was actually applied to a crate. if (!_container.TryGetContainer(sold, LabelSystem.ContainerName, out var container, containerMan)) continue; if (container.ContainedEntities.FirstOrNull() is not { } label || !labelQuery.TryGetComponent(label, out var component)) continue; if (!TryGetBountyFromId(args.Station, component.Id, out var bounty)) continue; if (!IsBountyComplete(container.Owner, bounty.Value)) continue; TryRemoveBounty(args.Station, bounty.Value); FillBountyDatabase(args.Station); _adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled"); } } 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 bool IsBountyComplete(EntityUid container, CargoBountyData data) { if (!_protoMan.TryIndex(data.Bounty, out var proto)) return false; return IsBountyComplete(container, proto.Entries); } 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) { var contained = new HashSet(); if (TryComp(container, out var containers)) { foreach (var con in containers.Containers.Values) { if (con.ID == LabelSystem.ContainerName) continue; foreach (var ent in con.ContainedEntities) { contained.Add(ent); } } } return IsBountyComplete(contained, entries); } public bool IsBountyComplete(HashSet entities, IEnumerable entries) { 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 (!entry.Whitelist.IsValid(entity, EntityManager)) continue; count++; temp.Add(entity); if (count >= entry.Amount) break; } if (count < entry.Amount) return false; foreach (var ent in temp) { entities.Remove(ent); } } return true; } [PublicAPI] public bool TryAddBounty(EntityUid uid, StationCargoBountyDatabaseComponent? component = null) { // todo: consider making the cargo bounties weighted. var bounty = _random.Pick(_protoMan.EnumeratePrototypes().ToList()); 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; var duration = MathF.Round(_random.NextFloat(component.MinBountyTime, component.MaxBountyTime) / 15) * 15; var endTime = _timing.CurTime + TimeSpan.FromSeconds(duration); component.Bounties.Add(new CargoBountyData(component.TotalBounties, bounty.ID, endTime)); _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, int 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, int 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; _uiSystem.TrySetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties), ui: ui); } } private void UpdateBounty() { var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var bountyDatabase)) { var bounties = new ValueList(bountyDatabase.Bounties); foreach (var bounty in bounties) { if (_timing.CurTime < bounty.EndTime) continue; TryRemoveBounty(uid, bounty, bountyDatabase); FillBountyDatabase(uid, bountyDatabase); } } } }