Files
RobustToolbox/Robust.Client/Graphics/LoadingScreenManager.cs
PJB3005 02b64b7386 Show task bar progress bar for loading progress
Using the new API in SDL 3.4.0
2026-01-18 23:30:15 +01:00

319 lines
9.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Client.ResourceManagement;
using Robust.Client.UserInterface;
using Robust.Shared;
using Robust.Shared.Configuration;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Stopwatch = Robust.Shared.Timing.Stopwatch;
namespace Robust.Client.Graphics;
internal interface ILoadingScreenManager
{
void BeginLoadingSection(string sectionName);
/// <summary>
/// Start a loading bar "section" for the given method.
/// Must be ended with EndSection.
/// </summary>
void BeginLoadingSection(object method);
void EndLoadingSection();
/// <summary>
/// Will run the giving function and add a custom "section" for it on the loading screen.
/// </summary>
void LoadingStep(Action action, object method);
}
/// <summary>
/// Manager that creates and displays a basic splash screen and loading bar.
/// </summary>
internal sealed class LoadingScreenManager : ILoadingScreenManager
{
[Dependency] private readonly IResourceCache _resourceCache = default!;
[Dependency] private readonly IClydeInternal _clyde = default!;
[Dependency] private readonly IConfigurationManager _cfg = default!;
[Dependency] private readonly ILogManager _logManager = default!;
private ISawmill _sawmill = default!;
private readonly Stopwatch _sw = new();
#region UI constants
private const int LoadingBarWidth = 250;
private const int LoadingBarHeight = 20;
private const int LoadingBarOutlineOffset = 5;
private static readonly Vector2i LogoLoadingBarOffset = (0, 20);
private static readonly Vector2i LoadTimesIndent = (20, 0);
private const int NumLongestLoadTimes = 5;
private static readonly Color LoadingBarColor = Color.White;
#endregion
#region Cvars
private string _splashLogo = "";
private bool _showLoadingBar;
private bool _showDebug;
#endregion
private const string FontLocation = "/EngineFonts/NotoSans/NotoSans-Regular.ttf";
private const int FontSize = 11;
private VectorFont? _font;
// Number of loading sections for the loading bar. This has to be manually set!
private int _numberOfLoadingSections;
// The name of the section and how much time it took to load
internal readonly List<(string Name, TimeSpan LoadTime)> Times = [];
private int _currentSection;
private string? _currentSectionName;
private bool _currentlyInSection;
private bool _finished;
public void Initialize(int sections)
{
if (_finished)
return;
_clyde.VsyncEnabled = false;
_numberOfLoadingSections = sections;
_sawmill = _logManager.GetSawmill("loading");
_splashLogo = _cfg.GetCVar(CVars.DisplaySplashLogo);
_showLoadingBar = _cfg.GetCVar(CVars.LoadingShowBar);
_showDebug = _cfg.GetCVar(CVars.LoadingShowDebug);
if (_resourceCache.TryGetResource<FontResource>(FontLocation, out var fontResource))
_font = new VectorFont(fontResource, FontSize);
else
_sawmill.Error($"Could not load font: {FontLocation}");
}
public void BeginLoadingSection(string sectionName) => BeginLoadingSection(sectionName, false);
public void BeginLoadingSection(string sectionName, bool dontRender)
{
if (_finished)
return;
if (_currentlyInSection)
throw new InvalidOperationException("You cannot begin more than one section at a time!");
_currentlyInSection = true;
_currentSectionName = sectionName;
if (_clyde.IsInitialized)
{
_clyde.MainWindow.SetWindowProgress(
WindowProgressState.Normal,
_currentSection / (float)_numberOfLoadingSections);
}
if (!dontRender)
{
// This ensures that if the screen was resized or something the new size is properly updated to clyde.
_clyde.ProcessInput(new FrameEventArgs((float)_sw.Elapsed.TotalSeconds));
_sw.Restart();
_clyde.Render();
}
else
{
_sw.Restart();
}
}
/// <summary>
/// Start a loading bar "section" for the given method.
/// Must be ended with EndSection.
/// </summary>
public void BeginLoadingSection(object method)
{
if (_finished)
return;
BeginLoadingSection(method.GetType().Name);
}
public void EndLoadingSection()
{
if (_finished)
return;
var time = _sw.Elapsed;
if (_currentSectionName != null)
Times.Add((_currentSectionName, time));
_currentSection++;
_currentlyInSection = false;
}
/// <summary>
/// Will run the giving function and add a custom "section" for it on the loading screen.
/// </summary>
public void LoadingStep(Action action, object method)
{
if (_finished)
return;
BeginLoadingSection(method as string ?? method.GetType().Name);
action();
EndLoadingSection();
}
public void Finish()
{
if (_finished)
return;
if (_currentSection != _numberOfLoadingSections)
_sawmill.Error($"The number of seen loading sections isn't equal to the total number of loading sections! Seen: {_currentSection}, Total: {_numberOfLoadingSections}");
_clyde.MainWindow.SetWindowProgress(WindowProgressState.None, 1);
_finished = true;
}
#region Drawing functions
/// <summary>
/// Draw out the splash and loading screen.
/// </summary>
public void DrawLoadingScreen(IRenderHandle handle, Vector2i screenSize)
{
if (_finished)
return;
var scale = UserInterfaceManager.CalculateUIScale(_clyde.MainWindow.ContentScale.X, _cfg);
// Start at the center!
var location = screenSize / 2;
DrawSplash(handle, ref location, scale);
DrawLoadingBar(handle, ref location, scale);
if (_showDebug)
{
DrawCurrentLoading(handle, ref location, scale);
DrawTopTimes(handle, ref location, scale);
}
}
private void DrawSplash(IRenderHandle handle, ref Vector2i startLocation, float scale)
{
if (string.IsNullOrEmpty(_splashLogo))
return;
if (!_resourceCache.TryGetResource<TextureResource>(_splashLogo, out var textureResource))
return;
var drawSize = textureResource.Texture.Size * scale;
handle.DrawingHandleScreen.DrawTextureRect(textureResource.Texture, UIBox2.FromDimensions(startLocation - drawSize / 2, drawSize));
startLocation += Vector2i.Up * (int) drawSize.Y / 2;
}
private void DrawLoadingBar(IRenderHandle handle, ref Vector2i location, float scale)
{
var barWidth = (int)(LoadingBarWidth * scale);
var barHeight = (int)(LoadingBarHeight * scale);
var outlineOffset = (int)(LoadingBarOutlineOffset * scale);
// Always do the offsets, it looks a lot better!
location.X -= barWidth / 2;
location += (Vector2i) (LogoLoadingBarOffset * scale);
if (!_showLoadingBar)
return;
var sectionWidth = barWidth / _numberOfLoadingSections;
var barTopLeft = location;
var barBottomRight = new Vector2i(_currentSection * sectionWidth % barWidth, barHeight);
var barBottomRightMax = new Vector2i(barWidth, barHeight);
var outlinePosition = barTopLeft + Vector2i.DownLeft * outlineOffset;
var outlineSize = barBottomRightMax + Vector2i.UpRight * 2 * outlineOffset;
// Outline
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(outlinePosition, outlineSize), LoadingBarColor, false);
// Progress bar
handle.DrawingHandleScreen.DrawRect(UIBox2.FromDimensions(barTopLeft, barBottomRight), LoadingBarColor);
location += Vector2i.Up * outlineSize;
}
// Draw the currently loading section to the screen.
private void DrawCurrentLoading(IRenderHandle handle, ref Vector2i location, float scale)
{
if (_font == null || _currentSectionName == null)
return;
handle.DrawingHandleScreen.DrawString(_font, location, _currentSectionName, scale, Color.White);
location += Vector2i.Up * _font.GetLineHeight(scale);
}
// Draw the slowest loading times to the screen.
private void DrawTopTimes(IRenderHandle handle, ref Vector2i location, float scale)
{
if (_font == null)
return;
location += (Vector2i)(LoadTimesIndent * scale);
var offset = 0;
var x = 0;
Times.Sort((a, b) => b.LoadTime.CompareTo(a.LoadTime));
foreach (var (name, time) in Times)
{
if (x >= NumLongestLoadTimes)
break;
var entry = $"{time.TotalSeconds:F2} - {name}";
handle.DrawingHandleScreen.DrawString(_font, location + new Vector2i(0, offset), entry, scale, Color.White);
offset += _font.GetLineHeight(scale);
x++;
}
location += Vector2i.Up * offset;
}
#endregion // Drawing functions
}
internal sealed class ShowTopLoadingTimesCommand : IConsoleCommand
{
[Dependency] private readonly LoadingScreenManager _mgr = default!;
public string Command => "loading_top";
public string Description => "";
public string Help => "";
public void Execute(IConsoleShell shell, string argStr, string[] args)
{
var sorted = _mgr.Times.Where(x => x.LoadTime > TimeSpan.FromSeconds(0.01)).OrderByDescending(x => x.LoadTime);
foreach (var (name, time) in sorted)
{
shell.WriteLine($"{time.TotalSeconds:F2} - {name}");
}
}
}