SharedStationAiSystem.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542
  1. using Content.Shared.ActionBlocker;
  2. using Content.Shared.Actions;
  3. using Content.Shared.Administration.Managers;
  4. using Content.Shared.Containers.ItemSlots;
  5. using Content.Shared.Database;
  6. using Content.Shared.Doors.Systems;
  7. using Content.Shared.DoAfter;
  8. using Content.Shared.Electrocution;
  9. using Content.Shared.Intellicard;
  10. using Content.Shared.Interaction;
  11. using Content.Shared.Item.ItemToggle;
  12. using Content.Shared.Mind;
  13. using Content.Shared.Movement.Components;
  14. using Content.Shared.Movement.Systems;
  15. using Content.Shared.Popups;
  16. using Content.Shared.Power;
  17. using Content.Shared.Power.EntitySystems;
  18. using Content.Shared.StationAi;
  19. using Content.Shared.Verbs;
  20. using Robust.Shared.Audio;
  21. using Robust.Shared.Audio.Systems;
  22. using Robust.Shared.Containers;
  23. using Robust.Shared.Map;
  24. using Robust.Shared.Map.Components;
  25. using Robust.Shared.Network;
  26. using Robust.Shared.Physics;
  27. using Robust.Shared.Prototypes;
  28. using Robust.Shared.Serialization;
  29. using Robust.Shared.Timing;
  30. using System.Diagnostics.CodeAnalysis;
  31. namespace Content.Shared.Silicons.StationAi;
  32. public abstract partial class SharedStationAiSystem : EntitySystem
  33. {
  34. [Dependency] private readonly ISharedAdminManager _admin = default!;
  35. [Dependency] private readonly IGameTiming _timing = default!;
  36. [Dependency] private readonly INetManager _net = default!;
  37. [Dependency] private readonly ItemSlotsSystem _slots = default!;
  38. [Dependency] private readonly ItemToggleSystem _toggles = default!;
  39. [Dependency] private readonly ActionBlockerSystem _blocker = default!;
  40. [Dependency] private readonly MetaDataSystem _metadata = default!;
  41. [Dependency] private readonly SharedAirlockSystem _airlocks = default!;
  42. [Dependency] private readonly SharedAppearanceSystem _appearance = default!;
  43. [Dependency] private readonly SharedAudioSystem _audio = default!;
  44. [Dependency] private readonly SharedContainerSystem _containers = default!;
  45. [Dependency] private readonly SharedDoorSystem _doors = default!;
  46. [Dependency] private readonly SharedDoAfterSystem _doAfter = default!;
  47. [Dependency] private readonly SharedElectrocutionSystem _electrify = default!;
  48. [Dependency] private readonly SharedEyeSystem _eye = default!;
  49. [Dependency] protected readonly SharedMapSystem Maps = default!;
  50. [Dependency] private readonly SharedMindSystem _mind = default!;
  51. [Dependency] private readonly SharedMoverController _mover = default!;
  52. [Dependency] private readonly SharedPopupSystem _popup = default!;
  53. [Dependency] private readonly SharedPowerReceiverSystem PowerReceiver = default!;
  54. [Dependency] private readonly SharedTransformSystem _xforms = default!;
  55. [Dependency] private readonly SharedUserInterfaceSystem _uiSystem = default!;
  56. [Dependency] private readonly StationAiVisionSystem _vision = default!;
  57. // StationAiHeld is added to anything inside of an AI core.
  58. // StationAiHolder indicates it can hold an AI positronic brain (e.g. holocard / core).
  59. // StationAiCore holds functionality related to the core itself.
  60. // StationAiWhitelist is a general whitelist to stop it being able to interact with anything
  61. // StationAiOverlay handles the static overlay. It also handles interaction blocking on client and server
  62. // for anything under it.
  63. private EntityQuery<BroadphaseComponent> _broadphaseQuery;
  64. private EntityQuery<MapGridComponent> _gridQuery;
  65. private const float MaxVisionMultiplier = 5f;
  66. public override void Initialize()
  67. {
  68. base.Initialize();
  69. _broadphaseQuery = GetEntityQuery<BroadphaseComponent>();
  70. _gridQuery = GetEntityQuery<MapGridComponent>();
  71. InitializeAirlock();
  72. InitializeHeld();
  73. InitializeLight();
  74. SubscribeLocalEvent<StationAiWhitelistComponent, BoundUserInterfaceCheckRangeEvent>(OnAiBuiCheck);
  75. SubscribeLocalEvent<StationAiOverlayComponent, AccessibleOverrideEvent>(OnAiAccessible);
  76. SubscribeLocalEvent<StationAiOverlayComponent, InRangeOverrideEvent>(OnAiInRange);
  77. SubscribeLocalEvent<StationAiOverlayComponent, MenuVisibilityEvent>(OnAiMenu);
  78. SubscribeLocalEvent<StationAiHolderComponent, ComponentInit>(OnHolderInit);
  79. SubscribeLocalEvent<StationAiHolderComponent, ComponentRemove>(OnHolderRemove);
  80. SubscribeLocalEvent<StationAiHolderComponent, AfterInteractEvent>(OnHolderInteract);
  81. SubscribeLocalEvent<StationAiHolderComponent, MapInitEvent>(OnHolderMapInit);
  82. SubscribeLocalEvent<StationAiHolderComponent, EntInsertedIntoContainerMessage>(OnHolderConInsert);
  83. SubscribeLocalEvent<StationAiHolderComponent, EntRemovedFromContainerMessage>(OnHolderConRemove);
  84. SubscribeLocalEvent<StationAiHolderComponent, IntellicardDoAfterEvent>(OnIntellicardDoAfter);
  85. SubscribeLocalEvent<StationAiCoreComponent, EntInsertedIntoContainerMessage>(OnAiInsert);
  86. SubscribeLocalEvent<StationAiCoreComponent, EntRemovedFromContainerMessage>(OnAiRemove);
  87. SubscribeLocalEvent<StationAiCoreComponent, MapInitEvent>(OnAiMapInit);
  88. SubscribeLocalEvent<StationAiCoreComponent, ComponentShutdown>(OnAiShutdown);
  89. SubscribeLocalEvent<StationAiCoreComponent, PowerChangedEvent>(OnCorePower);
  90. SubscribeLocalEvent<StationAiCoreComponent, GetVerbsEvent<Verb>>(OnCoreVerbs);
  91. }
  92. private void OnCoreVerbs(Entity<StationAiCoreComponent> ent, ref GetVerbsEvent<Verb> args)
  93. {
  94. if (!_admin.IsAdmin(args.User) ||
  95. TryGetHeld((ent.Owner, ent.Comp), out _))
  96. {
  97. return;
  98. }
  99. var user = args.User;
  100. }
  101. private void OnAiAccessible(Entity<StationAiOverlayComponent> ent, ref AccessibleOverrideEvent args)
  102. {
  103. args.Handled = true;
  104. // Hopefully AI never needs storage
  105. if (_containers.TryGetContainingContainer(args.Target, out var targetContainer))
  106. {
  107. return;
  108. }
  109. if (!_containers.IsInSameOrTransparentContainer(args.User, args.Target, otherContainer: targetContainer))
  110. {
  111. return;
  112. }
  113. args.Accessible = true;
  114. }
  115. private void OnAiMenu(Entity<StationAiOverlayComponent> ent, ref MenuVisibilityEvent args)
  116. {
  117. args.Visibility &= ~MenuVisibility.NoFov;
  118. }
  119. private void OnAiBuiCheck(Entity<StationAiWhitelistComponent> ent, ref BoundUserInterfaceCheckRangeEvent args)
  120. {
  121. if (!HasComp<StationAiHeldComponent>(args.Actor))
  122. return;
  123. args.Result = BoundUserInterfaceRangeResult.Fail;
  124. // Similar to the inrange check but more optimised so server doesn't die.
  125. var targetXform = Transform(args.Target);
  126. // No cross-grid
  127. if (targetXform.GridUid != args.Actor.Comp.GridUid)
  128. {
  129. return;
  130. }
  131. if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid))
  132. {
  133. return;
  134. }
  135. var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates);
  136. lock (_vision)
  137. {
  138. if (_vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile, fastPath: true))
  139. {
  140. args.Result = BoundUserInterfaceRangeResult.Pass;
  141. }
  142. }
  143. }
  144. private void OnAiInRange(Entity<StationAiOverlayComponent> ent, ref InRangeOverrideEvent args)
  145. {
  146. args.Handled = true;
  147. var targetXform = Transform(args.Target);
  148. // No cross-grid
  149. if (targetXform.GridUid != Transform(args.User).GridUid)
  150. {
  151. return;
  152. }
  153. // Validate it's in camera range yes this is expensive.
  154. // Yes it needs optimising
  155. if (!_broadphaseQuery.TryComp(targetXform.GridUid, out var broadphase) || !_gridQuery.TryComp(targetXform.GridUid, out var grid))
  156. {
  157. return;
  158. }
  159. var targetTile = Maps.LocalToTile(targetXform.GridUid.Value, grid, targetXform.Coordinates);
  160. args.InRange = _vision.IsAccessible((targetXform.GridUid.Value, broadphase, grid), targetTile);
  161. }
  162. private void OnIntellicardDoAfter(Entity<StationAiHolderComponent> ent, ref IntellicardDoAfterEvent args)
  163. {
  164. if (args.Cancelled)
  165. return;
  166. if (args.Handled)
  167. return;
  168. if (!TryComp(args.Args.Target, out StationAiHolderComponent? targetHolder))
  169. return;
  170. // Try to insert our thing into them
  171. if (_slots.CanEject(ent.Owner, args.User, ent.Comp.Slot))
  172. {
  173. if (!_slots.TryInsert(args.Args.Target.Value, targetHolder.Slot, ent.Comp.Slot.Item!.Value, args.User, excludeUserAudio: true))
  174. {
  175. return;
  176. }
  177. args.Handled = true;
  178. return;
  179. }
  180. // Otherwise try to take from them
  181. if (_slots.CanEject(args.Args.Target.Value, args.User, targetHolder.Slot))
  182. {
  183. if (!_slots.TryInsert(ent.Owner, ent.Comp.Slot, targetHolder.Slot.Item!.Value, args.User, excludeUserAudio: true))
  184. {
  185. return;
  186. }
  187. args.Handled = true;
  188. }
  189. }
  190. private void OnHolderInteract(Entity<StationAiHolderComponent> ent, ref AfterInteractEvent args)
  191. {
  192. if (args.Handled || !args.CanReach || args.Target == null)
  193. return;
  194. if (!TryComp(args.Target, out StationAiHolderComponent? targetHolder))
  195. return;
  196. //Don't want to download/upload between several intellicards. You can just pick it up at that point.
  197. if (HasComp<IntellicardComponent>(args.Target))
  198. return;
  199. if (!TryComp(args.Used, out IntellicardComponent? intelliComp))
  200. return;
  201. var cardHasAi = _slots.CanEject(ent.Owner, args.User, ent.Comp.Slot);
  202. var coreHasAi = _slots.CanEject(args.Target.Value, args.User, targetHolder.Slot);
  203. if (cardHasAi && coreHasAi)
  204. {
  205. _popup.PopupClient(Loc.GetString("intellicard-core-occupied"), args.User, args.User, PopupType.Medium);
  206. args.Handled = true;
  207. return;
  208. }
  209. if (!cardHasAi && !coreHasAi)
  210. {
  211. _popup.PopupClient(Loc.GetString("intellicard-core-empty"), args.User, args.User, PopupType.Medium);
  212. args.Handled = true;
  213. return;
  214. }
  215. if (TryGetHeld((args.Target.Value, targetHolder), out var held) && _timing.CurTime > intelliComp.NextWarningAllowed)
  216. {
  217. intelliComp.NextWarningAllowed = _timing.CurTime + intelliComp.WarningDelay;
  218. AnnounceIntellicardUsage(held, intelliComp.WarningSound);
  219. }
  220. var doAfterArgs = new DoAfterArgs(EntityManager, args.User, cardHasAi ? intelliComp.UploadTime : intelliComp.DownloadTime, new IntellicardDoAfterEvent(), args.Target, ent.Owner)
  221. {
  222. BreakOnDamage = true,
  223. BreakOnMove = true,
  224. NeedHand = true,
  225. BreakOnDropItem = true
  226. };
  227. _doAfter.TryStartDoAfter(doAfterArgs);
  228. args.Handled = true;
  229. }
  230. private void OnHolderInit(Entity<StationAiHolderComponent> ent, ref ComponentInit args)
  231. {
  232. _slots.AddItemSlot(ent.Owner, StationAiHolderComponent.Container, ent.Comp.Slot);
  233. }
  234. private void OnHolderRemove(Entity<StationAiHolderComponent> ent, ref ComponentRemove args)
  235. {
  236. _slots.RemoveItemSlot(ent.Owner, ent.Comp.Slot);
  237. }
  238. private void OnHolderConInsert(Entity<StationAiHolderComponent> ent, ref EntInsertedIntoContainerMessage args)
  239. {
  240. UpdateAppearance((ent.Owner, ent.Comp));
  241. }
  242. private void OnHolderConRemove(Entity<StationAiHolderComponent> ent, ref EntRemovedFromContainerMessage args)
  243. {
  244. UpdateAppearance((ent.Owner, ent.Comp));
  245. }
  246. private void OnHolderMapInit(Entity<StationAiHolderComponent> ent, ref MapInitEvent args)
  247. {
  248. UpdateAppearance(ent.Owner);
  249. }
  250. private void OnAiShutdown(Entity<StationAiCoreComponent> ent, ref ComponentShutdown args)
  251. {
  252. // TODO: Tryqueuedel
  253. if (_net.IsClient)
  254. return;
  255. QueueDel(ent.Comp.RemoteEntity);
  256. ent.Comp.RemoteEntity = null;
  257. }
  258. private void OnCorePower(Entity<StationAiCoreComponent> ent, ref PowerChangedEvent args)
  259. {
  260. // TODO: I think in 13 they just straightup die so maybe implement that
  261. if (args.Powered)
  262. {
  263. if (!SetupEye(ent))
  264. return;
  265. AttachEye(ent);
  266. }
  267. else
  268. {
  269. ClearEye(ent);
  270. }
  271. }
  272. private void OnAiMapInit(Entity<StationAiCoreComponent> ent, ref MapInitEvent args)
  273. {
  274. SetupEye(ent);
  275. AttachEye(ent);
  276. }
  277. public void SwitchRemoteEntityMode(Entity<StationAiCoreComponent?> entity, bool isRemote)
  278. {
  279. if (entity.Comp?.Remote == null || entity.Comp.Remote == isRemote)
  280. return;
  281. var ent = new Entity<StationAiCoreComponent>(entity.Owner, entity.Comp);
  282. ent.Comp.Remote = isRemote;
  283. EntityCoordinates? coords = ent.Comp.RemoteEntity != null ? Transform(ent.Comp.RemoteEntity.Value).Coordinates : null;
  284. // Attach new eye
  285. ClearEye(ent);
  286. if (SetupEye(ent, coords))
  287. AttachEye(ent);
  288. // Adjust user FoV
  289. var user = GetInsertedAI(ent);
  290. if (TryComp<EyeComponent>(user, out var eye))
  291. _eye.SetDrawFov(user.Value, !isRemote);
  292. }
  293. private bool SetupEye(Entity<StationAiCoreComponent> ent, EntityCoordinates? coords = null)
  294. {
  295. if (_net.IsClient)
  296. return false;
  297. if (ent.Comp.RemoteEntity != null)
  298. return false;
  299. var proto = ent.Comp.RemoteEntityProto;
  300. if (coords == null)
  301. coords = Transform(ent.Owner).Coordinates;
  302. if (!ent.Comp.Remote)
  303. proto = ent.Comp.PhysicalEntityProto;
  304. if (proto != null)
  305. {
  306. ent.Comp.RemoteEntity = SpawnAtPosition(proto, coords.Value);
  307. Dirty(ent);
  308. }
  309. return true;
  310. }
  311. private void ClearEye(Entity<StationAiCoreComponent> ent)
  312. {
  313. if (_net.IsClient)
  314. return;
  315. QueueDel(ent.Comp.RemoteEntity);
  316. ent.Comp.RemoteEntity = null;
  317. Dirty(ent);
  318. }
  319. private void AttachEye(Entity<StationAiCoreComponent> ent)
  320. {
  321. if (ent.Comp.RemoteEntity == null)
  322. return;
  323. if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) ||
  324. container.ContainedEntities.Count != 1)
  325. {
  326. return;
  327. }
  328. // Attach them to the portable eye that can move around.
  329. var user = container.ContainedEntities[0];
  330. if (TryComp(user, out EyeComponent? eyeComp))
  331. {
  332. _eye.SetDrawFov(user, false, eyeComp);
  333. _eye.SetTarget(user, ent.Comp.RemoteEntity.Value, eyeComp);
  334. }
  335. _mover.SetRelay(user, ent.Comp.RemoteEntity.Value);
  336. }
  337. private EntityUid? GetInsertedAI(Entity<StationAiCoreComponent> ent)
  338. {
  339. if (!_containers.TryGetContainer(ent.Owner, StationAiHolderComponent.Container, out var container) ||
  340. container.ContainedEntities.Count != 1)
  341. {
  342. return null;
  343. }
  344. return container.ContainedEntities[0];
  345. }
  346. private void OnAiInsert(Entity<StationAiCoreComponent> ent, ref EntInsertedIntoContainerMessage args)
  347. {
  348. if (args.Container.ID != StationAiCoreComponent.Container)
  349. return;
  350. if (_timing.ApplyingState)
  351. return;
  352. ent.Comp.Remote = true;
  353. SetupEye(ent);
  354. // Just so text and the likes works properly
  355. _metadata.SetEntityName(ent.Owner, MetaData(args.Entity).EntityName);
  356. AttachEye(ent);
  357. }
  358. private void OnAiRemove(Entity<StationAiCoreComponent> ent, ref EntRemovedFromContainerMessage args)
  359. {
  360. if (_timing.ApplyingState)
  361. return;
  362. ent.Comp.Remote = true;
  363. // Reset name to whatever
  364. _metadata.SetEntityName(ent.Owner, Prototype(ent.Owner)?.Name ?? string.Empty);
  365. // Remove eye relay
  366. RemCompDeferred<RelayInputMoverComponent>(args.Entity);
  367. if (TryComp(args.Entity, out EyeComponent? eyeComp))
  368. {
  369. _eye.SetDrawFov(args.Entity, true, eyeComp);
  370. _eye.SetTarget(args.Entity, null, eyeComp);
  371. }
  372. ClearEye(ent);
  373. }
  374. private void UpdateAppearance(Entity<StationAiHolderComponent?> entity)
  375. {
  376. if (!Resolve(entity.Owner, ref entity.Comp, false))
  377. return;
  378. if (!_containers.TryGetContainer(entity.Owner, StationAiHolderComponent.Container, out var container) ||
  379. container.Count == 0)
  380. {
  381. _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Empty);
  382. return;
  383. }
  384. _appearance.SetData(entity.Owner, StationAiVisualState.Key, StationAiState.Occupied);
  385. }
  386. public virtual void AnnounceIntellicardUsage(EntityUid uid, SoundSpecifier? cue = null) { }
  387. public virtual bool SetVisionEnabled(Entity<StationAiVisionComponent> entity, bool enabled, bool announce = false)
  388. {
  389. if (entity.Comp.Enabled == enabled)
  390. return false;
  391. entity.Comp.Enabled = enabled;
  392. Dirty(entity);
  393. return true;
  394. }
  395. public virtual bool SetWhitelistEnabled(Entity<StationAiWhitelistComponent> entity, bool value, bool announce = false)
  396. {
  397. if (entity.Comp.Enabled == value)
  398. return false;
  399. entity.Comp.Enabled = value;
  400. Dirty(entity);
  401. return true;
  402. }
  403. /// <summary>
  404. /// BUI validation for ai interactions.
  405. /// </summary>
  406. private bool ValidateAi(Entity<StationAiHeldComponent?> entity)
  407. {
  408. if (!Resolve(entity.Owner, ref entity.Comp, false))
  409. {
  410. return false;
  411. }
  412. return _blocker.CanComplexInteract(entity.Owner);
  413. }
  414. }
  415. public sealed partial class JumpToCoreEvent : InstantActionEvent
  416. {
  417. }
  418. [Serializable, NetSerializable]
  419. public sealed partial class IntellicardDoAfterEvent : SimpleDoAfterEvent;
  420. [Serializable, NetSerializable]
  421. public enum StationAiVisualState : byte
  422. {
  423. Key,
  424. }
  425. [Serializable, NetSerializable]
  426. public enum StationAiState : byte
  427. {
  428. Empty,
  429. Occupied,
  430. Dead,
  431. }