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
}
}
}
}