using Content.Shared.Access.Components; using Content.Shared.DeviceLinking.Events; using Content.Shared.Emag.Components; using Content.Shared.Emag.Systems; using Content.Shared.Hands.EntitySystems; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.StationRecords; using Robust.Shared.Containers; using Robust.Shared.GameStates; using System.Diagnostics.CodeAnalysis; using System.Linq; using Robust.Shared.Collections; using Robust.Shared.Prototypes; namespace Content.Shared.Access.Systems; public sealed class AccessReaderSystem : EntitySystem { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly InventorySystem _inventorySystem = default!; [Dependency] private readonly SharedHandsSystem _handsSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnEmagged); SubscribeLocalEvent(OnLinkAttempt); SubscribeLocalEvent(OnGetState); SubscribeLocalEvent(OnHandleState); } private void OnGetState(EntityUid uid, AccessReaderComponent component, ref ComponentGetState args) { args.State = new AccessReaderComponentState(component.Enabled, component.DenyTags, component.AccessLists, component.AccessKeys); } private void OnHandleState(EntityUid uid, AccessReaderComponent component, ref ComponentHandleState args) { if (args.Current is not AccessReaderComponentState state) return; component.Enabled = state.Enabled; component.AccessKeys = new(state.AccessKeys); component.AccessLists = new(state.AccessLists); component.DenyTags = new(state.DenyTags); } private void OnLinkAttempt(EntityUid uid, AccessReaderComponent component, LinkAttemptEvent args) { if (args.User == null) // AutoLink (and presumably future external linkers) have no user. return; if (!HasComp(uid) && !IsAllowed(args.User.Value, uid, component)) args.Cancel(); } private void OnEmagged(EntityUid uid, AccessReaderComponent reader, ref GotEmaggedEvent args) { args.Handled = true; reader.Enabled = false; Dirty(reader); } /// /// Searches the source for access tags /// then compares it with the all targets accesses to see if it is allowed. /// /// The entity that wants access. /// The entity to search for an access reader /// Optional reader from the target entity public bool IsAllowed(EntityUid user, EntityUid target, AccessReaderComponent? reader = null) { if (!Resolve(target, ref reader, false)) return true; if (!reader.Enabled) return true; var accessSources = FindPotentialAccessItems(user); var access = FindAccessTags(user, accessSources); FindStationRecordKeys(user, out var stationKeys, accessSources); return IsAllowed(access, stationKeys, target, reader); } /// /// Check whether the given access permissions satisfy an access reader's requirements. /// public bool IsAllowed( ICollection access, ICollection stationKeys, EntityUid target, AccessReaderComponent reader) { if (!reader.Enabled) return true; if (reader.ContainerAccessProvider == null) return IsAllowedInternal(access, stationKeys, reader); if (!_containerSystem.TryGetContainer(target, reader.ContainerAccessProvider, out var container)) return false; foreach (var entity in container.ContainedEntities) { if (!TryComp(entity, out AccessReaderComponent? containedReader)) continue; if (IsAllowed(access, stationKeys, entity, containedReader)) return true; } return false; } private bool IsAllowedInternal(ICollection access, ICollection stationKeys, AccessReaderComponent reader) { return !reader.Enabled || AreAccessTagsAllowed(access, reader) || AreStationRecordKeysAllowed(stationKeys, reader); } /// /// Compares the given tags with the readers access list to see if it is allowed. /// /// A list of access tags /// An access reader to check against public bool AreAccessTagsAllowed(ICollection accessTags, AccessReaderComponent reader) { if (reader.DenyTags.Overlaps(accessTags)) { // Sec owned by cargo. // Note that in resolving the issue with only one specific item "counting" for access, this became a bit more strict. // As having an ID card in any slot that "counts" with a denied access group will cause denial of access. // 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. return false; } if (reader.AccessLists.Count == 0) return true; foreach (var set in reader.AccessLists) { if (set.IsSubsetOf(accessTags)) return true; } return false; } /// /// Compares the given stationrecordkeys with the accessreader to see if it is allowed. /// public bool AreStationRecordKeysAllowed(ICollection keys, AccessReaderComponent reader) { foreach (var key in reader.AccessKeys) { if (keys.Contains(key)) return true; } return false; } /// /// Finds all the items that could potentially give access to a given entity /// public HashSet FindPotentialAccessItems(EntityUid uid) { FindAccessItemsInventory(uid, out var items); foreach (var item in new ValueList(items)) { items.UnionWith(FindPotentialAccessItems(item)); } var ev = new GetAdditionalAccessEvent { Entities = items }; RaiseLocalEvent(uid, ref ev); items.Add(uid); return items; } /// /// Finds the access tags on the given entity /// /// The entity that is being searched. /// All of the items to search for access. If none are passed in, will be used. public ICollection FindAccessTags(EntityUid uid, HashSet? items = null) { HashSet? tags = null; var owned = false; items ??= FindPotentialAccessItems(uid); foreach (var ent in items) { FindAccessTagsItem(ent, ref tags, ref owned); } return (ICollection?) tags ?? Array.Empty(); } /// /// Finds the access tags on the given entity /// /// The entity that is being searched. /// /// All of the items to search for access. If none are passed in, will be used. public bool FindStationRecordKeys(EntityUid uid, out ICollection recordKeys, HashSet? items = null) { recordKeys = new HashSet(); items ??= FindPotentialAccessItems(uid); foreach (var ent in items) { if (FindStationRecordKeyItem(ent, out var key)) recordKeys.Add(key.Value); } return recordKeys.Any(); } /// /// Try to find on this item /// or inside this item (if it's pda) /// This version merges into a set or replaces the set. /// If owned is false, the existing tag-set "isn't ours" and can't be merged with (is read-only). /// private void FindAccessTagsItem(EntityUid uid, ref HashSet? tags, ref bool owned) { if (!FindAccessTagsItem(uid, out var targetTags)) { // no tags, no problem return; } if (tags != null) { // existing tags, so copy to make sure we own them if (!owned) { tags = new(tags); owned = true; } // then merge tags.UnionWith(targetTags); } else { // no existing tags, so now they're ours tags = targetTags; owned = false; } } public bool FindAccessItemsInventory(EntityUid uid, out HashSet items) { items = new(); foreach (var item in _handsSystem.EnumerateHeld(uid)) { items.Add(item); } // maybe its inside an inventory slot? if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid)) { items.Add(idUid.Value); } return items.Any(); } /// /// Try to find on this item /// or inside this item (if it's pda) /// private bool FindAccessTagsItem(EntityUid uid, out HashSet tags) { tags = new(); var ev = new GetAccessTagsEvent(tags, _prototype); RaiseLocalEvent(uid, ref ev); return tags.Count != 0; } /// /// Try to find on this item /// or inside this item (if it's pda) /// private bool FindStationRecordKeyItem(EntityUid uid, [NotNullWhen(true)] out StationRecordKey? key) { if (TryComp(uid, out StationRecordKeyStorageComponent? storage) && storage.Key != null) { key = storage.Key; return true; } if (TryComp(uid, out var pda) && pda.ContainedId is { Valid: true } id) { if (TryComp(id, out var pdastorage) && pdastorage.Key != null) { key = pdastorage.Key; return true; } } key = null; return false; } }