using System.Collections.Frozen; using System.Diagnostics.CodeAnalysis; using System.Linq; using Content.Server._NF.Access; using Content.Server.Administration.Logs; using Content.Server.CartridgeLoader; using Content.Server.Chat.Systems; using Content.Server.StationRecords.Systems; using Content.Shared._NF.Bank; using Content.Shared._NF.BountyContracts; using Content.Shared.Access.Systems; using Content.Shared.CartridgeLoader; using Content.Shared.Database; using Robust.Shared.Prototypes; namespace Content.Server._NF.BountyContracts; /// /// Used to control all bounty contracts placed by players. /// public sealed partial class BountyContractSystem : SharedBountyContractSystem { private ISawmill _sawmill = default!; [Dependency] private readonly CartridgeLoaderSystem _cartridgeLoader = default!; [Dependency] private readonly StationRecordsSystem _records = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly NFAccessSystemUtilities _accessUtils = default!; [Dependency] private readonly ChatSystem _chat = default!; [Dependency] private readonly IPrototypeManager _proto = default!; [Dependency] private readonly IAdminLogManager _adminLog = default!; public override void Initialize() { base.Initialize(); _sawmill = Logger.GetSawmill("bounty.contracts"); SubscribeLocalEvent(ContractInit); InitializeUi(); } private void ContractInit(Entity ent, ref ComponentInit ev) { SortedList> orderedCollections = new(); Dictionary, Dictionary> contracts = new(); foreach (var proto in _proto.EnumeratePrototypes()) { contracts[proto.ID] = new(); orderedCollections[proto.Order] = proto.ID; } ent.Comp.Contracts = contracts.ToFrozenDictionary(); ent.Comp.OrderedCollections = orderedCollections.Values.ToList(); } private BountyContractDataComponent? GetContracts() { TryComp(_sectorService.GetServiceEntity(), out BountyContractDataComponent? bountyContracts); return bountyContracts; } // Returns a list of all readable collections that a user can see. private List> GetReadableCollections(EntityUid user, BountyContractDataComponent? bounties = null) { var returnList = new List>(); if (bounties == null) { bounties = GetContracts(); // Nothing to read from, no read access if (bounties == null) return returnList; } if (bounties.Contracts == null) return returnList; var accessTags = _accessReader.FindAccessTags(user); foreach (var collection in bounties.OrderedCollections) { if (!_proto.TryIndex(collection, out var collectionProto)) continue; if (_accessUtils.IsAllowed(accessTags, collectionProto.ReadAccess, collectionProto.ReadGroups)) returnList.Add(collection); } return returnList; } private bool HasReadAccess(EntityUid user, ProtoId collection, BountyContractDataComponent? bounties = null) { if (bounties == null) { bounties = GetContracts(); // Nothing to read from, no read access if (bounties == null) return false; } if (!_proto.TryIndex(collection, out var collectionProto)) return false; return _accessUtils.IsAllowed(_accessReader.FindAccessTags(user), collectionProto.ReadAccess, collectionProto.ReadGroups); } private bool HasWriteAccess(EntityUid user, ProtoId collection, BountyContractDataComponent? bounties = null) { if (bounties == null) { bounties = GetContracts(); // Nothing to write to, no write access if (bounties == null) return false; } if (!_proto.TryIndex(collection, out var collectionProto)) return false; return _accessUtils.IsAllowed(_accessReader.FindAccessTags(user), collectionProto.WriteAccess, collectionProto.WriteGroups); } private bool HasDeleteAccess(EntityUid user, ProtoId collection, BountyContractDataComponent? bounties = null) { if (bounties == null) { bounties = GetContracts(); // Nothing to delete from, no write access if (bounties == null) return false; } if (!_proto.TryIndex(collection, out var collectionProto)) return false; return _accessUtils.IsAllowed(_accessReader.FindAccessTags(user), collectionProto.DeleteAccess, collectionProto.DeleteGroups); } /// /// Try to create a new bounty contract and put it in bounties list. /// /// Bounty contract collection (command, public, etc.) /// Bounty contract category (bounty head, construction, etc.) /// IC name for the contract bounty head. Can be players IC name or custom string. /// Cash reward for completing bounty. Can be zero. /// IC description of players crimes, details, etc. /// IC name of last known bounty vessel. Can be station/ship name or custom string. /// Optional DNA of the bounty head. /// Optional bounty poster IC name. /// Uid of the cartridge loader that created the bounty /// Should PDAs send a localized alert? /// The entity posting the bounty. /// New bounty contract. Null if contract creation failed. public BountyContract? TryCreateBountyContract(ProtoId collection, BountyContractCategory category, string name, int reward, EntityUid authorUid, EntityUid actor, string? description = null, string? vessel = null, string? dna = null, string? author = null) { var data = GetContracts(); if (data == null || data.Contracts == null || !data.Contracts.TryGetValue(collection, out var contracts) || !HasWriteAccess(authorUid, collection)) { return null; } if (name.Length > MaxNameLength) name = name.Substring(0, MaxNameLength); if (vessel != null && vessel.Length > MaxVesselLength) vessel = vessel.Substring(0, MaxVesselLength); if (description != null && description.Length > MaxDescriptionLength) description = description.Substring(0, MaxDescriptionLength); // create a new contract var contractId = data.LastId++; var contract = new BountyContract(contractId, category, name, reward, GetNetEntity(authorUid), dna, vessel, description, author); // try to save it if (!contracts.TryAdd(contractId, contract)) { _sawmill.Error($"Failed to create bounty contract with {contractId}! LastId: {data.LastId}."); return null; } var notificationType = BountyContractNotificationType.None; if (_proto.TryIndex(collection, out var bountyCollection)) notificationType = bountyCollection.NotificationType; LocId announcement = "bounty-contracts-announcement-generic-create"; if (CategoriesMeta.TryGetValue(category, out var categoryMeta) && categoryMeta.Announcement != null) announcement = categoryMeta.Announcement.Value; // Generate a notification if (notificationType == BountyContractNotificationType.PDA) { var sender = Loc.GetString("bounty-contracts-announcement-pda-name"); var target = !string.IsNullOrEmpty(contract.Vessel) && contract.Vessel != Loc.GetString("bounty-contracts-ui-create-vessel-unknown") ? $"{contract.Name} ({contract.Vessel})" : contract.Name; var msg = Loc.GetString(announcement, ("target", target), ("reward", BankSystemExtensions.ToSpesoString(contract.Reward))); var pdaList = EntityQueryEnumerator(); while (pdaList.MoveNext(out var loaderUid, out var loaderComp)) { if (_cartridgeLoader.TryGetProgram(loaderUid, out _, out var cartComp, true, loaderComp) && cartComp.NotificationsEnabled) { _cartridgeLoader.SendNotification(loaderUid, sender, msg, loaderComp); } } } else if (notificationType == BountyContractNotificationType.Radio) { var sender = Loc.GetString("bounty-contracts-announcement-radio-name"); var target = !string.IsNullOrEmpty(contract.Vessel) && contract.Vessel != Loc.GetString("bounty-contracts-ui-create-vessel-unknown") ? $"{contract.Name} ({contract.Vessel})" : contract.Name; var msg = Loc.GetString(announcement, ("target", target), ("reward", BankSystemExtensions.ToSpesoString(contract.Reward))); var color = Color.FromHex("#D7D7BE"); _chat.DispatchGlobalAnnouncement(msg, sender, false, colorOverride: color); } _adminLog.Add(LogType.BountyContractCreated, $"{ToPrettyString(actor):actor} posted a {category} bounty with ID {contractId} in the {collection} collection for ${reward}: {description ?? ""}"); return contract; } /// /// Try to get a bounty contract by its id. /// public bool TryGetContract(uint contractId, [NotNullWhen(true)] out BountyContract? contract) { contract = null; var data = GetContracts(); if (data == null || data.Contracts == null) return false; // Linear over # collections, should be a small set foreach (var collection in data.Contracts.Values) { if (collection.TryGetValue(contractId, out contract)) return true; } return false; } /// /// Try to get all bounty contracts available within a particular collection. /// public IEnumerable GetPermittedContracts(Entity cartridge, EntityUid loader, out ProtoId? newCollection) { newCollection = null; var data = GetContracts(); if (data == null || data.Contracts == null) return Enumerable.Empty(); if (cartridge.Comp.Collection != null) { if (data.Contracts.TryGetValue(cartridge.Comp.Collection.Value, out var contracts) && HasReadAccess(loader, cartridge.Comp.Collection.Value, data)) { newCollection = cartridge.Comp.Collection.Value; return contracts.Values; } } foreach (var collection in data.Contracts.Keys) { if (HasReadAccess(loader, collection, data)) { newCollection = collection; return data.Contracts[collection].Values; } } // No valid permitted contracts to get return Enumerable.Empty(); } /// /// Try to remove bounty contract by its id. /// /// True if contract was found and removed. public bool TryRemoveBountyContract(EntityUid authorUid, EntityUid actor, uint contractId) { var data = GetContracts(); if (data == null || data.Contracts == null) return false; foreach (var (collectionId, collection) in data.Contracts) { if (!collection.TryGetValue(contractId, out var contract)) continue; if (!HasDeleteAccess(authorUid, collectionId, data) && authorUid != GetEntity(contract.AuthorUid)) return false; collection.Remove(contractId); _adminLog.Add(LogType.BountyContractRemoved, $"{ToPrettyString(actor):actor} deleted bounty with ID {contractId}"); return true; } _sawmill.Warning($"Failed to remove bounty contract with {contractId}!"); return false; } public override void Update(float frameTime) { var cartList = EntityQueryEnumerator(); while (cartList.MoveNext(out var loaderUid, out var cartComponent)) { if (cartComponent.CreateEnabled) continue; if (_timing.CurTime >= cartComponent.NextCreate) { cartComponent.CreateEnabled = true; // TODO: update UI if on the create menu } } } }