CargoSystem.Bounty.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Server.Cargo.Components;
  4. using Content.Server.Labels;
  5. using Content.Server.NameIdentifier;
  6. using Content.Shared.Access.Components;
  7. using Content.Shared.Cargo;
  8. using Content.Shared.Cargo.Components;
  9. using Content.Shared.Cargo.Prototypes;
  10. using Content.Shared.Database;
  11. using Content.Shared.IdentityManagement;
  12. using Content.Shared.NameIdentifier;
  13. using Content.Shared.Paper;
  14. using Content.Shared.Stacks;
  15. using Content.Shared.Whitelist;
  16. using JetBrains.Annotations;
  17. using Robust.Server.Containers;
  18. using Robust.Shared.Containers;
  19. using Robust.Shared.Random;
  20. using Robust.Shared.Timing;
  21. using Robust.Shared.Utility;
  22. namespace Content.Server.Cargo.Systems;
  23. public sealed partial class CargoSystem
  24. {
  25. [Dependency] private readonly ContainerSystem _container = default!;
  26. [Dependency] private readonly NameIdentifierSystem _nameIdentifier = default!;
  27. [Dependency] private readonly EntityWhitelistSystem _whitelistSys = default!;
  28. [Dependency] private readonly IGameTiming _gameTiming = default!;
  29. [ValidatePrototypeId<NameIdentifierGroupPrototype>]
  30. private const string BountyNameIdentifierGroup = "Bounty";
  31. private EntityQuery<StackComponent> _stackQuery;
  32. private EntityQuery<ContainerManagerComponent> _containerQuery;
  33. private EntityQuery<CargoBountyLabelComponent> _bountyLabelQuery;
  34. private void InitializeBounty()
  35. {
  36. SubscribeLocalEvent<CargoBountyConsoleComponent, BoundUIOpenedEvent>(OnBountyConsoleOpened);
  37. SubscribeLocalEvent<CargoBountyConsoleComponent, BountyPrintLabelMessage>(OnPrintLabelMessage);
  38. SubscribeLocalEvent<CargoBountyConsoleComponent, BountySkipMessage>(OnSkipBountyMessage);
  39. SubscribeLocalEvent<CargoBountyLabelComponent, PriceCalculationEvent>(OnGetBountyPrice);
  40. SubscribeLocalEvent<EntitySoldEvent>(OnSold);
  41. SubscribeLocalEvent<StationCargoBountyDatabaseComponent, MapInitEvent>(OnMapInit);
  42. _stackQuery = GetEntityQuery<StackComponent>();
  43. _containerQuery = GetEntityQuery<ContainerManagerComponent>();
  44. _bountyLabelQuery = GetEntityQuery<CargoBountyLabelComponent>();
  45. }
  46. private void OnBountyConsoleOpened(EntityUid uid, CargoBountyConsoleComponent component, BoundUIOpenedEvent args)
  47. {
  48. if (_station.GetOwningStation(uid) is not { } station ||
  49. !TryComp<StationCargoBountyDatabaseComponent>(station, out var bountyDb))
  50. return;
  51. var untilNextSkip = bountyDb.NextSkipTime - _timing.CurTime;
  52. _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(bountyDb.Bounties, bountyDb.History, untilNextSkip));
  53. }
  54. private void OnPrintLabelMessage(EntityUid uid, CargoBountyConsoleComponent component, BountyPrintLabelMessage args)
  55. {
  56. if (_timing.CurTime < component.NextPrintTime)
  57. return;
  58. if (_station.GetOwningStation(uid) is not { } station)
  59. return;
  60. if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
  61. return;
  62. var label = Spawn(component.BountyLabelId, Transform(uid).Coordinates);
  63. component.NextPrintTime = _timing.CurTime + component.PrintDelay;
  64. SetupBountyLabel(label, station, bounty.Value);
  65. _audio.PlayPvs(component.PrintSound, uid);
  66. }
  67. private void OnSkipBountyMessage(EntityUid uid, CargoBountyConsoleComponent component, BountySkipMessage args)
  68. {
  69. if (_station.GetOwningStation(uid) is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
  70. return;
  71. if (_timing.CurTime < db.NextSkipTime)
  72. return;
  73. if (!TryGetBountyFromId(station, args.BountyId, out var bounty))
  74. return;
  75. if (args.Actor is not { Valid: true } mob)
  76. return;
  77. if (TryComp<AccessReaderComponent>(uid, out var accessReaderComponent) &&
  78. !_accessReaderSystem.IsAllowed(mob, uid, accessReaderComponent))
  79. {
  80. _audio.PlayPvs(component.DenySound, uid);
  81. return;
  82. }
  83. if (!TryRemoveBounty(station, bounty.Value, true, args.Actor))
  84. return;
  85. FillBountyDatabase(station);
  86. db.NextSkipTime = _timing.CurTime + db.SkipDelay;
  87. var untilNextSkip = db.NextSkipTime - _timing.CurTime;
  88. _uiSystem.SetUiState(uid, CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
  89. _audio.PlayPvs(component.SkipSound, uid);
  90. }
  91. public void SetupBountyLabel(EntityUid uid, EntityUid stationId, CargoBountyData bounty, PaperComponent? paper = null, CargoBountyLabelComponent? label = null)
  92. {
  93. if (!Resolve(uid, ref paper, ref label) || !_protoMan.TryIndex<CargoBountyPrototype>(bounty.Bounty, out var prototype))
  94. return;
  95. label.Id = bounty.Id;
  96. label.AssociatedStationId = stationId;
  97. var msg = new FormattedMessage();
  98. msg.AddText(Loc.GetString("bounty-manifest-header", ("id", bounty.Id)));
  99. msg.PushNewline();
  100. msg.AddText(Loc.GetString("bounty-manifest-list-start"));
  101. msg.PushNewline();
  102. foreach (var entry in prototype.Entries)
  103. {
  104. msg.AddMarkupOrThrow($"- {Loc.GetString("bounty-console-manifest-entry",
  105. ("amount", entry.Amount),
  106. ("item", Loc.GetString(entry.Name)))}");
  107. msg.PushNewline();
  108. }
  109. msg.AddMarkupOrThrow(Loc.GetString("bounty-console-manifest-reward", ("reward", prototype.Reward)));
  110. _paperSystem.SetContent((uid, paper), msg.ToMarkup());
  111. }
  112. /// <summary>
  113. /// Bounties do not sell for any currency. The reward for a bounty is
  114. /// calculated after it is sold separately from the selling system.
  115. /// </summary>
  116. private void OnGetBountyPrice(EntityUid uid, CargoBountyLabelComponent component, ref PriceCalculationEvent args)
  117. {
  118. if (args.Handled || component.Calculating)
  119. return;
  120. // make sure this label was actually applied to a crate.
  121. if (!_container.TryGetContainingContainer((uid, null, null), out var container) || container.ID != LabelSystem.ContainerName)
  122. return;
  123. if (component.AssociatedStationId is not { } station || !TryComp<StationCargoBountyDatabaseComponent>(station, out var database))
  124. return;
  125. if (database.CheckedBounties.Contains(component.Id))
  126. return;
  127. if (!TryGetBountyFromId(station, component.Id, out var bounty, database))
  128. return;
  129. if (!_protoMan.TryIndex(bounty.Value.Bounty, out var bountyPrototype) ||
  130. !IsBountyComplete(container.Owner, bountyPrototype))
  131. return;
  132. database.CheckedBounties.Add(component.Id);
  133. args.Handled = true;
  134. component.Calculating = true;
  135. args.Price = bountyPrototype.Reward - _pricing.GetPrice(container.Owner);
  136. component.Calculating = false;
  137. }
  138. private void OnSold(ref EntitySoldEvent args)
  139. {
  140. foreach (var sold in args.Sold)
  141. {
  142. if (!TryGetBountyLabel(sold, out _, out var component))
  143. continue;
  144. if (component.AssociatedStationId is not { } station || !TryGetBountyFromId(station, component.Id, out var bounty))
  145. {
  146. continue;
  147. }
  148. if (!IsBountyComplete(sold, bounty.Value))
  149. {
  150. continue;
  151. }
  152. TryRemoveBounty(station, bounty.Value, false);
  153. FillBountyDatabase(station);
  154. _adminLogger.Add(LogType.Action, LogImpact.Low, $"Bounty \"{bounty.Value.Bounty}\" (id:{bounty.Value.Id}) was fulfilled");
  155. }
  156. }
  157. private bool TryGetBountyLabel(EntityUid uid,
  158. [NotNullWhen(true)] out EntityUid? labelEnt,
  159. [NotNullWhen(true)] out CargoBountyLabelComponent? labelComp)
  160. {
  161. labelEnt = null;
  162. labelComp = null;
  163. if (!_containerQuery.TryGetComponent(uid, out var containerMan))
  164. return false;
  165. // make sure this label was actually applied to a crate.
  166. if (!_container.TryGetContainer(uid, LabelSystem.ContainerName, out var container, containerMan))
  167. return false;
  168. if (container.ContainedEntities.FirstOrNull() is not { } label ||
  169. !_bountyLabelQuery.TryGetComponent(label, out var component))
  170. return false;
  171. labelEnt = label;
  172. labelComp = component;
  173. return true;
  174. }
  175. private void OnMapInit(EntityUid uid, StationCargoBountyDatabaseComponent component, MapInitEvent args)
  176. {
  177. FillBountyDatabase(uid, component);
  178. }
  179. /// <summary>
  180. /// Fills up the bounty database with random bounties.
  181. /// </summary>
  182. public void FillBountyDatabase(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
  183. {
  184. if (!Resolve(uid, ref component))
  185. return;
  186. while (component.Bounties.Count < component.MaxBounties)
  187. {
  188. if (!TryAddBounty(uid, component))
  189. break;
  190. }
  191. UpdateBountyConsoles();
  192. }
  193. public void RerollBountyDatabase(Entity<StationCargoBountyDatabaseComponent?> entity)
  194. {
  195. if (!Resolve(entity, ref entity.Comp))
  196. return;
  197. entity.Comp.Bounties.Clear();
  198. FillBountyDatabase(entity);
  199. }
  200. public bool IsBountyComplete(EntityUid container, out HashSet<EntityUid> bountyEntities)
  201. {
  202. if (!TryGetBountyLabel(container, out _, out var component))
  203. {
  204. bountyEntities = new();
  205. return false;
  206. }
  207. var station = component.AssociatedStationId;
  208. if (station == null)
  209. {
  210. bountyEntities = new();
  211. return false;
  212. }
  213. if (!TryGetBountyFromId(station.Value, component.Id, out var bounty))
  214. {
  215. bountyEntities = new();
  216. return false;
  217. }
  218. return IsBountyComplete(container, bounty.Value, out bountyEntities);
  219. }
  220. public bool IsBountyComplete(EntityUid container, CargoBountyData data)
  221. {
  222. return IsBountyComplete(container, data, out _);
  223. }
  224. public bool IsBountyComplete(EntityUid container, CargoBountyData data, out HashSet<EntityUid> bountyEntities)
  225. {
  226. if (!_protoMan.TryIndex(data.Bounty, out var proto))
  227. {
  228. bountyEntities = new();
  229. return false;
  230. }
  231. return IsBountyComplete(container, proto.Entries, out bountyEntities);
  232. }
  233. public bool IsBountyComplete(EntityUid container, string id)
  234. {
  235. if (!_protoMan.TryIndex<CargoBountyPrototype>(id, out var proto))
  236. return false;
  237. return IsBountyComplete(container, proto.Entries);
  238. }
  239. public bool IsBountyComplete(EntityUid container, CargoBountyPrototype prototype)
  240. {
  241. return IsBountyComplete(container, prototype.Entries);
  242. }
  243. public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries)
  244. {
  245. return IsBountyComplete(container, entries, out _);
  246. }
  247. public bool IsBountyComplete(EntityUid container, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities)
  248. {
  249. return IsBountyComplete(GetBountyEntities(container), entries, out bountyEntities);
  250. }
  251. /// <summary>
  252. /// Determines whether the <paramref name="entity"/> meets the criteria for the bounty <paramref name="entry"/>.
  253. /// </summary>
  254. /// <returns>true if <paramref name="entity"/> is a valid item for the bounty entry, otherwise false</returns>
  255. public bool IsValidBountyEntry(EntityUid entity, CargoBountyItemEntry entry)
  256. {
  257. if (!_whitelistSys.IsValid(entry.Whitelist, entity))
  258. return false;
  259. if (entry.Blacklist != null && _whitelistSys.IsValid(entry.Blacklist, entity))
  260. return false;
  261. return true;
  262. }
  263. public bool IsBountyComplete(HashSet<EntityUid> entities, IEnumerable<CargoBountyItemEntry> entries, out HashSet<EntityUid> bountyEntities)
  264. {
  265. bountyEntities = new();
  266. foreach (var entry in entries)
  267. {
  268. var count = 0;
  269. // store entities that already satisfied an
  270. // entry so we don't double-count them.
  271. var temp = new HashSet<EntityUid>();
  272. foreach (var entity in entities)
  273. {
  274. if (!IsValidBountyEntry(entity, entry))
  275. continue;
  276. count += _stackQuery.CompOrNull(entity)?.Count ?? 1;
  277. temp.Add(entity);
  278. if (count >= entry.Amount)
  279. break;
  280. }
  281. if (count < entry.Amount)
  282. return false;
  283. foreach (var ent in temp)
  284. {
  285. entities.Remove(ent);
  286. bountyEntities.Add(ent);
  287. }
  288. }
  289. return true;
  290. }
  291. private HashSet<EntityUid> GetBountyEntities(EntityUid uid)
  292. {
  293. var entities = new HashSet<EntityUid>
  294. {
  295. uid
  296. };
  297. if (!TryComp<ContainerManagerComponent>(uid, out var containers))
  298. return entities;
  299. foreach (var container in containers.Containers.Values)
  300. {
  301. foreach (var ent in container.ContainedEntities)
  302. {
  303. if (_bountyLabelQuery.HasComponent(ent))
  304. continue;
  305. var children = GetBountyEntities(ent);
  306. foreach (var child in children)
  307. {
  308. entities.Add(child);
  309. }
  310. }
  311. }
  312. return entities;
  313. }
  314. [PublicAPI]
  315. public bool TryAddBounty(EntityUid uid, StationCargoBountyDatabaseComponent? component = null)
  316. {
  317. if (!Resolve(uid, ref component))
  318. return false;
  319. // todo: consider making the cargo bounties weighted.
  320. var allBounties = _protoMan.EnumeratePrototypes<CargoBountyPrototype>().ToList();
  321. var filteredBounties = new List<CargoBountyPrototype>();
  322. foreach (var proto in allBounties)
  323. {
  324. if (component.Bounties.Any(b => b.Bounty == proto.ID))
  325. continue;
  326. filteredBounties.Add(proto);
  327. }
  328. var pool = filteredBounties.Count == 0 ? allBounties : filteredBounties;
  329. var bounty = _random.Pick(pool);
  330. return TryAddBounty(uid, bounty, component);
  331. }
  332. [PublicAPI]
  333. public bool TryAddBounty(EntityUid uid, string bountyId, StationCargoBountyDatabaseComponent? component = null)
  334. {
  335. if (!_protoMan.TryIndex<CargoBountyPrototype>(bountyId, out var bounty))
  336. {
  337. return false;
  338. }
  339. return TryAddBounty(uid, bounty, component);
  340. }
  341. public bool TryAddBounty(EntityUid uid, CargoBountyPrototype bounty, StationCargoBountyDatabaseComponent? component = null)
  342. {
  343. if (!Resolve(uid, ref component))
  344. return false;
  345. if (component.Bounties.Count >= component.MaxBounties)
  346. return false;
  347. _nameIdentifier.GenerateUniqueName(uid, BountyNameIdentifierGroup, out var randomVal);
  348. var newBounty = new CargoBountyData(bounty, randomVal);
  349. // This bounty id already exists! Probably because NameIdentifierSystem ran out of ids.
  350. if (component.Bounties.Any(b => b.Id == newBounty.Id))
  351. {
  352. Log.Error("Failed to add bounty {ID} because another one with the same ID already existed!", newBounty.Id);
  353. return false;
  354. }
  355. component.Bounties.Add(new CargoBountyData(bounty, randomVal));
  356. _adminLogger.Add(LogType.Action, LogImpact.Low, $"Added bounty \"{bounty.ID}\" (id:{component.TotalBounties}) to station {ToPrettyString(uid)}");
  357. component.TotalBounties++;
  358. return true;
  359. }
  360. [PublicAPI]
  361. public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
  362. string dataId,
  363. bool skipped,
  364. EntityUid? actor = null)
  365. {
  366. if (!TryGetBountyFromId(ent.Owner, dataId, out var data, ent.Comp))
  367. return false;
  368. return TryRemoveBounty(ent, data.Value, skipped, actor);
  369. }
  370. public bool TryRemoveBounty(Entity<StationCargoBountyDatabaseComponent?> ent,
  371. CargoBountyData data,
  372. bool skipped,
  373. EntityUid? actor = null)
  374. {
  375. if (!Resolve(ent, ref ent.Comp))
  376. return false;
  377. for (var i = 0; i < ent.Comp.Bounties.Count; i++)
  378. {
  379. if (ent.Comp.Bounties[i].Id == data.Id)
  380. {
  381. string? actorName = null;
  382. if (actor != null)
  383. {
  384. var getIdentityEvent = new TryGetIdentityShortInfoEvent(ent.Owner, actor.Value);
  385. RaiseLocalEvent(getIdentityEvent);
  386. actorName = getIdentityEvent.Title;
  387. }
  388. ent.Comp.History.Add(new CargoBountyHistoryData(data,
  389. skipped
  390. ? CargoBountyHistoryData.BountyResult.Skipped
  391. : CargoBountyHistoryData.BountyResult.Completed,
  392. _gameTiming.CurTime,
  393. actorName));
  394. ent.Comp.Bounties.RemoveAt(i);
  395. return true;
  396. }
  397. }
  398. return false;
  399. }
  400. public bool TryGetBountyFromId(
  401. EntityUid uid,
  402. string id,
  403. [NotNullWhen(true)] out CargoBountyData? bounty,
  404. StationCargoBountyDatabaseComponent? component = null)
  405. {
  406. bounty = null;
  407. if (!Resolve(uid, ref component))
  408. return false;
  409. foreach (var bountyData in component.Bounties)
  410. {
  411. if (bountyData.Id != id)
  412. continue;
  413. bounty = bountyData;
  414. break;
  415. }
  416. return bounty != null;
  417. }
  418. public void UpdateBountyConsoles()
  419. {
  420. var query = EntityQueryEnumerator<CargoBountyConsoleComponent, UserInterfaceComponent>();
  421. while (query.MoveNext(out var uid, out _, out var ui))
  422. {
  423. if (_station.GetOwningStation(uid) is not { } station ||
  424. !TryComp<StationCargoBountyDatabaseComponent>(station, out var db))
  425. {
  426. continue;
  427. }
  428. var untilNextSkip = db.NextSkipTime - _timing.CurTime;
  429. _uiSystem.SetUiState((uid, ui), CargoConsoleUiKey.Bounty, new CargoBountyConsoleState(db.Bounties, db.History, untilNextSkip));
  430. }
  431. }
  432. private void UpdateBounty()
  433. {
  434. var query = EntityQueryEnumerator<StationCargoBountyDatabaseComponent>();
  435. while (query.MoveNext(out var bountyDatabase))
  436. {
  437. bountyDatabase.CheckedBounties.Clear();
  438. }
  439. }
  440. }