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;
[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()
{
_extendedDisconnectInformation.Initialize();
_jobRequirements.Initialize();
_playbackMan.Initialize();
+ _clientsidePlaytimeManager.Initialize();
//AUTOSCALING default Setup!
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);
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;
collection.Register<PlayerRateLimitManager>();
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
collection.Register<TitleWindowManager>();
+ collection.Register<ClientsidePlaytimeTrackingManager>();
}
}
}
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;
[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!;
{
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)
StyleClasses="ButtonBig" MinWidth="137" />
</BoxContainer>
</controls:StripeBack>
+ <RichTextLabel Name="PlaytimeComment" Visible="False" Access="Public" HorizontalAlignment="Center" />
</BoxContainer>
</PanelContainer>
<!-- Voting Popups -->
--- /dev/null
+using Content.Shared.CCVar;
+using Robust.Client.Player;
+using Robust.Shared.Network;
+using Robust.Shared.Configuration;
+using Robust.Shared.Timing;
+
+namespace Content.Client.Playtime;
+
+/// <summary>
+/// Keeps track of how long the player has played today.
+/// </summary>
+/// <remarks>
+/// <para>
+/// Playtime is treated as any time in which the player is attached to an entity.
+/// This notably excludes scenarios like the lobby.
+/// </para>
+/// </remarks>
+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;
+
+ /// <summary>
+ /// The total amount of time played today, in minutes.
+ /// </summary>
+ [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 '^'
+ }
+}
/// </summary>
public static readonly CVarDef<float> PointingCooldownSeconds =
CVarDef.Create("pointing.cooldown_seconds", 0.5f, CVar.SERVERONLY);
+
+ /// <summary>
+ /// The last time the client recorded a valid connection to a game server.
+ /// Used in conjunction with <see cref="PlaytimeMinutesToday"/> to track how long the player has been playing for the given day.
+ /// </summary>
+ public static readonly CVarDef<string> PlaytimeLastConnectDate =
+ CVarDef.Create("playtime.last_connect_date", "", CVar.CLIENTONLY | CVar.ARCHIVE);
+
+ /// <summary>
+ /// 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 <code>ClientsidePlaytimeTrackingManager</code> instead.
+ /// </summary>
+ public static readonly CVarDef<float> PlaytimeMinutesToday =
+ CVarDef.Create("playtime.minutes_today", 0f, CVar.CLIENTONLY | CVar.ARCHIVE);
}
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?