--- /dev/null
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Info.PlaytimeStats;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlaytimeStatsEntry : ContainerButton
+{
+ public TimeSpan Playtime { get; private set; } // new TimeSpan property
+
+ public PlaytimeStatsEntry(string role, TimeSpan playtime, StyleBox styleBox)
+ {
+ RobustXamlLoader.Load(this);
+
+ RoleLabel.Text = role;
+ Playtime = playtime; // store the TimeSpan value directly
+ PlaytimeLabel.Text = ConvertTimeSpanToHoursMinutes(playtime); // convert to string for display
+ BackgroundColorPanel.PanelOverride = styleBox;
+ }
+
+ private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
+ {
+ var hours = (int)timeSpan.TotalHours;
+ var minutes = timeSpan.Minutes;
+
+ var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
+ return formattedTimeLoc;
+ }
+
+ public void UpdateShading(StyleBoxFlat styleBox)
+ {
+ BackgroundColorPanel.PanelOverride = styleBox;
+ }
+ public string? PlaytimeText => PlaytimeLabel.Text;
+
+ public string? RoleText => RoleLabel.Text;
+}
--- /dev/null
+<ContainerButton xmlns="https://spacestation14.io"
+ xmlns:customControls1="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ EnableAllKeybinds="True">
+ <PanelContainer Name="BackgroundColorPanel"/>
+ <BoxContainer Orientation="Horizontal"
+ HorizontalExpand="True"
+ SeparationOverride="4">
+ <Label Name="RoleLabel"
+ SizeFlagsStretchRatio="3"
+ HorizontalExpand="True"
+ ClipText="True"
+ Margin="5,5,5,5"/>
+ <customControls1:VSeparator/>
+ <Label Name="PlaytimeLabel"
+ SizeFlagsStretchRatio="3"
+ HorizontalExpand="True"
+ ClipText="True"
+ Margin="5,5,5,5"/>
+ </BoxContainer>
+</ContainerButton>
--- /dev/null
+using Robust.Client.AutoGenerated;
+using Robust.Client.UserInterface;
+using Robust.Client.UserInterface.Controls;
+using Robust.Client.UserInterface.XAML;
+using Robust.Shared.Input;
+
+namespace Content.Client.Info.PlaytimeStats;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlaytimeStatsHeader : ContainerButton
+{
+ public event Action<Header, SortDirection>? OnHeaderClicked;
+ private SortDirection _roleDirection = SortDirection.Ascending;
+ private SortDirection _playtimeDirection = SortDirection.Descending;
+
+ public PlaytimeStatsHeader()
+ {
+ RobustXamlLoader.Load(this);
+
+ RoleLabel.OnKeyBindDown += RoleClicked;
+ PlaytimeLabel.OnKeyBindDown += PlaytimeClicked;
+
+ UpdateLabels();
+ }
+
+ public enum Header : byte
+ {
+ Role,
+ Playtime
+ }
+ public enum SortDirection : byte
+ {
+ Ascending,
+ Descending
+ }
+
+ private void HeaderClicked(GUIBoundKeyEventArgs args, Header header)
+ {
+ if (args.Function != EngineKeyFunctions.UIClick)
+ {
+ return;
+ }
+
+ switch (header)
+ {
+ case Header.Role:
+ _roleDirection = _roleDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending;
+ break;
+ case Header.Playtime:
+ _playtimeDirection = _playtimeDirection == SortDirection.Ascending ? SortDirection.Descending : SortDirection.Ascending;
+ break;
+ }
+
+ UpdateLabels();
+ OnHeaderClicked?.Invoke(header, header == Header.Role ? _roleDirection : _playtimeDirection);
+ args.Handle();
+ }
+ private void UpdateLabels()
+ {
+ RoleLabel.Text = Loc.GetString("ui-playtime-header-role-type") +
+ (_roleDirection == SortDirection.Ascending ? " ↓" : " ↑");
+ PlaytimeLabel.Text = Loc.GetString("ui-playtime-header-role-time") +
+ (_playtimeDirection == SortDirection.Ascending ? " ↓" : " ↑");
+ }
+
+ private void RoleClicked(GUIBoundKeyEventArgs args)
+ {
+ HeaderClicked(args, Header.Role);
+ }
+
+ private void PlaytimeClicked(GUIBoundKeyEventArgs args)
+ {
+ HeaderClicked(args, Header.Playtime);
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ if (disposing)
+ {
+ RoleLabel.OnKeyBindDown -= RoleClicked;
+ PlaytimeLabel.OnKeyBindDown -= PlaytimeClicked;
+ }
+ }
+}
--- /dev/null
+<ContainerButton xmlns="https://spacestation14.io"
+ xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ EnableAllKeybinds="True">
+ <PanelContainer Name="BackgroundColorPlaytimePanel" Access="Public"/>
+ <BoxContainer Orientation="Vertical"
+ HorizontalExpand="True">
+ <BoxContainer Orientation="Horizontal"
+ HorizontalExpand="True"
+ SeparationOverride="4">
+ <Label Name="RoleLabel"
+ SizeFlagsStretchRatio="3"
+ HorizontalExpand="True"
+ ClipText="True"
+ Text="{Loc ui-playtime-header-role-type}"
+ MouseFilter="Pass"
+ Margin="5,5,5,5"/>
+ <customControls:VSeparator/>
+ <Label Name="PlaytimeLabel"
+ SizeFlagsStretchRatio="3"
+ HorizontalExpand="True"
+ ClipText="True"
+ Text="{Loc ui-playtime-header-role-time}"
+ MouseFilter="Pass"
+ Margin="5,5,5,5"/>
+ </BoxContainer>
+ <!-- Horizontal Separator -->
+ <customControls:HSeparator/>
+ </BoxContainer>
+</ContainerButton>
--- /dev/null
+using System.Linq;
+using System.Text.RegularExpressions;
+using Content.Client.Players.PlayTimeTracking;
+using Content.Client.UserInterface.Controls;
+using Robust.Client.AutoGenerated;
+using Robust.Client.Graphics;
+using Robust.Client.UserInterface.XAML;
+
+namespace Content.Client.Info.PlaytimeStats;
+
+[GenerateTypedNameReferences]
+public sealed partial class PlaytimeStatsWindow : FancyWindow
+{
+ [Dependency] private readonly JobRequirementsManager _jobRequirementsManager = default!;
+ private ISawmill _sawmill = Logger.GetSawmill("PlaytimeStatsWindow");
+ private readonly Color _altColor = Color.FromHex("#292B38");
+ private readonly Color _defaultColor = Color.FromHex("#2F2F3B");
+ private bool _useAltColor;
+
+ public PlaytimeStatsWindow()
+ {
+ IoCManager.InjectDependencies(this);
+ RobustXamlLoader.Load(this);
+
+ PopulatePlaytimeHeader();
+ PopulatePlaytimeData();
+ }
+
+ private void PopulatePlaytimeHeader()
+ {
+ var header = new PlaytimeStatsHeader();
+ header.OnHeaderClicked += HeaderClicked;
+ header.BackgroundColorPlaytimePanel.PanelOverride = new StyleBoxFlat(_altColor);
+ RolesPlaytimeList.AddChild(header);
+ }
+
+ private void HeaderClicked(PlaytimeStatsHeader.Header header, PlaytimeStatsHeader.SortDirection direction)
+ {
+ switch (header)
+ {
+ case PlaytimeStatsHeader.Header.Role:
+ SortByRole(direction);
+ break;
+ case PlaytimeStatsHeader.Header.Playtime:
+ SortByPlaytime(direction);
+ break;
+ }
+ }
+
+ private void SortByRole(PlaytimeStatsHeader.SortDirection direction)
+ {
+ var header = RolesPlaytimeList.GetChild(0) as PlaytimeStatsHeader;
+
+ var entries = RolesPlaytimeList.Children.OfType<PlaytimeStatsEntry>().ToList();
+
+ RolesPlaytimeList.RemoveAllChildren();
+
+ if (header != null)
+ RolesPlaytimeList.AddChild(header);
+
+ var sortedEntries = (direction == PlaytimeStatsHeader.SortDirection.Ascending)
+ ? entries.OrderBy(entry => entry.RoleText).ToList()
+ : entries.OrderByDescending(entry => entry.RoleText).ToList();
+
+ _useAltColor = false;
+
+ foreach (var entry in sortedEntries)
+ {
+ var styleBox = new StyleBoxFlat { BackgroundColor = _useAltColor ? _altColor : _defaultColor };
+ entry.UpdateShading(styleBox);
+ RolesPlaytimeList.AddChild(entry);
+ _useAltColor ^= true;
+ }
+ }
+
+ private void SortByPlaytime(PlaytimeStatsHeader.SortDirection direction)
+ {
+ var header = RolesPlaytimeList.GetChild(0) as PlaytimeStatsHeader;
+
+ var entries = RolesPlaytimeList.Children.OfType<PlaytimeStatsEntry>().ToList();
+
+ RolesPlaytimeList.RemoveAllChildren();
+
+ if (header != null)
+ RolesPlaytimeList.AddChild(header);
+
+ var sortedEntries = (direction == PlaytimeStatsHeader.SortDirection.Ascending)
+ ? entries.OrderBy(entry => entry.Playtime).ToList()
+ : entries.OrderByDescending(entry => entry.Playtime).ToList();
+
+ _useAltColor = false;
+
+ foreach (var entry in sortedEntries)
+ {
+ var styleBox = new StyleBoxFlat { BackgroundColor = _useAltColor ? _altColor : _defaultColor };
+ entry.UpdateShading(styleBox);
+ RolesPlaytimeList.AddChild(entry);
+ _useAltColor ^= true;
+ }
+ }
+
+
+ private void PopulatePlaytimeData()
+ {
+ var overallPlaytime = _jobRequirementsManager.FetchOverallPlaytime();
+
+ var formattedPlaytime = ConvertTimeSpanToHoursMinutes(overallPlaytime);
+ OverallPlaytimeLabel.Text = Loc.GetString("ui-playtime-overall", ("time", formattedPlaytime));
+
+ var rolePlaytimes = _jobRequirementsManager.FetchPlaytimeByRoles();
+
+ RolesPlaytimeList.RemoveAllChildren();
+ PopulatePlaytimeHeader();
+
+ foreach (var rolePlaytime in rolePlaytimes)
+ {
+ var role = rolePlaytime.Key;
+ var playtime = rolePlaytime.Value;
+ AddRolePlaytimeEntryToTable(Loc.GetString(role), playtime.ToString());
+ }
+ }
+
+ private void AddRolePlaytimeEntryToTable(string role, string playtimeString)
+ {
+ if (TimeSpan.TryParse(playtimeString, out var playtime))
+ {
+ var entry = new PlaytimeStatsEntry(role, playtime,
+ new StyleBoxFlat(_useAltColor ? _altColor : _defaultColor));
+ RolesPlaytimeList.AddChild(entry);
+ _useAltColor ^= true;
+ }
+ else
+ {
+ _sawmill.Error($"The provided playtime string '{playtimeString}' is not in the correct format.");
+ }
+ }
+
+ private static string ConvertTimeSpanToHoursMinutes(TimeSpan timeSpan)
+ {
+ var hours = (int) timeSpan.TotalHours;
+ var minutes = timeSpan.Minutes;
+
+ var formattedTimeLoc = Loc.GetString("ui-playtime-time-format", ("hours", hours), ("minutes", minutes));
+ return formattedTimeLoc;
+ }
+}
--- /dev/null
+<ui:FancyWindow xmlns="https://spacestation14.io"
+ xmlns:ui="clr-namespace:Content.Client.UserInterface.Controls"
+ xmlns:pt="clr-namespace:Content.Client.Info.PlaytimeStats"
+ xmlns:customControls="clr-namespace:Content.Client.Administration.UI.CustomControls"
+ VerticalExpand="True" HorizontalExpand="True"
+ Title="{Loc ui-playtime-stats-title}"
+ SetSize="600 400">
+ <Control>
+ <BoxContainer Name="statsBox" Orientation="Vertical" Margin="10,10,10,10">
+
+ <!-- Overall Playtime -->
+ <Label Name="OverallPlaytimeLabel" HorizontalExpand="True" Text="{Loc ui-playtime-overall-base}" />
+ <Control MinSize="0 5" />
+
+ <!-- Table for roles -->
+ <ScrollContainer HorizontalExpand="True" VerticalExpand="True">
+ <BoxContainer Orientation="Vertical" Name="RolesPlaytimeList">
+ <!-- Table Header -->
+ <pt:PlaytimeStatsHeader Name="ListHeader" />
+ <customControls:HSeparator />
+ </BoxContainer>
+ </ScrollContainer>
+ </BoxContainer>
+ </Control>
+</ui:FancyWindow>
using System.Diagnostics.CodeAnalysis;
-using System.Linq;
-using System.Text;
using Content.Shared.CCVar;
using Content.Shared.Players;
using Content.Shared.Players.PlayTimeTracking;
reason = reasons.Count == 0 ? null : FormattedMessage.FromMarkup(string.Join('\n', reasons));
return reason == null;
}
+
+ public TimeSpan FetchOverallPlaytime()
+ {
+ return _roles.TryGetValue("Overall", out var overallPlaytime) ? overallPlaytime : TimeSpan.Zero;
+ }
+
+ public IEnumerable<KeyValuePair<string, TimeSpan>> FetchPlaytimeByRoles()
+ {
+ var jobsToMap = _prototypes.EnumeratePrototypes<JobPrototype>();
+
+ foreach (var job in jobsToMap)
+ {
+ if (_roles.TryGetValue(job.PlayTimeTracker, out var locJobName))
+ {
+ yield return new KeyValuePair<string, TimeSpan>(job.Name, locJobName);
+ }
+ }
+ }
+
+
}
<Label Text="{Loc 'character-setup-gui-character-setup-label'}"
Margin="8 0 0 0" VAlign="Center"
StyleClasses="LabelHeadingBigger" />
- <Button Name="RulesButton" HorizontalExpand="True"
- Text="{Loc 'character-setup-gui-character-setup-rules-button'}"
+ <Button Name="StatsButton" HorizontalExpand="True"
+ Text="{Loc 'character-setup-gui-character-setup-stats-button'}"
StyleClasses="ButtonBig"
HorizontalAlignment="Right" />
+ <Button Name="RulesButton"
+ Text="{Loc 'character-setup-gui-character-setup-rules-button'}"
+ StyleClasses="ButtonBig"/>
<Button Name="SaveButton"
Access="Public"
Text="{Loc 'character-setup-gui-character-setup-save-button'}"
using System.Numerics;
using Content.Client.Humanoid;
using Content.Client.Info;
+using Content.Client.Info.PlaytimeStats;
using Content.Client.Lobby.UI;
using Content.Client.Resources;
using Content.Client.Stylesheets;
UpdateUI();
RulesButton.OnPressed += _ => new RulesAndInfoWindow().Open();
+
+ StatsButton.OnPressed += _ => new PlaytimeStatsWindow().OpenCentered();
preferencesManager.OnServerDataLoaded += UpdateUI;
}
--- /dev/null
+# Playtime Stats
+
+ui-playtime-stats-title = User Playtime Stats
+ui-playtime-overall-base = Overall Playtime:
+ui-playtime-overall = Overall Playtime: {$time}
+ui-playtime-first-time = First Time Playing
+ui-playtime-roles = Playtime per Role
+ui-playtime-time-format = {$hours}H {$minutes}M
+ui-playtime-header-role-type = Role
+ui-playtime-header-role-time = Time
character-setup-gui-character-setup-label = Character setup
+character-setup-gui-character-setup-stats-button = Stats
character-setup-gui-character-setup-rules-button = Rules
character-setup-gui-character-setup-save-button = Save
character-setup-gui-character-setup-close-button = Close