using System.Linq; using Content.Server.Chat.Systems; using Content.Server.Containers; using Content.Server.StationRecords.Systems; using Content.Shared.Access.Components; using static Content.Shared.Access.Components.IdCardConsoleComponent; using Content.Shared.Access.Systems; using Content.Shared.Access; using Content.Shared.Administration.Logs; using Content.Shared.Construction; using Content.Shared.Containers.ItemSlots; using Content.Shared.Damage; using Content.Shared.Database; using Content.Shared.Roles; using Content.Shared.StationRecords; using Content.Shared.Throwing; using JetBrains.Annotations; using Robust.Server.GameObjects; using Robust.Shared.Containers; using Robust.Shared.Prototypes; using Robust.Shared.Random; namespace Content.Server.Access.Systems; [UsedImplicitly] public sealed class IdCardConsoleSystem : SharedIdCardConsoleSystem { [Dependency] private readonly IPrototypeManager _prototype = default!; [Dependency] private readonly StationRecordsSystem _record = default!; [Dependency] private readonly UserInterfaceSystem _userInterface = default!; [Dependency] private readonly AccessReaderSystem _accessReader = default!; [Dependency] private readonly AccessSystem _access = default!; [Dependency] private readonly IdCardSystem _idCard = default!; [Dependency] private readonly ISharedAdminLogManager _adminLogger = default!; [Dependency] private readonly SharedContainerSystem _container = default!; [Dependency] private readonly ThrowingSystem _throwing = default!; [Dependency] private readonly IRobustRandom _random = default!; [Dependency] private readonly ChatSystem _chat = default!; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnWriteToTargetIdMessage); // one day, maybe bound user interfaces can be shared too. SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(UpdateUserInterface); SubscribeLocalEvent(OnDamageChanged); // Intercept the event before anyone can do anything with it! SubscribeLocalEvent(OnMachineDeconstructed, before: [typeof(EmptyOnMachineDeconstructSystem), typeof(ItemSlotsSystem)]); } private void OnWriteToTargetIdMessage(EntityUid uid, IdCardConsoleComponent component, WriteToTargetIdMessage args) { if (args.Actor is not { Valid: true } player) return; TryWriteToTargetId(uid, args.FullName, args.JobTitle, args.AccessList, args.JobPrototype, player, component); UpdateUserInterface(uid, component, args); } private void UpdateUserInterface(EntityUid uid, IdCardConsoleComponent component, EntityEventArgs args) { if (!component.Initialized) return; var privilegedIdName = string.Empty; List>? possibleAccess = null; if (component.PrivilegedIdSlot.Item is { Valid: true } item) { privilegedIdName = Comp(item).EntityName; possibleAccess = _accessReader.FindAccessTags(item).ToList(); } IdCardConsoleBoundUserInterfaceState newState; // this could be prettier if (component.TargetIdSlot.Item is not { Valid: true } targetId) { newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, PrivilegedIdIsAuthorized(uid, component), false, null, null, null, possibleAccess, string.Empty, privilegedIdName, string.Empty); } else { var targetIdComponent = Comp(targetId); var targetAccessComponent = Comp(targetId); var jobProto = targetIdComponent.JobPrototype ?? new ProtoId(string.Empty); if (TryComp(targetId, out var keyStorage) && keyStorage.Key is { } key && _record.TryGetRecord(key, out var record)) { jobProto = record.JobPrototype; } newState = new IdCardConsoleBoundUserInterfaceState( component.PrivilegedIdSlot.HasItem, PrivilegedIdIsAuthorized(uid, component), true, targetIdComponent.FullName, targetIdComponent.LocalizedJobTitle, targetAccessComponent.Tags.ToList(), possibleAccess, jobProto, privilegedIdName, Name(targetId)); } _userInterface.SetUiState(uid, IdCardConsoleUiKey.Key, newState); } /// /// Called whenever an access button is pressed, adding or removing that access from the target ID card. /// Writes data passed from the UI into the ID stored in , if present. /// private void TryWriteToTargetId(EntityUid uid, string newFullName, string newJobTitle, List> newAccessList, ProtoId newJobProto, EntityUid player, IdCardConsoleComponent? component = null) { if (!Resolve(uid, ref component)) return; if (component.TargetIdSlot.Item is not { Valid: true } targetId || !PrivilegedIdIsAuthorized(uid, component)) return; _idCard.TryChangeFullName(targetId, newFullName, player: player); _idCard.TryChangeJobTitle(targetId, newJobTitle, player: player); if (_prototype.TryIndex(newJobProto, out var job) && _prototype.Resolve(job.Icon, out var jobIcon)) { _idCard.TryChangeJobIcon(targetId, jobIcon, player: player); _idCard.TryChangeJobDepartment(targetId, job); } UpdateStationRecord(uid, targetId, newFullName, newJobTitle, job); if ((!TryComp(targetId, out var keyStorage) || keyStorage.Key is not { } key || !_record.TryGetRecord(key, out _)) && newJobProto != string.Empty) { Comp(targetId).JobPrototype = newJobProto; } if (!newAccessList.TrueForAll(x => component.AccessLevels.Contains(x))) { _sawmill.Warning($"User {ToPrettyString(uid)} tried to write unknown access tag."); return; } var oldTags = _access.TryGetTags(targetId) ?? new List>(); oldTags = oldTags.ToList(); var privilegedId = component.PrivilegedIdSlot.Item; if (oldTags.SequenceEqual(newAccessList)) return; // I hate that C# doesn't have an option for this and don't desire to write this out the hard way. // var difference = newAccessList.Difference(oldTags); var difference = newAccessList.Union(oldTags).Except(newAccessList.Intersect(oldTags)).ToHashSet(); // NULL SAFETY: PrivilegedIdIsAuthorized checked this earlier. 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; } var addedTags = newAccessList.Except(oldTags).Select(tag => "+" + tag).ToList(); var removedTags = oldTags.Except(newAccessList).Select(tag => "-" + tag).ToList(); _access.TrySetTags(targetId, newAccessList); /*TODO: ECS SharedIdCardConsoleComponent and then log on card ejection, together with the save. This current implementation is pretty shit as it logs 27 entries (27 lines) if someone decides to give themselves AA*/ _adminLogger.Add(LogType.Action, LogImpact.Medium, $"{ToPrettyString(player):player} has modified {ToPrettyString(targetId):entity} with the following accesses: [{string.Join(", ", addedTags.Union(removedTags))}] [{string.Join(", ", newAccessList)}]"); } /// /// 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, IdCardConsoleComponent? component = null) { if (!Resolve(uid, ref component)) return true; if (!TryComp(uid, out var reader)) return true; var privilegedId = component.PrivilegedIdSlot.Item; return privilegedId != null && _accessReader.IsAllowed(privilegedId.Value, uid, reader); } private void UpdateStationRecord(EntityUid uid, EntityUid targetId, string newFullName, ProtoId newJobTitle, JobPrototype? newJobProto) { if (!TryComp(targetId, out var keyStorage) || keyStorage.Key is not { } key || !_record.TryGetRecord(key, out var record)) { return; } record.Name = newFullName; record.JobTitle = newJobTitle; if (newJobProto != null) { record.JobPrototype = newJobProto.ID; record.JobIcon = newJobProto.Icon; } _record.Synchronize(key); } private void OnMachineDeconstructed(Entity entity, ref MachineDeconstructedEvent args) { TryDropAndThrowIds(entity.AsNullable()); } private void OnDamageChanged(Entity entity, ref DamageChangedEvent args) { if (TryDropAndThrowIds(entity.AsNullable())) _chat.TrySendInGameICMessage(entity, Loc.GetString("id-card-console-damaged"), InGameICChatType.Speak, true); } #region PublicAPI /// /// Tries to drop any IDs stored in the console, and then tries to throw them away. /// Returns true if anything was ejected and false otherwise. /// public bool TryDropAndThrowIds(Entity ent) { if (!Resolve(ent, ref ent.Comp1, ref ent.Comp2)) return false; var didEject = false; foreach (var slot in ent.Comp2.Slots.Values) { if (slot.Item == null || slot.ContainerSlot == null) continue; var item = slot.Item.Value; if (_container.Remove(item, slot.ContainerSlot)) { _throwing.TryThrow(item, _random.NextVector2(), baseThrowSpeed: 5f); didEject = true; } } return didEject; } #endregion }