diff --git a/Content.Client/Entry/EntryPoint.cs b/Content.Client/Entry/EntryPoint.cs index 322b6e113c..a458e24aed 100644 --- a/Content.Client/Entry/EntryPoint.cs +++ b/Content.Client/Entry/EntryPoint.cs @@ -14,6 +14,7 @@ using Content.Client.Lobby; using Content.Client.MainMenu; using Content.Client.Parallax.Managers; using Content.Client.Players.PlayTimeTracking; +using Content.Client.Playtime; using Content.Client.Radiation.Overlays; using Content.Client.Replay; using Content.Client.Screenshot; @@ -74,6 +75,7 @@ namespace Content.Client.Entry [Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!; [Dependency] private readonly TitleWindowManager _titleWindowManager = default!; [Dependency] private readonly IEntitySystemManager _entitySystemManager = default!; + [Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!; public override void Init() { @@ -136,6 +138,7 @@ namespace Content.Client.Entry _extendedDisconnectInformation.Initialize(); _jobRequirements.Initialize(); _playbackMan.Initialize(); + _clientsidePlaytimeManager.Initialize(); //AUTOSCALING default Setup! _configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080); diff --git a/Content.Client/IoC/ClientContentIoC.cs b/Content.Client/IoC/ClientContentIoC.cs index 1ea7868e9a..ed2199bb81 100644 --- a/Content.Client/IoC/ClientContentIoC.cs +++ b/Content.Client/IoC/ClientContentIoC.cs @@ -12,6 +12,7 @@ using Content.Client.Launcher; using Content.Client.Mapping; using Content.Client.Parallax.Managers; using Content.Client.Players.PlayTimeTracking; +using Content.Client.Playtime; using Content.Client.Replay; using Content.Client.Screenshot; using Content.Client.Stylesheets; @@ -60,6 +61,7 @@ namespace Content.Client.IoC collection.Register(); collection.Register(); collection.Register(); + collection.Register(); } } } diff --git a/Content.Client/Lobby/LobbyState.cs b/Content.Client/Lobby/LobbyState.cs index 4677245509..867a7bb8a5 100644 --- a/Content.Client/Lobby/LobbyState.cs +++ b/Content.Client/Lobby/LobbyState.cs @@ -3,6 +3,7 @@ using Content.Client.GameTicking.Managers; using Content.Client.LateJoin; using Content.Client.Lobby.UI; using Content.Client.Message; +using Content.Client.Playtime; using Content.Client.UserInterface.Systems.Chat; using Content.Client.Voting; using Content.Shared.CCVar; @@ -26,6 +27,7 @@ namespace Content.Client.Lobby [Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!; [Dependency] private readonly IGameTiming _gameTiming = default!; [Dependency] private readonly IVoteManager _voteManager = default!; + [Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!; private ClientGameTicker _gameTicker = default!; private ContentAudioSystem _contentAudioSystem = default!; @@ -195,6 +197,26 @@ namespace Content.Client.Lobby { Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob); } + + var minutesToday = _playtimeTracking.PlaytimeMinutesToday; + if (minutesToday > 60) + { + Lobby!.PlaytimeComment.Visible = true; + + var hoursToday = Math.Round(minutesToday / 60f, 1); + + var chosenString = minutesToday switch + { + < 180 => "lobby-state-playtime-comment-normal", + < 360 => "lobby-state-playtime-comment-concerning", + < 720 => "lobby-state-playtime-comment-grasstouchless", + _ => "lobby-state-playtime-comment-selfdestructive" + }; + + Lobby.PlaytimeComment.SetMarkup(Loc.GetString(chosenString, ("hours", hoursToday))); + } + else + Lobby!.PlaytimeComment.Visible = false; } private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev) diff --git a/Content.Client/Lobby/UI/LobbyGui.xaml b/Content.Client/Lobby/UI/LobbyGui.xaml index 761795452e..64291816ab 100644 --- a/Content.Client/Lobby/UI/LobbyGui.xaml +++ b/Content.Client/Lobby/UI/LobbyGui.xaml @@ -41,6 +41,7 @@ StyleClasses="ButtonBig" MinWidth="137" /> + diff --git a/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs b/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs new file mode 100644 index 0000000000..330ff9122f --- /dev/null +++ b/Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs @@ -0,0 +1,108 @@ +using Content.Shared.CCVar; +using Robust.Client.Player; +using Robust.Shared.Network; +using Robust.Shared.Configuration; +using Robust.Shared.Timing; + +namespace Content.Client.Playtime; + +/// +/// Keeps track of how long the player has played today. +/// +/// +/// +/// Playtime is treated as any time in which the player is attached to an entity. +/// This notably excludes scenarios like the lobby. +/// +/// +public sealed class ClientsidePlaytimeTrackingManager +{ + [Dependency] private readonly IClientNetManager _clientNetManager = default!; + [Dependency] private readonly IConfigurationManager _configurationManager = default!; + [Dependency] private readonly ILogManager _logManager = default!; + [Dependency] private readonly IPlayerManager _playerManager = default!; + [Dependency] private readonly IGameTiming _gameTiming = default!; + + private ISawmill _sawmill = default!; + + private const string InternalDateFormat = "yyyy-MM-dd"; + + [ViewVariables] + private TimeSpan? _mobAttachmentTime; + + /// + /// The total amount of time played today, in minutes. + /// + [ViewVariables] + public float PlaytimeMinutesToday + { + get + { + var cvarValue = _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday); + if (_mobAttachmentTime == null) + return cvarValue; + + return cvarValue + (float)(_gameTiming.RealTime - _mobAttachmentTime.Value).TotalMinutes; + } + } + + public void Initialize() + { + _sawmill = _logManager.GetSawmill("clientplaytime"); + _clientNetManager.Connected += OnConnected; + + // The downside to relying on playerattached and playerdetached is that unsaved playtime won't be saved in the event of a crash + // But then again, the config doesn't get saved in the event of a crash, either, so /shrug + // Playerdetached gets called on quit, though, so at least that's covered. + _playerManager.LocalPlayerAttached += OnPlayerAttached; + _playerManager.LocalPlayerDetached += OnPlayerDetached; + } + + private void OnConnected(object? sender, NetChannelArgs args) + { + var datatimey = DateTime.Now; + _sawmill.Info($"Current day: {datatimey.Day} Current Date: {datatimey.Date.ToString(InternalDateFormat)}"); + + var recordedDateString = _configurationManager.GetCVar(CCVars.PlaytimeLastConnectDate); + var formattedDate = datatimey.Date.ToString(InternalDateFormat); + + if (formattedDate == recordedDateString) + return; + + _configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, 0); + _configurationManager.SetCVar(CCVars.PlaytimeLastConnectDate, formattedDate); + } + + private void OnPlayerAttached(EntityUid entity) + { + _mobAttachmentTime = _gameTiming.RealTime; + } + + private void OnPlayerDetached(EntityUid entity) + { + if (_mobAttachmentTime == null) + return; + + var newTimeValue = PlaytimeMinutesToday; + + _mobAttachmentTime = null; + + var timeDiffMinutes = newTimeValue - _configurationManager.GetCVar(CCVars.PlaytimeMinutesToday); + if (timeDiffMinutes < 0) + { + _sawmill.Error("Time differential on player detachment somehow less than zero!"); + return; + } + + // At less than 1 minute of time diff, there's not much point, and saving regardless will brick tests + // The reason this isn't checking for 0 is because TotalMinutes is fractional, rather than solely whole minutes + if (timeDiffMinutes < 1) + return; + + _configurationManager.SetCVar(CCVars.PlaytimeMinutesToday, newTimeValue); + + _sawmill.Info($"Recorded {timeDiffMinutes} minutes of living playtime!"); + + _configurationManager.SaveToFile(); // We don't like that we have to save the entire config just to store playtime stats '^' + } +} diff --git a/Content.Shared/CCVar/CCVars.Misc.cs b/Content.Shared/CCVar/CCVars.Misc.cs index 5fda4dc2fd..3e8a7badf9 100644 --- a/Content.Shared/CCVar/CCVars.Misc.cs +++ b/Content.Shared/CCVar/CCVars.Misc.cs @@ -94,4 +94,19 @@ public sealed partial class CCVars /// public static readonly CVarDef PointingCooldownSeconds = CVarDef.Create("pointing.cooldown_seconds", 0.5f, CVar.SERVERONLY); + + /// + /// The last time the client recorded a valid connection to a game server. + /// Used in conjunction with to track how long the player has been playing for the given day. + /// + public static readonly CVarDef PlaytimeLastConnectDate = + CVarDef.Create("playtime.last_connect_date", "", CVar.CLIENTONLY | CVar.ARCHIVE); + + /// + /// The total minutes that the client has spent since the date of last connection. + /// This is reset to 0 when the last connect date is updated. + /// Do not read this value directly, use ClientsidePlaytimeTrackingManager instead. + /// + public static readonly CVarDef PlaytimeMinutesToday = + CVarDef.Create("playtime.minutes_today", 0f, CVar.CLIENTONLY | CVar.ARCHIVE); } diff --git a/Resources/Locale/en-US/lobby/lobby-state.ftl b/Resources/Locale/en-US/lobby/lobby-state.ftl index 0c4c401daa..0c92923b1c 100644 --- a/Resources/Locale/en-US/lobby/lobby-state.ftl +++ b/Resources/Locale/en-US/lobby/lobby-state.ftl @@ -21,3 +21,11 @@ lobby-state-song-text = Playing: [color=white]{$songTitle}[/color] by [color=whi lobby-state-song-no-song-text = No lobby song playing. lobby-state-song-unknown-title = [color=dimgray]Unknown title[/color] lobby-state-song-unknown-artist = [color=dimgray]Unknown artist[/color] +lobby-state-playtime-comment-normal = + You've spent {$hours} {$hours -> + [1]hour + *[other]hours + } ingame today. Remember to take breaks! +lobby-state-playtime-comment-concerning = You've played for {$hours} hours today. Please take a break. +lobby-state-playtime-comment-grasstouchless = {$hours} hours. Consider logging off to attend to your needs. +lobby-state-playtime-comment-selfdestructive = {$hours} hours. Really?