NPCUtilitySystem.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. using Content.Server.Atmos.Components;
  2. using Content.Server.Fluids.EntitySystems;
  3. using Content.Server.NPC.Queries;
  4. using Content.Server.NPC.Queries.Considerations;
  5. using Content.Server.NPC.Queries.Curves;
  6. using Content.Server.NPC.Queries.Queries;
  7. using Content.Server.Nutrition.Components;
  8. using Content.Server.Nutrition.EntitySystems;
  9. using Content.Server.Storage.Components;
  10. using Content.Shared.Chemistry.EntitySystems;
  11. using Content.Shared.Damage;
  12. using Content.Shared.Examine;
  13. using Content.Shared.Fluids.Components;
  14. using Content.Shared.Hands.Components;
  15. using Content.Shared.Inventory;
  16. using Content.Shared.Mobs;
  17. using Content.Shared.Mobs.Components;
  18. using Content.Shared.Mobs.Systems;
  19. using Content.Shared.NPC.Systems;
  20. using Content.Shared.Nutrition.Components;
  21. using Content.Shared.Nutrition.EntitySystems;
  22. using Content.Shared.Tools.Systems;
  23. using Content.Shared.Turrets;
  24. using Content.Shared.Weapons.Melee;
  25. using Content.Shared.Weapons.Ranged.Components;
  26. using Content.Shared.Weapons.Ranged.Events;
  27. using Content.Shared.Whitelist;
  28. using Microsoft.Extensions.ObjectPool;
  29. using Robust.Server.Containers;
  30. using Robust.Shared.Prototypes;
  31. using Robust.Shared.Utility;
  32. using System.Linq;
  33. namespace Content.Server.NPC.Systems;
  34. /// <summary>
  35. /// Handles utility queries for NPCs.
  36. /// </summary>
  37. public sealed class NPCUtilitySystem : EntitySystem
  38. {
  39. [Dependency] private readonly IPrototypeManager _proto = default!;
  40. [Dependency] private readonly ContainerSystem _container = default!;
  41. [Dependency] private readonly DrinkSystem _drink = default!;
  42. [Dependency] private readonly EntityLookupSystem _lookup = default!;
  43. [Dependency] private readonly FoodSystem _food = default!;
  44. [Dependency] private readonly InventorySystem _inventory = default!;
  45. [Dependency] private readonly MobStateSystem _mobState = default!;
  46. [Dependency] private readonly NpcFactionSystem _npcFaction = default!;
  47. [Dependency] private readonly OpenableSystem _openable = default!;
  48. [Dependency] private readonly PuddleSystem _puddle = default!;
  49. [Dependency] private readonly SharedTransformSystem _transform = default!;
  50. [Dependency] private readonly SharedSolutionContainerSystem _solutions = default!;
  51. [Dependency] private readonly WeldableSystem _weldable = default!;
  52. [Dependency] private readonly ExamineSystemShared _examine = default!;
  53. [Dependency] private readonly EntityWhitelistSystem _whitelistSystem = default!;
  54. [Dependency] private readonly MobThresholdSystem _thresholdSystem = default!;
  55. [Dependency] private readonly TurretTargetSettingsSystem _turretTargetSettings = default!;
  56. private EntityQuery<PuddleComponent> _puddleQuery;
  57. private EntityQuery<TransformComponent> _xformQuery;
  58. private ObjectPool<HashSet<EntityUid>> _entPool =
  59. new DefaultObjectPool<HashSet<EntityUid>>(new SetPolicy<EntityUid>(), 256);
  60. // Temporary caches.
  61. private List<EntityUid> _entityList = new();
  62. private HashSet<Entity<IComponent>> _entitySet = new();
  63. private List<EntityPrototype.ComponentRegistryEntry> _compTypes = new();
  64. public override void Initialize()
  65. {
  66. base.Initialize();
  67. _puddleQuery = GetEntityQuery<PuddleComponent>();
  68. _xformQuery = GetEntityQuery<TransformComponent>();
  69. }
  70. /// <summary>
  71. /// Runs the UtilityQueryPrototype and returns the best-matching entities.
  72. /// </summary>
  73. /// <param name="bestOnly">Should we only return the entity with the best score.</param>
  74. public UtilityResult GetEntities(
  75. NPCBlackboard blackboard,
  76. string proto,
  77. bool bestOnly = true)
  78. {
  79. // TODO: PickHostilesop or whatever needs to juse be UtilityQueryOperator
  80. var weh = _proto.Index<UtilityQueryPrototype>(proto);
  81. var ents = _entPool.Get();
  82. foreach (var query in weh.Query)
  83. {
  84. switch (query)
  85. {
  86. case UtilityQueryFilter filter:
  87. Filter(blackboard, ents, filter);
  88. break;
  89. default:
  90. Add(blackboard, ents, query);
  91. break;
  92. }
  93. }
  94. if (ents.Count == 0)
  95. {
  96. _entPool.Return(ents);
  97. return UtilityResult.Empty;
  98. }
  99. var results = new Dictionary<EntityUid, float>();
  100. var highestScore = 0f;
  101. foreach (var ent in ents)
  102. {
  103. if (results.Count > weh.Limit)
  104. break;
  105. var score = 1f;
  106. foreach (var con in weh.Considerations)
  107. {
  108. var conScore = GetScore(blackboard, ent, con);
  109. var curve = con.Curve;
  110. var curveScore = GetScore(curve, conScore);
  111. var adjusted = GetAdjustedScore(curveScore, weh.Considerations.Count);
  112. score *= adjusted;
  113. // If the score is too low OR we only care about best entity then early out.
  114. // Due to the adjusted score only being able to decrease it can never exceed the highest from here.
  115. if (score <= 0f || bestOnly && score <= highestScore)
  116. {
  117. break;
  118. }
  119. }
  120. if (score <= 0f)
  121. continue;
  122. highestScore = MathF.Max(score, highestScore);
  123. results.Add(ent, score);
  124. }
  125. var result = new UtilityResult(results);
  126. blackboard.Remove<EntityUid>(NPCBlackboard.UtilityTarget);
  127. _entPool.Return(ents);
  128. return result;
  129. }
  130. private float GetScore(IUtilityCurve curve, float conScore)
  131. {
  132. switch (curve)
  133. {
  134. case BoolCurve:
  135. return conScore > 0f ? 1f : 0f;
  136. case InverseBoolCurve:
  137. return conScore.Equals(0f) ? 1f : 0f;
  138. case PresetCurve presetCurve:
  139. return GetScore(_proto.Index<UtilityCurvePresetPrototype>(presetCurve.Preset).Curve, conScore);
  140. case QuadraticCurve quadraticCurve:
  141. return Math.Clamp(quadraticCurve.Slope * MathF.Pow(conScore - quadraticCurve.XOffset, quadraticCurve.Exponent) + quadraticCurve.YOffset, 0f, 1f);
  142. default:
  143. throw new NotImplementedException();
  144. }
  145. }
  146. private float GetScore(NPCBlackboard blackboard, EntityUid targetUid, UtilityConsideration consideration)
  147. {
  148. var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
  149. switch (consideration)
  150. {
  151. case FoodValueCon:
  152. {
  153. if (!TryComp<FoodComponent>(targetUid, out var food))
  154. return 0f;
  155. // mice can't eat unpeeled bananas, need monkey's help
  156. if (_openable.IsClosed(targetUid))
  157. return 0f;
  158. if (!_food.IsDigestibleBy(owner, targetUid, food))
  159. return 0f;
  160. var avoidBadFood = !HasComp<IgnoreBadFoodComponent>(owner);
  161. // only eat when hungry or if it will eat anything
  162. if (TryComp<HungerComponent>(owner, out var hunger) && hunger.CurrentThreshold > HungerThreshold.Okay && avoidBadFood)
  163. return 0f;
  164. // no mouse don't eat the uranium-235
  165. if (avoidBadFood && HasComp<BadFoodComponent>(targetUid))
  166. return 0f;
  167. return 1f;
  168. }
  169. case DrinkValueCon:
  170. {
  171. if (!TryComp<DrinkComponent>(targetUid, out var drink))
  172. return 0f;
  173. // can't drink closed drinks
  174. if (_openable.IsClosed(targetUid))
  175. return 0f;
  176. // only drink when thirsty
  177. if (TryComp<ThirstComponent>(owner, out var thirst) && thirst.CurrentThirstThreshold > ThirstThreshold.Okay)
  178. return 0f;
  179. // no janicow don't drink the blood puddle
  180. if (HasComp<BadDrinkComponent>(targetUid))
  181. return 0f;
  182. // needs to have something that will satiate thirst, mice wont try to drink 100% pure mutagen.
  183. var hydration = _drink.TotalHydration(targetUid, drink);
  184. if (hydration <= 1.0f)
  185. return 0f;
  186. return 1f;
  187. }
  188. case OrderedTargetCon:
  189. {
  190. if (!blackboard.TryGetValue<EntityUid>(NPCBlackboard.CurrentOrderedTarget, out var orderedTarget, EntityManager))
  191. return 0f;
  192. if (targetUid != orderedTarget)
  193. return 0f;
  194. return 1f;
  195. }
  196. case TargetAccessibleCon:
  197. {
  198. if (_container.TryGetContainingContainer(targetUid, out var container))
  199. {
  200. if (TryComp<EntityStorageComponent>(container.Owner, out var storageComponent))
  201. {
  202. if (storageComponent is { Open: false } && _weldable.IsWelded(container.Owner))
  203. {
  204. return 0.0f;
  205. }
  206. }
  207. else
  208. {
  209. // If we're in a container (e.g. held or whatever) then we probably can't get it. Only exception
  210. // Is a locker / crate
  211. // TODO: Some mobs can break it so consider that.
  212. return 0.0f;
  213. }
  214. }
  215. // TODO: Pathfind there, though probably do it in a separate con.
  216. return 1f;
  217. }
  218. case TargetAmmoMatchesCon:
  219. {
  220. if (!blackboard.TryGetValue(NPCBlackboard.ActiveHand, out Hand? activeHand, EntityManager) ||
  221. !TryComp<BallisticAmmoProviderComponent>(activeHand.HeldEntity, out var heldGun))
  222. {
  223. return 0f;
  224. }
  225. if (_whitelistSystem.IsWhitelistFailOrNull(heldGun.Whitelist, targetUid))
  226. {
  227. return 0f;
  228. }
  229. return 1f;
  230. }
  231. case TargetDistanceCon:
  232. {
  233. var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
  234. if (!TryComp(targetUid, out TransformComponent? targetXform) ||
  235. !TryComp(owner, out TransformComponent? xform))
  236. {
  237. return 0f;
  238. }
  239. if (!targetXform.Coordinates.TryDistance(EntityManager, _transform, xform.Coordinates,
  240. out var distance))
  241. {
  242. return 0f;
  243. }
  244. return Math.Clamp(distance / radius, 0f, 1f);
  245. }
  246. case TargetAmmoCon:
  247. {
  248. if (!HasComp<GunComponent>(targetUid))
  249. return 0f;
  250. var ev = new GetAmmoCountEvent();
  251. RaiseLocalEvent(targetUid, ref ev);
  252. if (ev.Count == 0)
  253. return 0f;
  254. // Wat
  255. if (ev.Capacity == 0)
  256. return 1f;
  257. return (float) ev.Count / ev.Capacity;
  258. }
  259. case TargetHealthCon con:
  260. {
  261. if (!TryComp(targetUid, out DamageableComponent? damage))
  262. return 0f;
  263. if (con.TargetState != MobState.Invalid && _thresholdSystem.TryGetPercentageForState(targetUid, con.TargetState, damage.TotalDamage, out var percentage))
  264. return Math.Clamp((float)(1 - percentage), 0f, 1f);
  265. if (_thresholdSystem.TryGetIncapPercentage(targetUid, damage.TotalDamage, out var incapPercentage))
  266. return Math.Clamp((float)(1 - incapPercentage), 0f, 1f);
  267. return 0f;
  268. }
  269. case TargetInLOSCon:
  270. {
  271. var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
  272. return _examine.InRangeUnOccluded(owner, targetUid, radius + 0.5f, null) ? 1f : 0f;
  273. }
  274. case TargetInLOSOrCurrentCon:
  275. {
  276. var radius = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
  277. const float bufferRange = 0.5f;
  278. if (blackboard.TryGetValue<EntityUid>("Target", out var currentTarget, EntityManager) &&
  279. currentTarget == targetUid &&
  280. TryComp(owner, out TransformComponent? xform) &&
  281. TryComp(targetUid, out TransformComponent? targetXform) &&
  282. xform.Coordinates.TryDistance(EntityManager, _transform, targetXform.Coordinates, out var distance) &&
  283. distance <= radius + bufferRange)
  284. {
  285. return 1f;
  286. }
  287. return _examine.InRangeUnOccluded(owner, targetUid, radius + bufferRange, null) ? 1f : 0f;
  288. }
  289. case TargetIsAliveCon:
  290. {
  291. return _mobState.IsAlive(targetUid) ? 1f : 0f;
  292. }
  293. case TargetIsCritCon:
  294. {
  295. return _mobState.IsCritical(targetUid) ? 1f : 0f;
  296. }
  297. case TargetIsDeadCon:
  298. {
  299. return _mobState.IsDead(targetUid) ? 1f : 0f;
  300. }
  301. case TargetMeleeCon:
  302. {
  303. if (TryComp<MeleeWeaponComponent>(targetUid, out var melee))
  304. {
  305. return melee.Damage.GetTotal().Float() * melee.AttackRate / 100f;
  306. }
  307. return 0f;
  308. }
  309. case TargetOnFireCon:
  310. {
  311. if (TryComp(targetUid, out FlammableComponent? fire) && fire.OnFire)
  312. return 1f;
  313. return 0f;
  314. }
  315. case TurretTargetingCon:
  316. {
  317. if (!TryComp<TurretTargetSettingsComponent>(owner, out var turretTargetSettings) ||
  318. _turretTargetSettings.EntityIsTargetForTurret((owner, turretTargetSettings), targetUid))
  319. return 1f;
  320. return 0f;
  321. }
  322. default:
  323. throw new NotImplementedException();
  324. }
  325. }
  326. private float GetAdjustedScore(float score, int considerations)
  327. {
  328. /*
  329. * Now using the geometric mean
  330. * for n scores you take the n-th root of the scores multiplied
  331. * e.g. a, b, c scores you take Math.Pow(a * b * c, 1/3)
  332. * To get the ACTUAL geometric mean at any one stage you'd need to divide by the running consideration count
  333. * however, the downside to this is it will fluctuate up and down over time.
  334. * For our purposes if we go below the minimum threshold we want to cut it off, thus we take a
  335. * "running geometric mean" which can only ever go down (and by the final value will equal the actual geometric mean).
  336. */
  337. var adjusted = MathF.Pow(score, 1 / (float) considerations);
  338. return Math.Clamp(adjusted, 0f, 1f);
  339. }
  340. private void Add(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQuery query)
  341. {
  342. var owner = blackboard.GetValue<EntityUid>(NPCBlackboard.Owner);
  343. var vision = blackboard.GetValueOrDefault<float>(blackboard.GetVisionRadiusKey(EntityManager), EntityManager);
  344. switch (query)
  345. {
  346. case ComponentQuery compQuery:
  347. {
  348. if (compQuery.Components.Count == 0)
  349. return;
  350. var mapPos = _transform.GetMapCoordinates(owner, xform: _xformQuery.GetComponent(owner));
  351. _compTypes.Clear();
  352. var i = -1;
  353. EntityPrototype.ComponentRegistryEntry compZero = default!;
  354. foreach (var compType in compQuery.Components.Values)
  355. {
  356. i++;
  357. if (i == 0)
  358. {
  359. compZero = compType;
  360. continue;
  361. }
  362. _compTypes.Add(compType);
  363. }
  364. _entitySet.Clear();
  365. _lookup.GetEntitiesInRange(compZero.Component.GetType(), mapPos, vision, _entitySet);
  366. foreach (var comp in _entitySet)
  367. {
  368. var ent = comp.Owner;
  369. if (ent == owner)
  370. continue;
  371. var othersFound = true;
  372. foreach (var compOther in _compTypes)
  373. {
  374. if (!HasComp(ent, compOther.Component.GetType()))
  375. {
  376. othersFound = false;
  377. break;
  378. }
  379. }
  380. if (!othersFound)
  381. continue;
  382. entities.Add(ent);
  383. }
  384. break;
  385. }
  386. case InventoryQuery:
  387. {
  388. if (!_inventory.TryGetContainerSlotEnumerator(owner, out var enumerator))
  389. break;
  390. while (enumerator.MoveNext(out var slot))
  391. {
  392. foreach (var child in slot.ContainedEntities)
  393. {
  394. RecursiveAdd(child, entities);
  395. }
  396. }
  397. break;
  398. }
  399. case NearbyHostilesQuery:
  400. {
  401. foreach (var ent in _npcFaction.GetNearbyHostiles(owner, vision))
  402. {
  403. entities.Add(ent);
  404. }
  405. break;
  406. }
  407. default:
  408. throw new NotImplementedException();
  409. }
  410. }
  411. private void RecursiveAdd(EntityUid uid, HashSet<EntityUid> entities)
  412. {
  413. // TODO: Probably need a recursive struct enumerator on engine.
  414. var xform = _xformQuery.GetComponent(uid);
  415. var enumerator = xform.ChildEnumerator;
  416. entities.Add(uid);
  417. while (enumerator.MoveNext(out var child))
  418. {
  419. RecursiveAdd(child, entities);
  420. }
  421. }
  422. private void Filter(NPCBlackboard blackboard, HashSet<EntityUid> entities, UtilityQueryFilter filter)
  423. {
  424. switch (filter)
  425. {
  426. case ComponentFilter compFilter:
  427. {
  428. _entityList.Clear();
  429. foreach (var ent in entities)
  430. {
  431. foreach (var comp in compFilter.Components)
  432. {
  433. if (HasComp(ent, comp.Value.Component.GetType()))
  434. continue;
  435. _entityList.Add(ent);
  436. break;
  437. }
  438. }
  439. foreach (var ent in _entityList)
  440. {
  441. entities.Remove(ent);
  442. }
  443. break;
  444. }
  445. case RemoveAnchoredFilter:
  446. {
  447. _entityList.Clear();
  448. foreach (var ent in entities)
  449. {
  450. if (!TryComp(ent, out TransformComponent? xform))
  451. continue;
  452. if (xform.Anchored)
  453. _entityList.Add(ent);
  454. }
  455. foreach (var ent in _entityList)
  456. {
  457. entities.Remove(ent);
  458. }
  459. break;
  460. }
  461. case PuddleFilter:
  462. {
  463. _entityList.Clear();
  464. foreach (var ent in entities)
  465. {
  466. if (!_puddleQuery.TryGetComponent(ent, out var puddleComp) ||
  467. !_solutions.TryGetSolution(ent, puddleComp.SolutionName, out _, out var sol) ||
  468. _puddle.CanFullyEvaporate(sol))
  469. {
  470. _entityList.Add(ent);
  471. }
  472. }
  473. foreach (var ent in _entityList)
  474. {
  475. entities.Remove(ent);
  476. }
  477. break;
  478. }
  479. default:
  480. throw new NotImplementedException();
  481. }
  482. }
  483. }
  484. public readonly record struct UtilityResult(Dictionary<EntityUid, float> Entities)
  485. {
  486. public static readonly UtilityResult Empty = new(new Dictionary<EntityUid, float>());
  487. public readonly Dictionary<EntityUid, float> Entities = Entities;
  488. /// <summary>
  489. /// Returns the entity with the highest score.
  490. /// </summary>
  491. public EntityUid GetHighest()
  492. {
  493. if (Entities.Count == 0)
  494. return EntityUid.Invalid;
  495. return Entities.MaxBy(x => x.Value).Key;
  496. }
  497. /// <summary>
  498. /// Returns the entity with the lowest score. This does not consider entities with a 0 (invalid) score.
  499. /// </summary>
  500. public EntityUid GetLowest()
  501. {
  502. if (Entities.Count == 0)
  503. return EntityUid.Invalid;
  504. return Entities.MinBy(x => x.Value).Key;
  505. }
  506. }