using System.Linq; using Content.Server.Afk; using Content.Server.Afk.Events; using Content.Server.GameTicking; using Content.Server.Roles; using Content.Shared.CCVar; using Content.Shared.GameTicking; using Content.Shared.Mobs; using Content.Shared.Mobs.Components; using Content.Shared.Players.PlayTimeTracking; using Content.Shared.Roles; using Robust.Server.GameObjects; using Robust.Server.Player; using Robust.Shared.Configuration; using Robust.Shared.Network; using Robust.Shared.Prototypes; using Robust.Shared.Utility; namespace Content.Server.Players.PlayTimeTracking; /// /// Connects to the simulation state. Reports trackers and such. /// public sealed class PlayTimeTrackingSystem : EntitySystem { [Dependency] private readonly IAfkManager _afk = default!; [Dependency] private readonly IPlayerManager _playerManager = default!; [Dependency] private readonly IPrototypeManager _prototypes = default!; [Dependency] private readonly IConfigurationManager _cfg = default!; [Dependency] private readonly PlayTimeTrackingManager _tracking = default!; public override void Initialize() { base.Initialize(); _tracking.CalcTrackers += CalcTrackers; SubscribeLocalEvent(OnRoundEnd); SubscribeLocalEvent(OnPlayerAttached); SubscribeLocalEvent(OnPlayerDetached); SubscribeLocalEvent(OnRoleAdd); SubscribeLocalEvent(OnRoleRemove); SubscribeLocalEvent(OnAFK); SubscribeLocalEvent(OnUnAFK); SubscribeLocalEvent(OnMobStateChanged); SubscribeLocalEvent(OnPlayerJoinedLobby); } public override void Shutdown() { base.Shutdown(); _tracking.CalcTrackers -= CalcTrackers; } private void CalcTrackers(IPlayerSession player, HashSet trackers) { if (_afk.IsAfk(player)) return; if (!IsPlayerAlive(player)) return; trackers.Add(PlayTimeTrackingShared.TrackerOverall); trackers.UnionWith(GetTimedRoles(player)); } private bool IsPlayerAlive(IPlayerSession session) { var attached = session.AttachedEntity; if (attached == null) return false; if (!TryComp(attached, out var state)) return false; return state.CurrentState is MobState.Alive or MobState.Critical; } public IEnumerable GetTimedRoles(Mind.Mind mind) { foreach (var role in mind.AllRoles) { if (role is not IRoleTimer timer) continue; yield return _prototypes.Index(timer.Timer).ID; } } private IEnumerable GetTimedRoles(IPlayerSession session) { var contentData = _playerManager.GetPlayerData(session.UserId).ContentData(); if (contentData?.Mind == null) return Enumerable.Empty(); return GetTimedRoles(contentData.Mind); } private void OnRoleRemove(RoleRemovedEvent ev) { if (ev.Mind.Session == null) return; _tracking.QueueRefreshTrackers(ev.Mind.Session); } private void OnRoleAdd(RoleAddedEvent ev) { if (ev.Mind.Session == null) return; _tracking.QueueRefreshTrackers(ev.Mind.Session); } private void OnRoundEnd(RoundRestartCleanupEvent ev) { _tracking.Save(); } private void OnUnAFK(ref UnAFKEvent ev) { _tracking.QueueRefreshTrackers(ev.Session); } private void OnAFK(ref AFKEvent ev) { _tracking.QueueRefreshTrackers(ev.Session); } private void OnPlayerAttached(PlayerAttachedEvent ev) { _tracking.QueueRefreshTrackers(ev.Player); } private void OnPlayerDetached(PlayerDetachedEvent ev) { // This doesn't fire if the player doesn't leave their body. I guess it's fine? _tracking.QueueRefreshTrackers(ev.Player); } private void OnMobStateChanged(MobStateChangedEvent ev) { if (!TryComp(ev.Target, out ActorComponent? actor)) return; _tracking.QueueRefreshTrackers(actor.PlayerSession); } private void OnPlayerJoinedLobby(PlayerJoinedLobbyEvent ev) { _tracking.QueueRefreshTrackers(ev.PlayerSession); // Send timers to client when they join lobby, so the UIs are up-to-date. _tracking.QueueSendTimers(ev.PlayerSession); } public bool IsAllowed(IPlayerSession player, string role) { if (!_prototypes.TryIndex(role, out var job) || job.Requirements == null || !_cfg.GetCVar(CCVars.GameRoleTimers)) return true; var playTimes = _tracking.GetTrackerTimes(player); return JobRequirements.TryRequirementsMet(job, playTimes, out _, _prototypes); } public HashSet GetDisallowedJobs(IPlayerSession player) { var roles = new HashSet(); if (!_cfg.GetCVar(CCVars.GameRoleTimers)) return roles; var playTimes = _tracking.GetTrackerTimes(player); foreach (var job in _prototypes.EnumeratePrototypes()) { if (job.Requirements != null) { foreach (var requirement in job.Requirements) { if (JobRequirements.TryRequirementMet(requirement, playTimes, out _, _prototypes)) continue; goto NoRole; } } roles.Add(job.ID); NoRole:; } return roles; } public void RemoveDisallowedJobs(NetUserId userId, ref List jobs) { if (!_cfg.GetCVar(CCVars.GameRoleTimers)) return; var player = _playerManager.GetSessionByUserId(userId); var playTimes = _tracking.GetTrackerTimes(player); for (var i = 0; i < jobs.Count; i++) { var job = jobs[i]; if (!_prototypes.TryIndex(job, out var jobber) || jobber.Requirements == null || jobber.Requirements.Count == 0) continue; foreach (var requirement in jobber.Requirements) { if (JobRequirements.TryRequirementMet(requirement, playTimes, out _, _prototypes)) continue; jobs.RemoveSwap(i); i--; break; } } } public void PlayerRolesChanged(IPlayerSession player) { _tracking.QueueRefreshTrackers(player); } }