using System.Linq; using Content.Server.Popups; using Content.Shared.Access; using Content.Shared.Access.Components; using Content.Shared.Access.Systems; using Content.Shared.Administration.Logs; using Content.Shared.Database; using Content.Shared.DoAfter; using Content.Shared.Interaction; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Audio; using Robust.Shared.Audio.Systems; using Robust.Shared.Containers; using Robust.Shared.Player; using Robust.Shared.Prototypes; using static Content.Shared.Access.Components.AccessOverriderComponent; namespace Content.Server.Access.Systems; [UsedImplicitly] public sealed class AccessOverriderSystem : SharedAccessOverriderSystem { [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedInteractionSystem _interactionSystem = default!; [Dependency] private readonly PopupSystem _popupSystem = default!; [Dependency] private readonly SharedAudioSystem _audioSystem = default!; [Dependency] private readonly SharedDoAfterSystem _doAfterSystem = default!; [Dependency] private readonly SharedContainerSystem _containerSystem = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(AfterInteractOn); SubscribeLocalEvent(OnDoAfter); Subs.BuiEvents(AccessOverriderUiKey.Key, subs => { subs.Event(UpdateUserInterface); subs.Event(OnClose); subs.Event(OnWriteToTargetAccessReaderIdMessage); }); } private void AfterInteractOn(EntityUid uid, AccessOverriderComponent component, AfterInteractEvent args) { if (args.Target == null || !TryComp(args.Target, out AccessReaderComponent? accessReader)) return; if (!_interactionSystem.InRangeUnobstructed(args.User, (EntityUid) args.Target)) return; var doAfterEventArgs = new DoAfterArgs(EntityManager, args.User, component.DoAfter, new AccessOverriderDoAfterEvent(), uid, target: args.Target, used: uid) { BreakOnMove = true, BreakOnDamage = true, NeedHand = true, }; _doAfterSystem.TryStartDoAfter(doAfterEventArgs); } private void OnDoAfter(EntityUid uid, AccessOverriderComponent component, AccessOverriderDoAfterEvent args) { if (args.Handled || args.Cancelled) return; if (args.Args.Target != null) { component.TargetAccessReaderId = args.Args.Target.Value; _userInterface.OpenUi(uid, AccessOverriderUiKey.Key, args.User); UpdateUserInterface(uid, component, args); } args.Handled = true; } private void OnClose(EntityUid uid, AccessOverriderComponent component, BoundUIClosedEvent args) { if (args.UiKey.Equals(AccessOverriderUiKey.Key)) { component.TargetAccessReaderId = new(); } } private void OnWriteToTargetAccessReaderIdMessage(EntityUid uid, AccessOverriderComponent component, WriteToTargetAccessReaderIdMessage args) { if (args.Actor is not { Valid: true } player) return; TryWriteToTargetAccessReaderId(uid, args.AccessList, player, component); UpdateUserInterface(uid, component, args); } private void UpdateUserInterface(EntityUid uid, AccessOverriderComponent component, EntityEventArgs args) { if (!component.Initialized) return; var privilegedIdName = string.Empty; var targetLabel = Loc.GetString("access-overrider-window-no-target"); var targetLabelColor = Color.Red; ProtoId[]? possibleAccess = null; ProtoId[]? currentAccess = null; ProtoId[]? missingAccess = null; if (component.TargetAccessReaderId is { Valid: true } accessReader) { targetLabel = Loc.GetString("access-overrider-window-target-label") + " " + EntityManager.GetComponent(component.TargetAccessReaderId).EntityName; targetLabelColor = Color.White; if (!_accessReader.GetMainAccessReader(accessReader, out var accessReaderComponent)) return; var currentAccessHashsets = accessReaderComponent.AccessLists; currentAccess = ConvertAccessHashSetsToList(currentAccessHashsets).ToArray(); } if (component.PrivilegedIdSlot.Item is { Valid: true } idCard) { privilegedIdName = EntityManager.GetComponent(idCard).EntityName; if (component.TargetAccessReaderId is { Valid: true }) { possibleAccess = _accessReader.FindAccessTags(idCard).ToArray(); } if (currentAccess != null && possibleAccess != null) { missingAccess = currentAccess.Except(possibleAccess).ToArray(); } } AccessOverriderBoundUserInterfaceState newState; newState = new AccessOverriderBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, PrivilegedIdIsAuthorized(uid, component), currentAccess, possibleAccess, missingAccess, privilegedIdName, targetLabel, targetLabelColor); _userInterface.SetUiState(uid, AccessOverriderUiKey.Key, newState); } private List> ConvertAccessHashSetsToList(List>> accessHashsets) { var accessList = new List>(); if (accessHashsets.Count <= 0) return accessList; foreach (var hashSet in accessHashsets) { accessList.AddRange(hashSet); } return accessList; } private List>> ConvertAccessListToHashSet(List> accessList) { List>> accessHashsets = new List>>(); if (accessList != null && accessList.Any()) { foreach (ProtoId access in accessList) { accessHashsets.Add(new HashSet>() { access }); } } return accessHashsets; } /// /// Called whenever an access button is pressed, adding or removing that access requirement from the target access reader. /// private void TryWriteToTargetAccessReaderId(EntityUid uid, List> newAccessList, EntityUid player, AccessOverriderComponent? component = null) { if (!Resolve(uid, ref component) || component.TargetAccessReaderId is not { Valid: true }) return; if (!PrivilegedIdIsAuthorized(uid, component)) return; if (!_interactionSystem.InRangeUnobstructed(uid, component.TargetAccessReaderId)) { _popupSystem.PopupEntity(Loc.GetString("access-overrider-out-of-range"), player, player); return; } if (newAccessList.Count > 0 && !newAccessList.TrueForAll(x => component.AccessLevels.Contains(x))) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag."); return; } if (!_accessReader.GetMainAccessReader(component.TargetAccessReaderId, out var accessReader)) return; var oldTags = ConvertAccessHashSetsToList(accessReader.AccessLists); var privilegedId = component.PrivilegedIdSlot.Item; if (oldTags.SequenceEqual(newAccessList)) return; var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet(); var privilegedPerms = _accessReader.FindAccessTags(privilegedId!.Value).ToHashSet(); if (!difference.IsSubsetOf(privilegedPerms)) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions they could not give/take!"); return; } if (!oldTags.ToHashSet().IsSubsetOf(privilegedPerms)) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to modify permissions when they do not have sufficient access!"); _popupSystem.PopupEntity(Loc.GetString("access-overrider-cannot-modify-access"), player, player); _audioSystem.PlayPvs(component.DenialSound, uid); return; } var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList(); var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList(); _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(player):player} has modified {ToPrettyString(component.TargetAccessReaderId):entity} with the following allowed access level holders: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); accessReader.AccessLists = ConvertAccessListToHashSet(newAccessList); Dirty(component.TargetAccessReaderId, accessReader); } /// /// Returns true if there is an ID in and said ID satisfies the requirements of . /// /// /// Other code relies on the fact this returns false if privileged Id is null. Don't break that invariant. /// private bool PrivilegedIdIsAuthorized(EntityUid uid, AccessOverriderComponent? component = null) { if (!Resolve(uid, ref component)) return true; if (_accessReader.GetMainAccessReader(uid, out var accessReader)) return true; var privilegedId = component.PrivilegedIdSlot.Item; return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, accessReader); } }