1
0

AccessReaderSystem.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. using System.Diagnostics.CodeAnalysis;
  2. using System.Linq;
  3. using Content.Shared.Access.Components;
  4. using Content.Shared.DeviceLinking.Events;
  5. using Content.Shared.Emag.Systems;
  6. using Content.Shared.Hands.EntitySystems;
  7. using Content.Shared.Inventory;
  8. using Content.Shared.NameIdentifier;
  9. using Content.Shared.PDA;
  10. using Content.Shared.StationRecords;
  11. using Robust.Shared.Containers;
  12. using Robust.Shared.GameStates;
  13. using Content.Shared.GameTicking;
  14. using Content.Shared.IdentityManagement;
  15. using Robust.Shared.Collections;
  16. using Robust.Shared.Prototypes;
  17. using Robust.Shared.Timing;
  18. namespace Content.Shared.Access.Systems;
  19. public sealed class AccessReaderSystem : EntitySystem
  20. {
  21. [Dependency] private readonly IPrototypeManager _prototype = default!;
  22. [Dependency] private readonly InventorySystem _inventorySystem = default!;
  23. [Dependency] private readonly IGameTiming _gameTiming = default!;
  24. [Dependency] private readonly EmagSystem _emag = default!;
  25. [Dependency] private readonly SharedGameTicker _gameTicker = default!;
  26. [Dependency] private readonly SharedHandsSystem _handsSystem = default!;
  27. [Dependency] private readonly SharedContainerSystem _containerSystem = default!;
  28. [Dependency] private readonly SharedStationRecordsSystem _recordsSystem = default!;
  29. [Dependency] private readonly ILogManager _log = default!;
  30. public const string Sawmill = "accessreader";
  31. protected ISawmill _sawmill = default!;
  32. public override void Initialize()
  33. {
  34. base.Initialize();
  35. _sawmill = _log.GetSawmill(Sawmill);
  36. SubscribeLocalEvent<AccessReaderComponent, GotEmaggedEvent>(OnEmagged);
  37. SubscribeLocalEvent<AccessReaderComponent, LinkAttemptEvent>(OnLinkAttempt);
  38. SubscribeLocalEvent<AccessReaderComponent, ComponentGetState>(OnGetState);
  39. SubscribeLocalEvent<AccessReaderComponent, ComponentHandleState>(OnHandleState);
  40. }
  41. private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args)
  42. {
  43. args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists,
  44. _recordsSystem.Convert(component.AccessKeys), component.AccessLog, component.AccessLogLimit);
  45. }
  46. private void OnHandleState(EntityUid uid, AccessReaderComponent component, ref ComponentHandleState args)
  47. {
  48. if (args.Current is not AccessReaderComponentState state)
  49. return;
  50. component.Enabled = state.Enabled;
  51. component.AccessKeys.Clear();
  52. foreach (var key in state.AccessKeys)
  53. {
  54. var id = EnsureEntity<AccessReaderComponent>(key.Item1, uid);
  55. if (!id.IsValid())
  56. continue;
  57. component.AccessKeys.Add(new StationRecordKey(key.Item2, id));
  58. }
  59. component.AccessLists = new(state.AccessLists);
  60. component.DenyTags = new(state.DenyTags);
  61. component.AccessLog = new(state.AccessLog);
  62. component.AccessLogLimit = state.AccessLogLimit;
  63. }
  64. private void OnLinkAttempt(EntityUid uid, AccessReaderComponent component, LinkAttemptEvent args)
  65. {
  66. if (args.User == null) // AutoLink (and presumably future external linkers) have no user.
  67. return;
  68. if (!IsAllowed(args.User.Value, uid, component))
  69. args.Cancel();
  70. }
  71. private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args)
  72. {
  73. if (!_emag.CompareFlag(args.Type, EmagType.Access))
  74. return;
  75. if (!reader.BreakOnAccessBreaker)
  76. return;
  77. if (!GetMainAccessReader(uid, out var accessReader))
  78. return;
  79. if (accessReader.Value.Comp.AccessLists.Count < 1)
  80. return;
  81. args.Repeatable = true;
  82. args.Handled = true;
  83. accessReader.Value.Comp.AccessLists.Clear();
  84. accessReader.Value.Comp.AccessLog.Clear();
  85. Dirty(uid, reader);
  86. }
  87. /// <summary>
  88. /// Searches the source for access tags
  89. /// then compares it with the all targets accesses to see if it is allowed.
  90. /// </summary>
  91. /// <param name="user">The entity that wants access.</param>
  92. /// <param name="target">The entity to search for an access reader</param>
  93. /// <param name="reader">Optional reader from the target entity</param>
  94. public bool IsAllowed(EntityUid user, EntityUid target, AccessReaderComponent? reader = null)
  95. {
  96. if (!Resolve(target, ref reader, false))
  97. return true;
  98. if (!reader.Enabled)
  99. return true;
  100. var accessSources = FindPotentialAccessItems(user);
  101. var access = FindAccessTags(user, accessSources);
  102. FindStationRecordKeys(user, out var stationKeys, accessSources);
  103. if (IsAllowed(access, stationKeys, target, reader))
  104. {
  105. LogAccess((target, reader), user);
  106. return true;
  107. }
  108. //Also check if the user has access levels in their components
  109. if (TryComp<AccessComponent>(user, out var comp))
  110. {
  111. foreach (var tag in comp.Tags)
  112. {
  113. foreach (var alist in reader.AccessLists)
  114. {
  115. if (alist.Contains(tag))
  116. return true;
  117. }
  118. }
  119. }
  120. return false;
  121. }
  122. public bool GetMainAccessReader(EntityUid uid, [NotNullWhen(true)] out Entity<AccessReaderComponent>? ent)
  123. {
  124. ent = null;
  125. if (!TryComp<AccessReaderComponent>(uid, out var accessReader))
  126. return false;
  127. ent = (uid, accessReader);
  128. if (ent.Value.Comp.ContainerAccessProvider == null)
  129. return true;
  130. if (!_containerSystem.TryGetContainer(uid, ent.Value.Comp.ContainerAccessProvider, out var container))
  131. return true;
  132. foreach (var entity in container.ContainedEntities)
  133. {
  134. if (TryComp<AccessReaderComponent>(entity, out var containedReader))
  135. {
  136. ent = (entity, containedReader);
  137. return true;
  138. }
  139. }
  140. return true;
  141. }
  142. /// <summary>
  143. /// Check whether the given access permissions satisfy an access reader's requirements.
  144. /// </summary>
  145. public bool IsAllowed(
  146. ICollection<ProtoId<AccessLevelPrototype>> access,
  147. ICollection<StationRecordKey> stationKeys,
  148. EntityUid target,
  149. AccessReaderComponent reader)
  150. {
  151. if (!reader.Enabled)
  152. return true;
  153. if (reader.ContainerAccessProvider == null)
  154. return IsAllowedInternal(access, stationKeys, reader);
  155. if (!_containerSystem.TryGetContainer(target, reader.ContainerAccessProvider, out var container))
  156. return false;
  157. // If entity is paused then always allow it at this point.
  158. // Door electronics is kind of a mess but yeah, it should only be an unpaused ent interacting with it
  159. if (Paused(target))
  160. return true;
  161. foreach (var entity in container.ContainedEntities)
  162. {
  163. if (!TryComp(entity, out AccessReaderComponent? containedReader))
  164. continue;
  165. if (IsAllowed(access, stationKeys, entity, containedReader))
  166. return true;
  167. }
  168. return false;
  169. }
  170. private bool IsAllowedInternal(ICollection<ProtoId<AccessLevelPrototype>> access, ICollection<StationRecordKey> stationKeys, AccessReaderComponent reader)
  171. {
  172. return !reader.Enabled
  173. || AreAccessTagsAllowed(access, reader)
  174. || AreStationRecordKeysAllowed(stationKeys, reader);
  175. }
  176. /// <summary>
  177. /// Compares the given tags with the readers access list to see if it is allowed.
  178. /// </summary>
  179. /// <param name="accessTags">A list of access tags</param>
  180. /// <param name="reader">An access reader to check against</param>
  181. public bool AreAccessTagsAllowed(ICollection<ProtoId<AccessLevelPrototype>> accessTags, AccessReaderComponent reader)
  182. {
  183. if (reader.DenyTags.Overlaps(accessTags))
  184. {
  185. // Sec owned by cargo.
  186. // Note that in resolving the issue with only one specific item "counting" for access, this became a bit more strict.
  187. // As having an ID card in any slot that "counts" with a denied access group will cause denial of access.
  188. // DenyTags doesn't seem to be used right now anyway, though, so it'll be dependent on whoever uses it to figure out if this matters.
  189. return false;
  190. }
  191. if (reader.AccessLists.Count == 0)
  192. return true;
  193. foreach (var set in reader.AccessLists)
  194. {
  195. if (set.IsSubsetOf(accessTags))
  196. return true;
  197. }
  198. return false;
  199. }
  200. /// <summary>
  201. /// Compares the given stationrecordkeys with the accessreader to see if it is allowed.
  202. /// </summary>
  203. public bool AreStationRecordKeysAllowed(ICollection<StationRecordKey> keys, AccessReaderComponent reader)
  204. {
  205. foreach (var key in reader.AccessKeys)
  206. {
  207. if (keys.Contains(key))
  208. return true;
  209. }
  210. return false;
  211. }
  212. /// <summary>
  213. /// Finds all the items that could potentially give access to a given entity
  214. /// </summary>
  215. public HashSet<EntityUid> FindPotentialAccessItems(EntityUid uid)
  216. {
  217. FindAccessItemsInventory(uid, out var items);
  218. var ev = new GetAdditionalAccessEvent
  219. {
  220. Entities = items
  221. };
  222. RaiseLocalEvent(uid, ref ev);
  223. foreach (var item in new ValueList<EntityUid>(items))
  224. {
  225. items.UnionWith(FindPotentialAccessItems(item));
  226. }
  227. items.Add(uid);
  228. return items;
  229. }
  230. /// <summary>
  231. /// Finds the access tags on the given entity
  232. /// </summary>
  233. /// <param name="uid">The entity that is being searched.</param>
  234. /// <param name="items">All of the items to search for access. If none are passed in, <see cref="FindPotentialAccessItems"/> will be used.</param>
  235. public ICollection<ProtoId<AccessLevelPrototype>> FindAccessTags(EntityUid uid, HashSet<EntityUid>? items = null)
  236. {
  237. HashSet<ProtoId<AccessLevelPrototype>>? tags = null;
  238. var owned = false;
  239. items ??= FindPotentialAccessItems(uid);
  240. foreach (var ent in items)
  241. {
  242. FindAccessTagsItem(ent, ref tags, ref owned);
  243. }
  244. return (ICollection<ProtoId<AccessLevelPrototype>>?)tags ?? Array.Empty<ProtoId<AccessLevelPrototype>>();
  245. }
  246. /// <summary>
  247. /// Finds the access tags on the given entity
  248. /// </summary>
  249. /// <param name="uid">The entity that is being searched.</param>
  250. /// <param name="recordKeys"></param>
  251. /// <param name="items">All of the items to search for access. If none are passed in, <see cref="FindPotentialAccessItems"/> will be used.</param>
  252. public bool FindStationRecordKeys(EntityUid uid, out ICollection<StationRecordKey> recordKeys, HashSet<EntityUid>? items = null)
  253. {
  254. recordKeys = new HashSet<StationRecordKey>();
  255. items ??= FindPotentialAccessItems(uid);
  256. foreach (var ent in items)
  257. {
  258. if (FindStationRecordKeyItem(ent, out var key))
  259. recordKeys.Add(key.Value);
  260. }
  261. return recordKeys.Any();
  262. }
  263. /// <summary>
  264. /// Try to find <see cref="AccessComponent"/> on this item
  265. /// or inside this item (if it's pda)
  266. /// This version merges into a set or replaces the set.
  267. /// If owned is false, the existing tag-set "isn't ours" and can't be merged with (is read-only).
  268. /// </summary>
  269. private void FindAccessTagsItem(EntityUid uid, ref HashSet<ProtoId<AccessLevelPrototype>>? tags, ref bool owned)
  270. {
  271. if (!FindAccessTagsItem(uid, out var targetTags))
  272. {
  273. // no tags, no problem
  274. return;
  275. }
  276. if (tags != null)
  277. {
  278. // existing tags, so copy to make sure we own them
  279. if (!owned)
  280. {
  281. tags = new(tags);
  282. owned = true;
  283. }
  284. // then merge
  285. tags.UnionWith(targetTags);
  286. }
  287. else
  288. {
  289. // no existing tags, so now they're ours
  290. tags = targetTags;
  291. owned = false;
  292. }
  293. }
  294. public void SetAccesses(EntityUid uid, AccessReaderComponent component, List<ProtoId<AccessLevelPrototype>> accesses)
  295. {
  296. component.AccessLists.Clear();
  297. foreach (var access in accesses)
  298. {
  299. component.AccessLists.Add(new HashSet<ProtoId<AccessLevelPrototype>>() { access });
  300. }
  301. Dirty(uid, component);
  302. RaiseLocalEvent(uid, new AccessReaderConfigurationChangedEvent());
  303. }
  304. public bool FindAccessItemsInventory(EntityUid uid, out HashSet<EntityUid> items)
  305. {
  306. items = new();
  307. foreach (var item in _handsSystem.EnumerateHeld(uid))
  308. {
  309. items.Add(item);
  310. }
  311. // maybe its inside an inventory slot?
  312. if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid))
  313. {
  314. items.Add(idUid.Value);
  315. }
  316. return items.Any();
  317. }
  318. /// <summary>
  319. /// Try to find <see cref="AccessComponent"/> on this item
  320. /// or inside this item (if it's pda)
  321. /// </summary>
  322. private bool FindAccessTagsItem(EntityUid uid, out HashSet<ProtoId<AccessLevelPrototype>> tags)
  323. {
  324. tags = new();
  325. var ev = new GetAccessTagsEvent(tags, _prototype);
  326. RaiseLocalEvent(uid, ref ev);
  327. return tags.Count != 0;
  328. }
  329. /// <summary>
  330. /// Try to find <see cref="StationRecordKeyStorageComponent"/> on this item
  331. /// or inside this item (if it's pda)
  332. /// </summary>
  333. private bool FindStationRecordKeyItem(EntityUid uid, [NotNullWhen(true)] out StationRecordKey? key)
  334. {
  335. if (TryComp(uid, out StationRecordKeyStorageComponent? storage) && storage.Key != null)
  336. {
  337. key = storage.Key;
  338. return true;
  339. }
  340. if (TryComp<PdaComponent>(uid, out var pda) &&
  341. pda.ContainedId is { Valid: true } id)
  342. {
  343. if (TryComp<StationRecordKeyStorageComponent>(id, out var pdastorage) && pdastorage.Key != null)
  344. {
  345. key = pdastorage.Key;
  346. return true;
  347. }
  348. }
  349. key = null;
  350. return false;
  351. }
  352. /// <summary>
  353. /// Logs an access for a specific entity.
  354. /// </summary>
  355. /// <param name="ent">The reader to log the access on</param>
  356. /// <param name="accessor">The accessor to log</param>
  357. public void LogAccess(Entity<AccessReaderComponent> ent, EntityUid accessor)
  358. {
  359. if (IsPaused(ent) || ent.Comp.LoggingDisabled)
  360. return;
  361. string? name = null;
  362. if (TryComp<NameIdentifierComponent>(accessor, out var nameIdentifier))
  363. name = nameIdentifier.FullIdentifier;
  364. // TODO pass the ID card on IsAllowed() instead of using this expensive method
  365. // Set name if the accessor has a card and that card has a name and allows itself to be recorded
  366. var getIdentityShortInfoEvent = new TryGetIdentityShortInfoEvent(ent, accessor, true);
  367. RaiseLocalEvent(getIdentityShortInfoEvent);
  368. if (getIdentityShortInfoEvent.Title != null)
  369. {
  370. name = getIdentityShortInfoEvent.Title;
  371. }
  372. LogAccess(ent, name ?? Loc.GetString("access-reader-unknown-id"));
  373. }
  374. /// <summary>
  375. /// Logs an access with a predetermined name
  376. /// </summary>
  377. /// <param name="ent">The reader to log the access on</param>
  378. /// <param name="name">The name to log as</param>
  379. public void LogAccess(Entity<AccessReaderComponent> ent, string name)
  380. {
  381. if (IsPaused(ent) || ent.Comp.LoggingDisabled)
  382. return;
  383. if (ent.Comp.AccessLog.Count >= ent.Comp.AccessLogLimit)
  384. ent.Comp.AccessLog.Dequeue();
  385. var stationTime = _gameTiming.CurTime.Subtract(_gameTicker.RoundStartTimeSpan);
  386. ent.Comp.AccessLog.Enqueue(new AccessRecord(stationTime, name));
  387. }
  388. }