Playtime Reminders - Raising awareness of addiction by highlighting excessive playtime (#36483)
* grass touch protocol - Rebases to latest master to fix conflicts * aight local tests are passing lets see if our golf works * It is 5 am and our ass COMPLETELY overcomplicated this lmaooo * Addresses feedback - Clarifies comments, swaps internal var names for grasstouchless and selfdestructive, makes the third tier a little less demanding, and fixes 1 hours * Addresses review - conflict fix * This too * Axes playtime exclusion for ghosts * Use switch expression code style nit * Refactor/cleanup Use IGameTiming.RealTime to track time instead of DateTime. Use nullable instead of magic values. Expose the current day value through a property that is always up to date, instead of making the API to read the CVar that updates at inconsistent times. This also makes it trivial to debug with VV. Other minor cleanup like using string interp, code style fixes, comments, etc. --------- Co-authored-by: PJB3005 <pieterjan.briers+git@gmail.com>
This commit is contained in:
@@ -14,6 +14,7 @@ using Content.Client.Lobby;
|
|||||||
using Content.Client.MainMenu;
|
using Content.Client.MainMenu;
|
||||||
using Content.Client.Parallax.Managers;
|
using Content.Client.Parallax.Managers;
|
||||||
using Content.Client.Players.PlayTimeTracking;
|
using Content.Client.Players.PlayTimeTracking;
|
||||||
|
using Content.Client.Playtime;
|
||||||
using Content.Client.Radiation.Overlays;
|
using Content.Client.Radiation.Overlays;
|
||||||
using Content.Client.Replay;
|
using Content.Client.Replay;
|
||||||
using Content.Client.Screenshot;
|
using Content.Client.Screenshot;
|
||||||
@@ -74,6 +75,7 @@ namespace Content.Client.Entry
|
|||||||
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
[Dependency] private readonly DebugMonitorManager _debugMonitorManager = default!;
|
||||||
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
[Dependency] private readonly TitleWindowManager _titleWindowManager = default!;
|
||||||
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
[Dependency] private readonly IEntitySystemManager _entitySystemManager = default!;
|
||||||
|
[Dependency] private readonly ClientsidePlaytimeTrackingManager _clientsidePlaytimeManager = default!;
|
||||||
|
|
||||||
public override void Init()
|
public override void Init()
|
||||||
{
|
{
|
||||||
@@ -136,6 +138,7 @@ namespace Content.Client.Entry
|
|||||||
_extendedDisconnectInformation.Initialize();
|
_extendedDisconnectInformation.Initialize();
|
||||||
_jobRequirements.Initialize();
|
_jobRequirements.Initialize();
|
||||||
_playbackMan.Initialize();
|
_playbackMan.Initialize();
|
||||||
|
_clientsidePlaytimeManager.Initialize();
|
||||||
|
|
||||||
//AUTOSCALING default Setup!
|
//AUTOSCALING default Setup!
|
||||||
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);
|
_configManager.SetCVar("interface.resolutionAutoScaleUpperCutoffX", 1080);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ using Content.Client.Launcher;
|
|||||||
using Content.Client.Mapping;
|
using Content.Client.Mapping;
|
||||||
using Content.Client.Parallax.Managers;
|
using Content.Client.Parallax.Managers;
|
||||||
using Content.Client.Players.PlayTimeTracking;
|
using Content.Client.Players.PlayTimeTracking;
|
||||||
|
using Content.Client.Playtime;
|
||||||
using Content.Client.Replay;
|
using Content.Client.Replay;
|
||||||
using Content.Client.Screenshot;
|
using Content.Client.Screenshot;
|
||||||
using Content.Client.Stylesheets;
|
using Content.Client.Stylesheets;
|
||||||
@@ -60,6 +61,7 @@ namespace Content.Client.IoC
|
|||||||
collection.Register<PlayerRateLimitManager>();
|
collection.Register<PlayerRateLimitManager>();
|
||||||
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
collection.Register<SharedPlayerRateLimitManager, PlayerRateLimitManager>();
|
||||||
collection.Register<TitleWindowManager>();
|
collection.Register<TitleWindowManager>();
|
||||||
|
collection.Register<ClientsidePlaytimeTrackingManager>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using Content.Client.GameTicking.Managers;
|
|||||||
using Content.Client.LateJoin;
|
using Content.Client.LateJoin;
|
||||||
using Content.Client.Lobby.UI;
|
using Content.Client.Lobby.UI;
|
||||||
using Content.Client.Message;
|
using Content.Client.Message;
|
||||||
|
using Content.Client.Playtime;
|
||||||
using Content.Client.UserInterface.Systems.Chat;
|
using Content.Client.UserInterface.Systems.Chat;
|
||||||
using Content.Client.Voting;
|
using Content.Client.Voting;
|
||||||
using Content.Shared.CCVar;
|
using Content.Shared.CCVar;
|
||||||
@@ -26,6 +27,7 @@ namespace Content.Client.Lobby
|
|||||||
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
[Dependency] private readonly IUserInterfaceManager _userInterfaceManager = default!;
|
||||||
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
[Dependency] private readonly IGameTiming _gameTiming = default!;
|
||||||
[Dependency] private readonly IVoteManager _voteManager = default!;
|
[Dependency] private readonly IVoteManager _voteManager = default!;
|
||||||
|
[Dependency] private readonly ClientsidePlaytimeTrackingManager _playtimeTracking = default!;
|
||||||
|
|
||||||
private ClientGameTicker _gameTicker = default!;
|
private ClientGameTicker _gameTicker = default!;
|
||||||
private ContentAudioSystem _contentAudioSystem = default!;
|
private ContentAudioSystem _contentAudioSystem = default!;
|
||||||
@@ -195,6 +197,26 @@ namespace Content.Client.Lobby
|
|||||||
{
|
{
|
||||||
Lobby!.ServerInfo.SetInfoBlob(_gameTicker.ServerInfoBlob);
|
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)
|
private void UpdateLobbySoundtrackInfo(LobbySoundtrackChangedEvent ev)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
StyleClasses="ButtonBig" MinWidth="137" />
|
StyleClasses="ButtonBig" MinWidth="137" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</controls:StripeBack>
|
</controls:StripeBack>
|
||||||
|
<RichTextLabel Name="PlaytimeComment" Visible="False" Access="Public" HorizontalAlignment="Center" />
|
||||||
</BoxContainer>
|
</BoxContainer>
|
||||||
</PanelContainer>
|
</PanelContainer>
|
||||||
<!-- Voting Popups -->
|
<!-- Voting Popups -->
|
||||||
|
|||||||
108
Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs
Normal file
108
Content.Client/Playtime/ClientsidePlaytimeTrackingManager.cs
Normal file
@@ -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;
|
||||||
|
|
||||||
|
/// <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 '^'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -94,4 +94,19 @@ public sealed partial class CCVars
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly CVarDef<float> PointingCooldownSeconds =
|
public static readonly CVarDef<float> PointingCooldownSeconds =
|
||||||
CVarDef.Create("pointing.cooldown_seconds", 0.5f, CVar.SERVERONLY);
|
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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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-no-song-text = No lobby song playing.
|
||||||
lobby-state-song-unknown-title = [color=dimgray]Unknown title[/color]
|
lobby-state-song-unknown-title = [color=dimgray]Unknown title[/color]
|
||||||
lobby-state-song-unknown-artist = [color=dimgray]Unknown artist[/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?
|
||||||
|
|||||||
Reference in New Issue
Block a user