using System.Globalization; using Content.Shared.Access.Components; using Content.Shared.Administration.Logs; using Content.Shared.CCVar; using Content.Shared.Database; using Content.Shared.Hands.EntitySystems; using Content.Shared.IdentityManagement; using Content.Shared.Inventory; using Content.Shared.PDA; using Content.Shared.Roles; using Content.Shared.StatusIcon; using Robust.Shared.Configuration; using Robust.Shared.Prototypes; using Robust.Shared.Timing; namespace Content.Shared.Access.Systems; public abstract partial class SharedIdCardSystem : EntitySystem { [Dependency] private IConfigurationManager _cfgManager = default!; [Dependency] private ISharedAdminLogManager _adminLogger = default!; [Dependency] private IGameTiming _timing = default!; [Dependency] private SharedAccessSystem _access = default!; [Dependency] private SharedHandsSystem _hands = default!; [Dependency] private InventorySystem _inventorySystem = default!; [Dependency] private MetaDataSystem _metaSystem = default!; [Dependency] private SharedJobStatusSystem _jobStatus = default!; [Dependency] private SharedAgentIdCardSystem _agentIdCard = default!; // CCVar. private int _maxNameLength; private int _maxIdJobLength; public override void Initialize() { base.Initialize(); SubscribeLocalEvent(OnMapInit); SubscribeLocalEvent(OnHandleState); SubscribeLocalEvent(OnTryGetIdentityShortInfo); SubscribeLocalEvent(OnRename); Subs.CVar(_cfgManager, CCVars.MaxNameLength, value => _maxNameLength = value, true); Subs.CVar(_cfgManager, CCVars.MaxIdJobLength, value => _maxIdJobLength = value, true); } private void OnRename(ref EntityRenamedEvent ev) { // When a player gets renamed their id card is renamed as well to match. // Unfortunately since TryFindIdCard will succeed if the entity is also a card this means that the card will // keep renaming itself unless we return early. // We also do not include the PDA itself being renamed, as that triggers the same event (e.g. for chameleon PDAs). if (HasComp(ev.Uid) || HasComp(ev.Uid)) return; if (TryFindIdCard(ev.Uid, out var idCard)) TryChangeFullName(idCard, ev.NewName, idCard); } private void OnMapInit(EntityUid uid, IdCardComponent id, MapInitEvent args) { UpdateEntityName(uid, id); } private void OnTryGetIdentityShortInfo(TryGetIdentityShortInfoEvent ev) { if (ev.Handled) return; if (TryFindIdCard(ev.Target, out var idCard) && !(ev.RequestForAccessLogging && idCard.Comp.BypassLogging)) { ev.Title = ExtractFullTitle(idCard); ev.Handled = true; } } private void OnHandleState(Entity ent, ref AfterAutoHandleStateEvent args) { // Try to update the job status icon of the player owning the ID, if any. _jobStatus.UpdateIdHolderStatus(ent); _agentIdCard.UpdateUi(ent); } /// /// Attempt to find an ID card on an entity. This will look in the entity itself, in the entity's hands, and /// in the entity's inventory. /// public bool TryFindIdCard(EntityUid uid, out Entity idCard) { // check held item? if (_hands.GetActiveItem(uid) is { } heldItem && TryGetIdCard(heldItem, out idCard)) { return true; } // check entity itself if (TryGetIdCard(uid, out idCard)) return true; // check inventory slot? if (_inventorySystem.TryGetSlotEntity(uid, "id", out var idUid) && TryGetIdCard(idUid.Value, out idCard)) return true; return false; } /// /// Attempt to get an id card component from an entity, either by getting it directly from the entity, or by /// getting the contained id from a . /// public bool TryGetIdCard(EntityUid uid, out Entity idCard) { if (TryComp(uid, out IdCardComponent? idCardComp)) { idCard = (uid, idCardComp); return true; } if (TryComp(uid, out PdaComponent? pda) && TryComp(pda.ContainedId, out idCardComp)) { idCard = (pda.ContainedId.Value, idCardComp); return true; } idCard = default; return false; } /// /// Attempts to change the job title of a card. /// Returns true/false. /// /// /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. /// Actually works with the LocalizedJobTitle DataField and not with JobTitle. /// /// True if the job title changed, false if nothing changed. public bool TryChangeJobTitle(EntityUid uid, string? jobTitle, IdCardComponent? id = null, EntityUid? player = null) { if (!Resolve(uid, ref id)) return false; if (!string.IsNullOrWhiteSpace(jobTitle)) { jobTitle = jobTitle.Trim(); if (jobTitle.Length > _maxIdJobLength) jobTitle = jobTitle[.._maxIdJobLength]; } else { jobTitle = null; } if (id.LocalizedJobTitle == jobTitle) return false; id.LocalizedJobTitle = jobTitle; Dirty(uid, id); UpdateEntityName(uid, id); if (player != null) { _adminLogger.Add(LogType.Identity, LogImpact.Low, $"{ToPrettyString(player.Value):player} has changed the job title of {ToPrettyString(uid):entity} to {jobTitle} "); } return true; } /// True if the job icon changed, false if nothing changed. public bool TryChangeJobIcon(EntityUid uid, JobIconPrototype jobIcon, IdCardComponent? id = null, EntityUid? player = null) { if (!Resolve(uid, ref id)) { return false; } if (id.JobIcon == jobIcon.ID) { return false; } id.JobIcon = jobIcon.ID; Dirty(uid, id); if (player != null) { _adminLogger.Add(LogType.Identity, LogImpact.Low, $"{ToPrettyString(player.Value):player} has changed the job icon of {ToPrettyString(uid):entity} to {jobIcon} "); } return true; } public bool TryChangeJobDepartment(EntityUid uid, JobPrototype job, IdCardComponent? id = null) { if (!Resolve(uid, ref id)) return false; id.JobDepartments.Clear(); foreach (var department in ProtoMan.EnumeratePrototypes()) { if (department.Roles.Contains(job.ID)) id.JobDepartments.Add(department.ID); } Dirty(uid, id); return true; } public bool TryChangeJobDepartment(EntityUid uid, List> departments, IdCardComponent? id = null) { if (!Resolve(uid, ref id)) return false; id.JobDepartments.Clear(); foreach (var department in departments) { id.JobDepartments.Add(department); } Dirty(uid, id); return true; } /// /// Attempts to change the full name of a card. /// Returns true/false. /// /// /// If provided with a player's EntityUid to the player parameter, adds the change to the admin logs. /// /// True if the name changed, false if nothing changed. public bool TryChangeFullName(EntityUid uid, string? fullName, IdCardComponent? id = null, EntityUid? player = null) { if (!Resolve(uid, ref id)) return false; if (!string.IsNullOrWhiteSpace(fullName)) { fullName = fullName.Trim(); if (fullName.Length > _maxNameLength) fullName = fullName[.._maxNameLength]; } else { fullName = null; } if (id.FullName == fullName) return false; id.FullName = fullName; Dirty(uid, id); UpdateEntityName(uid, id); if (player != null) { _adminLogger.Add(LogType.Identity, LogImpact.Low, $"{ToPrettyString(player.Value):player} has changed the name of {ToPrettyString(uid):entity} to {fullName} "); } return true; } /// /// Changes the name of the id's owner. /// /// /// If either or is empty, it's replaced by placeholders. /// If both are empty, the original entity's name is restored. /// private void UpdateEntityName(EntityUid uid, IdCardComponent? id = null) { if (!Resolve(uid, ref id)) return; var jobSuffix = string.IsNullOrWhiteSpace(id.LocalizedJobTitle) ? string.Empty : $" ({id.LocalizedJobTitle})"; var val = string.IsNullOrWhiteSpace(id.FullName) ? Loc.GetString(id.NameLocId, ("jobSuffix", jobSuffix)) : Loc.GetString(id.FullNameLocId, ("fullName", id.FullName), ("jobSuffix", jobSuffix)); _metaSystem.SetEntityName(uid, val); } private static string ExtractFullTitle(IdCardComponent idCardComponent) { return $"{idCardComponent.FullName} ({CultureInfo.CurrentCulture.TextInfo.ToTitleCase(idCardComponent.LocalizedJobTitle ?? string.Empty)})" .Trim(); } public void SetExpireTime(Entity ent, TimeSpan time) { if (!Resolve(ent, ref ent.Comp)) return; ent.Comp.ExpireTime = time; ent.Comp.Expired = false; Dirty(ent); } /// /// Marks an as expired, setting the accesses. /// public virtual bool ExpireId(Entity ent) { if (ent.Comp.Expired) return false; _access.TrySetTags(ent, ent.Comp.ExpiredAccess); var pauseTime = _metaSystem.GetPauseTime(ent.Owner); ent.Comp.ExpireTime ??= _timing.CurTime - pauseTime - TimeSpan.FromTicks(1); ent.Comp.Expired = true; Dirty(ent); return true; } public override void Update(float frameTime) { base.Update(frameTime); var query = EntityQueryEnumerator(); while (query.MoveNext(out var uid, out var comp)) { if (comp.Expired || comp.ExpireTime is not { } expireTime) continue; var pauseTime = _metaSystem.GetPauseTime(uid); if (expireTime > TimeSpan.MaxValue - pauseTime) continue; if (_timing.CurTime <= expireTime + pauseTime) continue; ExpireId((uid, comp)); } } }