From 0c6bbaadac3fe7597ea55c758c02bdd9348e3935 Mon Sep 17 00:00:00 2001 From: Cinkafox <70429757+Cinkafox@users.noreply.github.com> Date: Sat, 6 Dec 2025 23:25:25 +0300 Subject: [PATCH] - tweak: loading popup thinks * - tweak: change loading handle logic * - tweak: beautify loading thinks * - fix: speed thinks while downloading --- Nebula.Launcher/Assets/lang/en-US.ftl | 1 + Nebula.Launcher/Assets/lang/ru-RU.ftl | 3 +- Nebula.Launcher/Controls/LocalizedLabel.cs | 2 +- Nebula.Launcher/Controls/SimpleGraph.cs | 157 ++++++++++++++++++ .../ProcessHelper/GameRunnerPreparer.cs | 24 ++- Nebula.Launcher/Services/DecompilerService.cs | 19 +-- ...ationService.cs => LocalizationService.cs} | 4 +- Nebula.Launcher/ViewModels/MainViewModel.cs | 16 +- .../ViewModels/Pages/AccountInfoViewModel.cs | 53 +++--- .../Pages/ConfigurationViewModel.cs | 2 +- .../Pages/ContentBrowserViewModel.cs | 29 ++-- .../ViewModels/Pages/ServerOverviewModel.cs | 2 +- .../ViewModels/Popup/AddFavoriteViewModel.cs | 4 +- .../Popup/EditServerNameViewModel.cs | 2 +- .../Popup/ExceptionListViewModel.cs | 2 +- .../ViewModels/Popup/InfoPopupViewModel.cs | 2 +- .../Popup/IsLoginCredentialsNullPopup.cs | 2 +- .../Popup/LoadingContextViewModel.cs | 153 ++++++++++------- .../ViewModels/Popup/TfaViewModel.cs | 2 +- .../ViewModels/ServerEntryModelView.cs | 9 +- Nebula.Launcher/Views/MainWindow.axaml | 4 +- .../Views/Pages/AccountInfoView.axaml | 4 +- .../Views/Popup/LoadingContextView.axaml | 78 ++++++--- Nebula.Runner/App.cs | 2 +- Nebula.Runner/Services/RunnerService.cs | 54 +----- Nebula.Shared/FileApis/FileApi.cs | 11 +- Nebula.Shared/FileApis/HashApi.cs | 4 +- .../FileApis/Interfaces/IWriteFileApi.cs | 6 +- Nebula.Shared/Models/ILoadingHandler.cs | 65 +++++++- .../Services/ConfigurationService.cs | 9 +- .../Services/ContentService.Download.cs | 152 +++++++++-------- .../Services/ContentService.Migration.cs | 15 +- Nebula.Shared/Services/EngineService.cs | 37 +++-- Nebula.Shared/Services/FileService.cs | 29 +++- Nebula.Shared/Services/RestService.cs | 5 +- Nebula.Shared/Utils/BandwidthStream.cs | 138 --------------- Nebula.Shared/Utils/ContentManifestParser.cs | 43 +++++ Nebula.Shared/Utils/StreamHelper.cs | 49 +++--- Nebula.sln.DotSettings.user | 8 + 39 files changed, 710 insertions(+), 491 deletions(-) create mode 100644 Nebula.Launcher/Controls/SimpleGraph.cs rename Nebula.Launcher/Services/{LocalisationService.cs => LocalizationService.cs} (95%) delete mode 100644 Nebula.Shared/Utils/BandwidthStream.cs create mode 100644 Nebula.Shared/Utils/ContentManifestParser.cs diff --git a/Nebula.Launcher/Assets/lang/en-US.ftl b/Nebula.Launcher/Assets/lang/en-US.ftl index 0fff2f6..f12fa75 100644 --- a/Nebula.Launcher/Assets/lang/en-US.ftl +++ b/Nebula.Launcher/Assets/lang/en-US.ftl @@ -70,3 +70,4 @@ popup-login-credentials-warning-cancel = Cancel popup-login-credentials-warning-proceed = Proceed goto-path-home = Root folder +tab-favorite = Favorite \ No newline at end of file diff --git a/Nebula.Launcher/Assets/lang/ru-RU.ftl b/Nebula.Launcher/Assets/lang/ru-RU.ftl index fa08c58..04baa10 100644 --- a/Nebula.Launcher/Assets/lang/ru-RU.ftl +++ b/Nebula.Launcher/Assets/lang/ru-RU.ftl @@ -69,4 +69,5 @@ popup-login-credentials-warning-go-auth = Перейти на страницу popup-login-credentials-warning-cancel = Отмена popup-login-credentials-warning-proceed = Продолжить -goto-path-home = Корн. папка \ No newline at end of file +goto-path-home = Корн. папка +tab-favorite = Избранное \ No newline at end of file diff --git a/Nebula.Launcher/Controls/LocalizedLabel.cs b/Nebula.Launcher/Controls/LocalizedLabel.cs index 4cdc949..22e3615 100644 --- a/Nebula.Launcher/Controls/LocalizedLabel.cs +++ b/Nebula.Launcher/Controls/LocalizedLabel.cs @@ -14,7 +14,7 @@ public class LocalizedLabel : Label set { SetValue(LocalIdProperty, value); - Content = LocalisationService.GetString(value); + Content = LocalizationService.GetString(value); } } } \ No newline at end of file diff --git a/Nebula.Launcher/Controls/SimpleGraph.cs b/Nebula.Launcher/Controls/SimpleGraph.cs new file mode 100644 index 0000000..4e0e327 --- /dev/null +++ b/Nebula.Launcher/Controls/SimpleGraph.cs @@ -0,0 +1,157 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.Reactive; +using Avalonia.Threading; + +namespace Nebula.Launcher.Controls; + +public class SimpleGraph : Control +{ + // Bindable data: list of doubles or points + public static readonly StyledProperty> ValuesProperty = + AvaloniaProperty.Register>(nameof(Values)); + + public static readonly StyledProperty GraphBrushProperty = + AvaloniaProperty.Register(nameof(GraphBrush), Brushes.CornflowerBlue); + + public static readonly StyledProperty GridBrushProperty = + AvaloniaProperty.Register(nameof(GridBrush), Brushes.LightGray); + + static SimpleGraph() + { + ValuesProperty.Changed.Subscribe( + new AnonymousObserver>>(args => + { + if (args.Sender is not SimpleGraph g) + return; + + g.InvalidateVisual(); + g.Values.CollectionChanged += g.ValuesOnCollectionChanged; + })); + } + + public SimpleGraph() + { + Values = new ObservableCollection(); + } + + private void ValuesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) + { + Dispatcher.UIThread.Post(InvalidateVisual); + } + + public ObservableCollection Values + { + get => GetValue(ValuesProperty); + set => SetValue(ValuesProperty, value); + } + + + public IBrush GraphBrush + { + get => GetValue(GraphBrushProperty); + set => SetValue(GraphBrushProperty, value); + } + + public IBrush GridBrush + { + get => GetValue(GridBrushProperty); + set => SetValue(GridBrushProperty, value); + } + + public override void Render(DrawingContext context) + { + base.Render(context); + + + if (Bounds.Width <= 0 || Bounds.Height <= 0) + return; + + // background grid + DrawGrid(context, Bounds); + + if (Values.Count == 0) + return; + + + var min = Values.Min(); + var max = Values.Max(); + if (Math.Abs(min - max) < 0.001) + { + min -= 1; + max += 1; + } + + + var geo = new StreamGeometry(); + using (var ctx = geo.Open()) + { + if (Values.Count > 1) + { + Point p0 = Map(0, Values[0]); + ctx.BeginFigure(p0, false); + + + for (int i = 0; i < Values.Count - 1; i++) + { + var p1 = Map(i, Values[i]); + var p2 = Map(i + 1, Values[i + 1]); + + + // control points for smoothing + var c1 = new Point((p1.X + p2.X) / 2, p1.Y); + var c2 = new Point((p1.X + p2.X) / 2, p2.Y); + + + ctx.CubicBezierTo(c1, c2, p2); + } + ctx.EndFigure(false); + } + } + + + // stroke + context.DrawGeometry(null, new Pen(GraphBrush, 2), geo); + + // draw points + for (var i = 0; i < Values.Count; i++) + { + var p = Map(i, Values[i]); + context.DrawEllipse(GraphBrush, null, p, 3, 3); + } + + return; + + // map data index/value -> point + Point Map(int i, double val) + { + var x = Bounds.X + Bounds.Width * (i / (double)Math.Max(1, Values.Count - 1)); + var y = Bounds.Y + Bounds.Height - (val - min) / (max - min) * Bounds.Height; + return new Point(x, y); + } + } + + private void DrawGrid(DrawingContext dc, Rect r) + { + var pen = new Pen(GridBrush, 0.5); + var rows = 4; + var cols = Math.Max(2, Values?.Count ?? 2); + for (var i = 0; i <= rows; i++) + { + var y = r.Y + i * (r.Height / rows); + dc.DrawLine(pen, new Point(r.X, y), new Point(r.Right, y)); + } + + for (var j = 0; j <= cols; j++) + { + var x = r.X + j * (r.Width / cols); + dc.DrawLine(pen, new Point(x, r.Y), new Point(x, r.Bottom)); + } + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs b/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs index e3dd663..d522859 100644 --- a/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs +++ b/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs @@ -5,23 +5,39 @@ using Microsoft.Extensions.DependencyInjection; using Nebula.Shared; using Nebula.Shared.Models; using Nebula.Shared.Services; +using Nebula.Shared.Utils; +using Robust.LoaderApi; namespace Nebula.Launcher.ProcessHelper; [ServiceRegister] public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService contentService, EngineService engineService) { - public async Task> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandler loadingHandler, CancellationToken cancellationToken = default) + public async Task> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default) { var buildInfo = await contentService.GetBuildInfo(address, cancellationToken); - var engine = await engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion); + var engine = await engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandlerFactory, cancellationToken); if (engine is null) throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion); - await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken); - await engineService.EnsureEngineModules("Robust.Client.WebView", buildInfo.BuildInfo.Build.EngineVersion); + var hashApi = await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandlerFactory, cancellationToken); + + + if (hashApi.TryOpen("manifest.yml", out var stream)) + { + var modules = ContentManifestParser.ExtractModules(stream); + + foreach (var moduleStr in modules) + { + var module = await engineService.EnsureEngineModules(moduleStr, loadingHandlerFactory, buildInfo.BuildInfo.Build.EngineVersion); + if(module is null) + throw new Exception("Module not found: " + moduleStr); + } + + await stream.DisposeAsync(); + } var gameInfo = provider.GetService()!.WithBuildInfo(buildInfo.BuildInfo.Auth.PublicKey, diff --git a/Nebula.Launcher/Services/DecompilerService.cs b/Nebula.Launcher/Services/DecompilerService.cs index c6ea2d2..ed24c0b 100644 --- a/Nebula.Launcher/Services/DecompilerService.cs +++ b/Nebula.Launcher/Services/DecompilerService.cs @@ -6,6 +6,7 @@ using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Nebula.Launcher.ViewModels.Popup; using Nebula.Shared; @@ -25,7 +26,6 @@ public sealed partial class DecompilerService [GenerateProperty] private ViewHelperService ViewHelperService {get;} [GenerateProperty] private ContentService ContentService {get;} [GenerateProperty] private FileService FileService {get;} - [GenerateProperty] private CancellationService CancellationService {get;} [GenerateProperty] private EngineService EngineService {get;} [GenerateProperty] private DebugService DebugService {get;} @@ -44,16 +44,14 @@ public sealed partial class DecompilerService Process.Start(startInfo); } - public async void OpenServerDecompiler(RobustUrl url) + public async void OpenServerDecompiler(RobustUrl url, CancellationToken cancellationToken) { var myTempDir = FileService.EnsureTempDir(out var tmpDir); - ILoadingHandler loadingHandler = ViewHelperService.GetViewModel(); - + using var loadingHandler = ViewHelperService.GetViewModel(); var buildInfo = - await ContentService.GetBuildInfo(url, CancellationService.Token); - var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion); - + await ContentService.GetBuildInfo(url, cancellationToken); + var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandler, cancellationToken); if (engine is null) throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion); @@ -64,8 +62,9 @@ public sealed partial class DecompilerService await stream.DisposeAsync(); } - var hashApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, CancellationService.Token); + var hashApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken); + foreach (var (file, hash) in hashApi.Manifest) { if(!file.Contains(".dll") || !hashApi.TryOpen(hash, out var stream)) continue; @@ -73,8 +72,6 @@ public sealed partial class DecompilerService await stream.DisposeAsync(); } - ((IDisposable)loadingHandler).Dispose(); - _logger.Log("File extracted. " + tmpDir); OpenDecompiler(string.Join(' ', myTempDir.AllFiles.Select(f=>Path.Join(tmpDir, f))) + " --newinstance"); @@ -94,7 +91,7 @@ public sealed partial class DecompilerService private async Task Download(){ using var loading = ViewHelperService.GetViewModel(); loading.LoadingName = "Download ILSpy"; - loading.SetJobsCount(1); + loading.CreateLoadingContext().SetJobsCount(1); PopupMessageService.Popup(loading); using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl)); using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync()); diff --git a/Nebula.Launcher/Services/LocalisationService.cs b/Nebula.Launcher/Services/LocalizationService.cs similarity index 95% rename from Nebula.Launcher/Services/LocalisationService.cs rename to Nebula.Launcher/Services/LocalizationService.cs index be16444..d3fabaa 100644 --- a/Nebula.Launcher/Services/LocalisationService.cs +++ b/Nebula.Launcher/Services/LocalizationService.cs @@ -11,7 +11,7 @@ using Nebula.Shared.Services; namespace Nebula.Launcher.Services; [ConstructGenerator, ServiceRegister] -public partial class LocalisationService +public partial class LocalizationService { [GenerateProperty] private ConfigurationService ConfigurationService { get; } [GenerateProperty] private DebugService DebugService { get; } @@ -74,6 +74,6 @@ public class LocaledText : MarkupExtension public override object ProvideValue(IServiceProvider serviceProvider) { - return LocalisationService.GetString(Key, Options); + return LocalizationService.GetString(Key, Options); } } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index 3e20760..336a003 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -41,9 +41,9 @@ public partial class MainViewModel : ViewModelBase [ObservableProperty] private bool _isPopupClosable = true; [ObservableProperty] private bool _popup; [ObservableProperty] private ListItemTemplate? _selectedListItem; - [ObservableProperty] private string? _loginText = LocalisationService.GetString("auth-current-login-no-name"); + [ObservableProperty] private string? _loginText = LocalizationService.GetString("auth-current-login-no-name"); - [GenerateProperty] private LocalisationService LocalisationService { get; } // Не убирать! Без этой хуйни вся локализация идет в пизду! + [GenerateProperty] private LocalizationService LocalizationService { get; } // Не убирать! Без этой хуйни вся локализация идет в пизду! [GenerateProperty] private AccountInfoViewModel AccountInfoViewModel { get; } [GenerateProperty] private DebugService DebugService { get; } = default!; [GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!; @@ -59,7 +59,7 @@ public partial class MainViewModel : ViewModelBase { Items = new ObservableCollection(_templates.Select(a=> { - return a with { Label = LocalisationService.GetString(a.Label) }; + return a with { Label = LocalizationService.GetString(a.Label) }; } )); RequirePage(); @@ -92,13 +92,13 @@ public partial class MainViewModel : ViewModelBase CheckMigration(); var loadingHandler = ViewHelperService.GetViewModel(); - loadingHandler.LoadingName = LocalisationService.GetString("migration-config-task"); + loadingHandler.LoadingName = LocalizationService.GetString("migration-config-task"); loadingHandler.IsCancellable = false; ConfigurationService.MigrateConfigs(loadingHandler); if (!VCRuntimeDllChecker.AreVCRuntimeDllsPresent()) { - OnPopupRequired(LocalisationService.GetString("vcruntime-check-error")); + OnPopupRequired(LocalizationService.GetString("vcruntime-check-error")); Helper.OpenBrowser("https://learn.microsoft.com/en-us/cpp/windows/latest-supported-vc-redist?view=msvc-170"); } } @@ -108,7 +108,7 @@ public partial class MainViewModel : ViewModelBase if(AccountInfoViewModel.Credentials.HasValue) { LoginText = - LocalisationService.GetString("auth-current-login-name", + LocalizationService.GetString("auth-current-login-name", new Dictionary { { "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" }, @@ -120,7 +120,7 @@ public partial class MainViewModel : ViewModelBase } else { - LoginText = LocalisationService.GetString("auth-current-login-no-name"); + LoginText = LocalizationService.GetString("auth-current-login-no-name"); } } @@ -130,7 +130,7 @@ public partial class MainViewModel : ViewModelBase return; var loadingHandler = ViewHelperService.GetViewModel(); - loadingHandler.LoadingName = LocalisationService.GetString("migration-label-task"); + loadingHandler.LoadingName = LocalizationService.GetString("migration-label-task"); loadingHandler.IsCancellable = false; if (!ContentService.CheckMigration(loadingHandler)) diff --git a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs index e4f018a..506f701 100644 --- a/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/AccountInfoViewModel.cs @@ -32,6 +32,7 @@ public partial class AccountInfoViewModel : ViewModelBase [ObservableProperty] private bool _isLogged; [ObservableProperty] private bool _doRetryAuth; [ObservableProperty] private AuthServerCredentials _authItemSelect; + [ObservableProperty] private string _authServerName; private bool _isProfilesEmpty; [GenerateProperty] private PopupMessageService PopupMessageService { get; } @@ -68,7 +69,7 @@ public partial class AccountInfoViewModel : ViewModelBase public void DoAuth(string? code = null) { var message = ViewHelperService.GetViewModel(); - message.InfoText = LocalisationService.GetString("auth-processing"); + message.InfoText = LocalizationService.GetString("auth-processing"); message.IsInfoClosable = false; PopupMessageService.Popup(message); @@ -95,7 +96,7 @@ public partial class AccountInfoViewModel : ViewModelBase } catch (Exception ex) { - exception = new Exception(LocalisationService.GetString("auth-error"), ex); + exception = new Exception(LocalizationService.GetString("auth-error"), ex); } } @@ -128,19 +129,19 @@ public partial class AccountInfoViewModel : ViewModelBase _logger.Log("TFA required"); break; case AuthenticateDenyCode.InvalidCredentials: - PopupError(LocalisationService.GetString("auth-invalid-credentials"), e); + PopupError(LocalizationService.GetString("auth-invalid-credentials"), e); break; case AuthenticateDenyCode.AccountLocked: - PopupError(LocalisationService.GetString("auth-account-locked"), e); + PopupError(LocalizationService.GetString("auth-account-locked"), e); break; case AuthenticateDenyCode.AccountUnconfirmed: - PopupError(LocalisationService.GetString("auth-account-unconfirmed"), e); + PopupError(LocalizationService.GetString("auth-account-unconfirmed"), e); break; case AuthenticateDenyCode.None: - PopupError(LocalisationService.GetString("auth-none"),e); + PopupError(LocalizationService.GetString("auth-none"),e); break; default: - PopupError(LocalisationService.GetString("auth-error-fuck"), e); + PopupError(LocalizationService.GetString("auth-error-fuck"), e); break; } } @@ -150,46 +151,46 @@ public partial class AccountInfoViewModel : ViewModelBase switch (e.HttpRequestError) { case HttpRequestError.ConnectionError: - PopupError(LocalisationService.GetString("auth-connection-error"), e); + PopupError(LocalizationService.GetString("auth-connection-error"), e); DoRetryAuth = true; break; case HttpRequestError.NameResolutionError: - PopupError(LocalisationService.GetString("auth-name-resolution-error"), e); + PopupError(LocalizationService.GetString("auth-name-resolution-error"), e); DoRetryAuth = true; break; case HttpRequestError.SecureConnectionError: - PopupError(LocalisationService.GetString("auth-secure-error"), e); + PopupError(LocalizationService.GetString("auth-secure-error"), e); DoRetryAuth = true; break; case HttpRequestError.UserAuthenticationError: - PopupError(LocalisationService.GetString("auth-user-authentication-error"), e); + PopupError(LocalizationService.GetString("auth-user-authentication-error"), e); break; case HttpRequestError.Unknown: - PopupError(LocalisationService.GetString("auth-unknown"), e); + PopupError(LocalizationService.GetString("auth-unknown"), e); break; case HttpRequestError.HttpProtocolError: - PopupError(LocalisationService.GetString("auth-http-protocol-error"), e); + PopupError(LocalizationService.GetString("auth-http-protocol-error"), e); break; case HttpRequestError.ExtendedConnectNotSupported: - PopupError(LocalisationService.GetString("auth-extended-connect-not-support"), e); + PopupError(LocalizationService.GetString("auth-extended-connect-not-support"), e); break; case HttpRequestError.VersionNegotiationError: - PopupError(LocalisationService.GetString("auth-version-negotiation-error"), e); + PopupError(LocalizationService.GetString("auth-version-negotiation-error"), e); break; case HttpRequestError.ProxyTunnelError: - PopupError(LocalisationService.GetString("auth-proxy-tunnel-error"), e); + PopupError(LocalizationService.GetString("auth-proxy-tunnel-error"), e); break; case HttpRequestError.InvalidResponse: - PopupError(LocalisationService.GetString("auth-invalid-response"), e); + PopupError(LocalizationService.GetString("auth-invalid-response"), e); break; case HttpRequestError.ResponseEnded: - PopupError(LocalisationService.GetString("auth-response-ended"), e); + PopupError(LocalizationService.GetString("auth-response-ended"), e); break; case HttpRequestError.ConfigurationLimitExceeded: - PopupError(LocalisationService.GetString("auth-configuration-limit-exceeded"), e); + PopupError(LocalizationService.GetString("auth-configuration-limit-exceeded"), e); break; default: - var authError = new Exception(LocalisationService.GetString("auth-error"), e); + var authError = new Exception(LocalizationService.GetString("auth-error"), e); _logger.Error(authError); PopupMessageService.Popup(authError); break; @@ -245,7 +246,7 @@ public partial class AccountInfoViewModel : ViewModelBase private async Task ReadAuthConfig() { var message = ViewHelperService.GetViewModel(); - message.InfoText = LocalisationService.GetString("auth-config-read"); + message.InfoText = LocalizationService.GetString("auth-config-read"); message.IsInfoClosable = false; PopupMessageService.Popup(message); @@ -318,7 +319,7 @@ public partial class AccountInfoViewModel : ViewModelBase } catch (Exception e) { - var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), e); + var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), e); _logger.Error(unexpectedError); return authTokenCredentials; } @@ -345,7 +346,7 @@ public partial class AccountInfoViewModel : ViewModelBase private void PopupError(string message, Exception e) { - message = LocalisationService.GetString("auth-error-occured") + message; + message = LocalizationService.GetString("auth-error-occured") + message; _logger.Error(new Exception(message, e)); var messageView = ViewHelperService.GetViewModel(); @@ -385,7 +386,7 @@ public partial class AccountInfoViewModel : ViewModelBase } var message = accountInfoViewModel.ViewHelperService.GetViewModel(); - message.InfoText = LocalisationService.GetString("auth-try-auth-config"); + message.InfoText = LocalizationService.GetString("auth-try-auth-config"); message.IsInfoClosable = false; accountInfoViewModel.PopupMessageService.Popup(message); @@ -423,7 +424,7 @@ public partial class AccountInfoViewModel : ViewModelBase { accountInfoViewModel.CurrentLogin = currProfile.Login; accountInfoViewModel.CurrentAuthServer = currProfile.AuthServer; - var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), ex); + var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), ex); accountInfoViewModel._logger.Error(unexpectedError); accountInfoViewModel.PopupMessageService.Popup(unexpectedError); errorRun = true; @@ -436,6 +437,8 @@ public partial class AccountInfoViewModel : ViewModelBase } accountInfoViewModel.IsLogged = true; + + accountInfoViewModel.AuthServerName = accountInfoViewModel.GetServerAuthName(currProfile.AuthServer); return currProfile; } diff --git a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs index 61e7ce7..0b2dee9 100644 --- a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs @@ -89,7 +89,7 @@ public partial class ConfigurationViewModel : ViewModelBase using var loader = ViewHelperService.GetViewModel(); loader.LoadingName = "Removing content"; PopupService.Popup(loader); - ContentService.RemoveAllContent(loader, CancellationService.Token); + ContentService.RemoveAllContent(loader.CreateLoadingContext(), CancellationService.Token); }); } diff --git a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs index 3e519e5..5d3db86 100644 --- a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs @@ -33,6 +33,7 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol [GenerateProperty] private FileService FileService { get; } = default!; [GenerateProperty] private PopupMessageService PopupService { get; } = default!; [GenerateProperty] private IServiceProvider ServiceProvider { get; } + [GenerateProperty] private CancellationService CancellationService { get; set; } = default!; [GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!; @@ -57,7 +58,11 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol loading.LoadingName = "Unpacking entry"; PopupService.Popup(loading); - Task.Run(() => ContentService.Unpack(serverEntry.FileApi, myTempDir, loading)); + Task.Run(() => + { + ContentService.Unpack(serverEntry.FileApi, myTempDir, loading.CreateLoadingContext()); + loading.Dispose(); + }); ExplorerHelper.OpenFolder(tmpDir); } @@ -74,7 +79,7 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol { var cur = ServiceProvider.GetService()!; cur.Init(this, ServerText.ToRobustUrl()); - var curContent = cur.Go(new ContentPath(SearchText)); + var curContent = cur.Go(new ContentPath(SearchText), CancellationService.Token); if(curContent == null) throw new NullReferenceException($"{SearchText} not found in {ServerText}"); @@ -144,11 +149,11 @@ public interface IContentEntry public string IconPath { get; } public ContentPath FullPath => Parent?.FullPath.With(Name) ?? new ContentPath(Name); - public IContentEntry? Go(ContentPath path); + public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken); public void GoCurrent() { - var entry = Go(ContentPath.Empty); + var entry = Go(ContentPath.Empty, CancellationToken.None); if(entry is not null) Holder.CurrentEntry = entry; } @@ -178,7 +183,7 @@ public sealed class LazyContentEntry : IContentEntry _lazyEntry = entry; _lazyEntryInit = lazyEntryInit; } - public IContentEntry? Go(ContentPath path) + public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken) { _lazyEntryInit?.Invoke(); return _lazyEntry; @@ -196,13 +201,13 @@ public sealed class ExtContentExecutor _decompilerService = decompilerService; } - public bool TryExecute(RobustManifestItem manifestItem) + public bool TryExecute(RobustManifestItem manifestItem, CancellationToken cancellationToken) { var ext = Path.GetExtension(manifestItem.Path); if (ext == ".dll") { - _decompilerService.OpenServerDecompiler(_root.ServerUrl); + _decompilerService.OpenServerDecompiler(_root.ServerUrl, cancellationToken); return true; } @@ -231,9 +236,9 @@ public sealed partial class ManifestContentEntry : IContentEntry _extContentExecutor = executor; } - public IContentEntry? Go(ContentPath path) + public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken) { - if (_extContentExecutor.TryExecute(_manifestItem)) + if (_extContentExecutor.TryExecute(_manifestItem, cancellationToken)) return null; var ext = Path.GetExtension(_manifestItem.Path); @@ -319,7 +324,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry { CreateContent(new ContentPath(path), item); } - + IsLoading = false; loading.Dispose(); }); @@ -433,11 +438,11 @@ public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry public IContentEntry? Parent { get; set; } public string? Name { get; private set; } - public IContentEntry? Go(ContentPath path) + public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken) { if (path.IsEmpty()) return this; if (_childs.TryGetValue(path.GetNext(), out var child)) - return child.Go(path); + return child.Go(path, cancellationToken); return null; } diff --git a/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs index d1fe23c..382ec5f 100644 --- a/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs @@ -66,7 +66,7 @@ public partial class ServerOverviewModel : ViewModelBase tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService()!.With(record.MainUrl), record.Name)); } - tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, "Favorite")); + tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, LocalizationService.GetString("tab-favorite"))); Items = new ObservableCollection(tempItems); diff --git a/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs b/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs index a895a71..073d1b7 100644 --- a/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/AddFavoriteViewModel.cs @@ -32,7 +32,7 @@ public partial class AddFavoriteViewModel : PopupViewModelBase [GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; } [GenerateProperty] private DebugService DebugService { get; } [GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; } - public override string Title => LocalisationService.GetString("popup-add-favorite"); + public override string Title => LocalizationService.GetString("popup-add-favorite"); public override bool IsClosable => true; [ObservableProperty] private string _ipInput; @@ -43,7 +43,7 @@ public partial class AddFavoriteViewModel : PopupViewModelBase try { if(string.IsNullOrWhiteSpace(IpInput)) - throw new Exception(LocalisationService.GetString("popup-add-favorite-invalid-ip")); + throw new Exception(LocalizationService.GetString("popup-add-favorite-invalid-ip")); var uri = IpInput.ToRobustUrl(); FavoriteServerListProvider.AddFavorite(uri); diff --git a/Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs b/Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs index 311d4de..25d61a0 100644 --- a/Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/EditServerNameViewModel.cs @@ -12,7 +12,7 @@ public sealed partial class EditServerNameViewModel : PopupViewModelBase { [GenerateProperty] public override PopupMessageService PopupMessageService { get; } [GenerateProperty] public ConfigurationService ConfigurationService { get; } - public override string Title => LocalisationService.GetString("popup-edit-name"); + public override string Title => LocalizationService.GetString("popup-edit-name"); public override bool IsClosable => true; [ObservableProperty] private string _ipInput; diff --git a/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs b/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs index 64d4f58..d5299a2 100644 --- a/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/ExceptionListViewModel.cs @@ -12,7 +12,7 @@ namespace Nebula.Launcher.ViewModels.Popup; public sealed partial class ExceptionListViewModel : PopupViewModelBase { [GenerateProperty] public override PopupMessageService PopupMessageService { get; } - public override string Title => LocalisationService.GetString("popup-exception"); + public override string Title => LocalizationService.GetString("popup-exception"); public override bool IsClosable => true; public ObservableCollection Errors { get; } = new(); diff --git a/Nebula.Launcher/ViewModels/Popup/InfoPopupViewModel.cs b/Nebula.Launcher/ViewModels/Popup/InfoPopupViewModel.cs index 99d1af7..dda5042 100644 --- a/Nebula.Launcher/ViewModels/Popup/InfoPopupViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/InfoPopupViewModel.cs @@ -14,7 +14,7 @@ public partial class InfoPopupViewModel : PopupViewModelBase [ObservableProperty] private string _infoText = "Test"; - public override string Title => LocalisationService.GetString("popup-information"); + public override string Title => LocalizationService.GetString("popup-information"); public bool IsInfoClosable { get; set; } = true; public override bool IsClosable => IsInfoClosable; diff --git a/Nebula.Launcher/ViewModels/Popup/IsLoginCredentialsNullPopup.cs b/Nebula.Launcher/ViewModels/Popup/IsLoginCredentialsNullPopup.cs index 8dd429b..175e419 100644 --- a/Nebula.Launcher/ViewModels/Popup/IsLoginCredentialsNullPopup.cs +++ b/Nebula.Launcher/ViewModels/Popup/IsLoginCredentialsNullPopup.cs @@ -45,6 +45,6 @@ public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase Dispose(); } - public override string Title => LocalisationService.GetString("popup-login-credentials-warning"); + public override string Title => LocalizationService.GetString("popup-login-credentials-warning"); public override bool IsClosable => true; } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Popup/LoadingContextViewModel.cs b/Nebula.Launcher/ViewModels/Popup/LoadingContextViewModel.cs index 71574ad..3d85195 100644 --- a/Nebula.Launcher/ViewModels/Popup/LoadingContextViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/LoadingContextViewModel.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; using Nebula.Launcher.Services; using Nebula.Launcher.Views.Popup; @@ -9,82 +11,121 @@ namespace Nebula.Launcher.ViewModels.Popup; [ViewModelRegister(typeof(LoadingContextView), false)] [ConstructGenerator] -public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandler +public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandlerFactory, IConnectionSpeedHandler { + public ObservableCollection LoadingContexts { get; } = []; + public ObservableCollection Values { get; } = []; + [ObservableProperty] private string _speedText = ""; + [ObservableProperty] private bool _showSpeed; + [ObservableProperty] private int _loadingColumnSize = 2; [GenerateProperty] public override PopupMessageService PopupMessageService { get; } [GenerateProperty] public CancellationService CancellationService { get; } - - [ObservableProperty] private int _currJobs; - [ObservableProperty] private int _resolvedJobs; - [ObservableProperty] private string _message = string.Empty; - public string LoadingName { get; set; } = LocalisationService.GetString("popup-loading"); + public string LoadingName { get; set; } = LocalizationService.GetString("popup-loading"); public bool IsCancellable { get; set; } = true; public override bool IsClosable => false; - public override string Title => LoadingName; + public override string Title => LocalizationService.GetString("popup-loading"); - public void SetJobsCount(int count) + public void Cancel() { - CurrJobs = count; - } - - public int GetJobsCount() - { - return CurrJobs; - } - - public void SetResolvedJobsCount(int count) - { - ResolvedJobs = count; - } - - public int GetResolvedJobsCount() - { - return ResolvedJobs; - } - - public void SetLoadingMessage(string message) - { - Message = message + "\n" + Message; - } - - public void Cancel(){ - if(!IsCancellable) return; + if (!IsCancellable) return; CancellationService.Cancel(); Dispose(); } + public void PasteSpeed(int speed) + { + if (Values.Count == 0) + { + ShowSpeed = true; + LoadingColumnSize = 1; + } + SpeedText = FileLoadingFormater.FormatBytes(speed) + " / s"; + Values.Add(speed); + if(Values.Count > 10) Values.RemoveAt(0); + } + + public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null) + { + var instance = new LoadingContext(this, loadingFormater ?? DefaultLoadingFormater.Instance); + LoadingContexts.Add(instance); + return instance; + } + + public void RemoveContextInstance(LoadingContext loadingContext) + { + LoadingContexts.Remove(loadingContext); + } + protected override void Initialise() { } protected override void InitialiseInDesignMode() { - SetJobsCount(5); - SetResolvedJobsCount(2); - string[] debugMessages = { - "Debug: Starting phase 1...", - "Debug: Loading assets...", - "Debug: Connecting to server...", - "Debug: Fetching user data...", - "Debug: Applying configurations...", - "Debug: Starting phase 2...", - "Debug: Rendering UI...", - "Debug: Preparing scene...", - "Debug: Initializing components...", - "Debug: Running diagnostics...", - "Debug: Checking dependencies...", - "Debug: Verifying files...", - "Debug: Cleaning up cache...", - "Debug: Finalizing setup...", - "Debug: Setup complete.", - "Debug: Ready for launch." - }; + var context = CreateLoadingContext(); + context.SetJobsCount(5); + context.SetResolvedJobsCount(2); + context.SetLoadingMessage("message"); - foreach (string message in debugMessages) + var ctx1 = CreateLoadingContext(new FileLoadingFormater()); + ctx1.SetJobsCount(1020120); + ctx1.SetResolvedJobsCount(12331); + ctx1.SetLoadingMessage("File data"); + + for (var i = 0; i < 14; i++) { - SetLoadingMessage(message); + PasteSpeed(Random.Shared.Next(10000000)); } } -} \ No newline at end of file +} + +public sealed partial class LoadingContext : ObservableObject, ILoadingHandler +{ + private readonly LoadingContextViewModel _master; + private readonly ILoadingFormater _loadingFormater; + public string LoadingText => _loadingFormater.Format(this); + + [ObservableProperty] private string _message = string.Empty; + [ObservableProperty] private long _currJobs; + [ObservableProperty] private long _resolvedJobs; + + public LoadingContext(LoadingContextViewModel master, ILoadingFormater loadingFormater) + { + _master = master; + _loadingFormater = loadingFormater; + } + + public void SetJobsCount(long count) + { + CurrJobs = count; + OnPropertyChanged(nameof(LoadingText)); + } + + public long GetJobsCount() + { + return CurrJobs; + } + + public void SetResolvedJobsCount(long count) + { + ResolvedJobs = count; + OnPropertyChanged(nameof(LoadingText)); + } + + public long GetResolvedJobsCount() + { + return ResolvedJobs; + } + + public void SetLoadingMessage(string message) + { + Message = message; + } + + public void Dispose() + { + _master.RemoveContextInstance(this); + } +} diff --git a/Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs b/Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs index 6cd1aba..864f239 100644 --- a/Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs +++ b/Nebula.Launcher/ViewModels/Popup/TfaViewModel.cs @@ -12,7 +12,7 @@ public partial class TfaViewModel : PopupViewModelBase { [GenerateProperty] public override PopupMessageService PopupMessageService { get; } [GenerateProperty] public AccountInfoViewModel AccountInfo { get; } - public override string Title => LocalisationService.GetString("popup-twofa"); + public override string Title => LocalizationService.GetString("popup-twofa"); public override bool IsClosable => true; protected override void InitialiseInDesignMode() diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index 142fda4..6cd2ff4 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -186,13 +186,12 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis try { - using var loadingContext = ViewHelperService.GetViewModel(); - loadingContext.LoadingName = "Loading instance..."; - ((ILoadingHandler)loadingContext).AppendJob(); + using var viewModelLoading = ViewHelperService.GetViewModel(); + viewModelLoading.LoadingName = "Loading instance..."; - PopupMessageService.Popup(loadingContext); + PopupMessageService.Popup(viewModelLoading); _currentInstance = - await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, loadingContext, CancellationService.Token); + await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, viewModelLoading, CancellationService.Token); _logger.Log("Preparing instance..."); _currentInstance.RegisterLogger(_currentContentLogConsumer); _currentInstance.RegisterLogger(new DebugLoggerBridge(DebugService.GetLogger($"PROCESS_{Random.Shared.Next(65535)}"))); diff --git a/Nebula.Launcher/Views/MainWindow.axaml b/Nebula.Launcher/Views/MainWindow.axaml index b1aeb63..1f0f900 100644 --- a/Nebula.Launcher/Views/MainWindow.axaml +++ b/Nebula.Launcher/Views/MainWindow.axaml @@ -3,9 +3,9 @@ ExtendClientAreaChromeHints="NoChrome" ExtendClientAreaTitleBarHeightHint="-1" ExtendClientAreaToDecorationsHint="True" - Height="500" + Height="550" Icon="/Assets/nebula.ico" - MinHeight="500" + MinHeight="550" MinWidth="800" SystemDecorations="BorderOnly" Title="Nebula.Launcher" diff --git a/Nebula.Launcher/Views/Pages/AccountInfoView.axaml b/Nebula.Launcher/Views/Pages/AccountInfoView.axaml index 5aa3af0..91ad0f1 100644 --- a/Nebula.Launcher/Views/Pages/AccountInfoView.axaml +++ b/Nebula.Launcher/Views/Pages/AccountInfoView.axaml @@ -188,14 +188,14 @@ Margin="0,0,0,20" Path="/Assets/svg/user.svg" /> diff --git a/Nebula.Launcher/Views/Popup/LoadingContextView.axaml b/Nebula.Launcher/Views/Popup/LoadingContextView.axaml index fed3645..dc1ec84 100644 --- a/Nebula.Launcher/Views/Popup/LoadingContextView.axaml +++ b/Nebula.Launcher/Views/Popup/LoadingContextView.axaml @@ -10,33 +10,71 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Nebula.Runner/App.cs b/Nebula.Runner/App.cs index 3d37907..0b0da77 100644 --- a/Nebula.Runner/App.cs +++ b/Nebula.Runner/App.cs @@ -49,7 +49,7 @@ public sealed class App(RunnerService runnerService, ContentService contentServi args.Add("--ss14-address"); args.Add(url.ToString()); - await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandler(), cancelTokenSource.Token); + await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandlerFactory(), cancelTokenSource.Token); } catch (Exception e) { diff --git a/Nebula.Runner/Services/RunnerService.cs b/Nebula.Runner/Services/RunnerService.cs index ab44822..cb9b2d9 100644 --- a/Nebula.Runner/Services/RunnerService.cs +++ b/Nebula.Runner/Services/RunnerService.cs @@ -6,6 +6,7 @@ using Nebula.Shared; using Nebula.Shared.Models; using Nebula.Shared.Services; using Nebula.Shared.Services.Logging; +using Nebula.Shared.Utils; using Robust.LoaderApi; namespace Nebula.Runner.Services; @@ -24,12 +25,12 @@ public sealed class RunnerService( private bool MetricEnabled = false; //TODO: ADD METRIC THINKS LATER public async Task Run(string[] runArgs, RobustBuildInfo buildInfo, IRedialApi redialApi, - ILoadingHandler loadingHandler, + ILoadingHandlerFactory loadingHandler, CancellationToken cancellationToken) { _logger.Log("Start Content!"); - - var engine = await engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion); + + var engine = await engineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandler, cancellationToken); if (engine is null) throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion); @@ -48,7 +49,7 @@ public sealed class RunnerService( foreach (var moduleStr in modules) { var module = - await engineService.EnsureEngineModules(moduleStr, buildInfo.BuildInfo.Build.EngineVersion); + await engineService.EnsureEngineModules(moduleStr, loadingHandler, buildInfo.BuildInfo.Build.EngineVersion); if (module is not null) extraMounts.Add(new ApiMount(module, "/")); } @@ -78,8 +79,8 @@ public sealed class RunnerService( MetricsEnabledPatcher.ApplyPatch(reflectionService, harmonyService); metricServer = RunHelper.RunMetric(prometheusAssembly); } - - + + loadingHandler.Dispose(); await Task.Run(() => loader.Main(args), cancellationToken); metricServer?.Dispose(); @@ -140,44 +141,3 @@ public static class RunHelper } } -public static class ContentManifestParser -{ - public static List ExtractModules(Stream manifestStream) - { - using var reader = new StreamReader(manifestStream); - return ExtractModules(reader.ReadToEnd()); - } - - public static List ExtractModules(string manifestContent) - { - var modules = new List(); - var lines = manifestContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); - - bool inModulesSection = false; - - foreach (var rawLine in lines) - { - var line = rawLine.Trim(); - - if (line.StartsWith("modules:")) - { - inModulesSection = true; - continue; - } - - if (inModulesSection) - { - if (line.StartsWith("- ")) - { - modules.Add(line.Substring(2).Trim()); - } - else if (!line.StartsWith(" ")) - { - break; - } - } - } - - return modules; - } -} \ No newline at end of file diff --git a/Nebula.Shared/FileApis/FileApi.cs b/Nebula.Shared/FileApis/FileApi.cs index c257c03..5ed2179 100644 --- a/Nebula.Shared/FileApis/FileApi.cs +++ b/Nebula.Shared/FileApis/FileApi.cs @@ -1,5 +1,7 @@ using System.Diagnostics.CodeAnalysis; using Nebula.Shared.FileApis.Interfaces; +using Nebula.Shared.Models; +using Nebula.Shared.Utils; namespace Nebula.Shared.FileApis; @@ -31,7 +33,7 @@ public sealed class FileApi : IReadWriteFileApi return false; } - public bool Save(string path, Stream input) + public bool Save(string path, Stream input, ILoadingHandler? loadingHandler = null) { var currPath = Path.Join(RootPath, path); @@ -41,6 +43,13 @@ public sealed class FileApi : IReadWriteFileApi if (!dirInfo.Exists) dirInfo.Create(); using var stream = new FileStream(currPath, FileMode.Create, FileAccess.Write, FileShare.None); + + if (loadingHandler != null) + { + input.CopyTo(stream, loadingHandler); + return true; + } + input.CopyTo(stream); return true; } diff --git a/Nebula.Shared/FileApis/HashApi.cs b/Nebula.Shared/FileApis/HashApi.cs index 66d0579..98ebe14 100644 --- a/Nebula.Shared/FileApis/HashApi.cs +++ b/Nebula.Shared/FileApis/HashApi.cs @@ -47,8 +47,8 @@ public class HashApi : IFileApi return false; } - public bool Save(RobustManifestItem item, Stream stream){ - return _fileApi.Save(GetManifestPath(item), stream); + public bool Save(RobustManifestItem item, Stream stream, ILoadingHandler? loadingHandler){ + return _fileApi.Save(GetManifestPath(item), stream, loadingHandler); } public bool Has(RobustManifestItem item){ diff --git a/Nebula.Shared/FileApis/Interfaces/IWriteFileApi.cs b/Nebula.Shared/FileApis/Interfaces/IWriteFileApi.cs index 801ec33..8d84f77 100644 --- a/Nebula.Shared/FileApis/Interfaces/IWriteFileApi.cs +++ b/Nebula.Shared/FileApis/Interfaces/IWriteFileApi.cs @@ -1,8 +1,10 @@ -namespace Nebula.Shared.FileApis.Interfaces; +using Nebula.Shared.Models; + +namespace Nebula.Shared.FileApis.Interfaces; public interface IWriteFileApi { - public bool Save(string path, Stream input); + public bool Save(string path, Stream input, ILoadingHandler? loadingHandler = null); public bool Remove(string path); public bool Has(string path); } \ No newline at end of file diff --git a/Nebula.Shared/Models/ILoadingHandler.cs b/Nebula.Shared/Models/ILoadingHandler.cs index bf3a0d6..5ccf67b 100644 --- a/Nebula.Shared/Models/ILoadingHandler.cs +++ b/Nebula.Shared/Models/ILoadingHandler.cs @@ -1,20 +1,20 @@ namespace Nebula.Shared.Models; -public interface ILoadingHandler +public interface ILoadingHandler : IDisposable { - public void SetJobsCount(int count); - public int GetJobsCount(); + public void SetJobsCount(long count); + public long GetJobsCount(); - public void SetResolvedJobsCount(int count); - public int GetResolvedJobsCount(); + public void SetResolvedJobsCount(long count); + public long GetResolvedJobsCount(); public void SetLoadingMessage(string message); - public void AppendJob(int count = 1) + public void AppendJob(long count = 1) { SetJobsCount(GetJobsCount() + count); } - public void AppendResolvedJob(int count = 1) + public void AppendResolvedJob(long count = 1) { SetResolvedJobsCount(GetResolvedJobsCount() + count); } @@ -31,6 +31,57 @@ public interface ILoadingHandler } } +public interface ILoadingFormater +{ + public string Format(ILoadingHandler loadingHandler); +} + +public interface ILoadingHandlerFactory: IDisposable +{ + public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null); +} + +public interface IConnectionSpeedHandler +{ + public void PasteSpeed(int speed); +} + +public sealed class DefaultLoadingFormater : ILoadingFormater +{ + public static DefaultLoadingFormater Instance = new DefaultLoadingFormater(); + public string Format(ILoadingHandler loadingHandler) + { + return loadingHandler.GetResolvedJobsCount() + "/" + loadingHandler.GetJobsCount(); + } +} + +public sealed class FileLoadingFormater : ILoadingFormater +{ + public string Format(ILoadingHandler loadingHandler) + { + return FormatBytes(loadingHandler.GetResolvedJobsCount()) + " / " + FormatBytes(loadingHandler.GetJobsCount()); + } + + public static string FormatBytes(long bytes) + { + const long KB = 1024; + const long MB = KB * 1024; + const long GB = MB * 1024; + const long TB = GB * 1024; + + if (bytes >= TB) + return $"{bytes / (double)TB:0.##} TB"; + if (bytes >= GB) + return $"{bytes / (double)GB:0.##} GB"; + if (bytes >= MB) + return $"{bytes / (double)MB:0.##} MB"; + if (bytes >= KB) + return $"{bytes / (double)KB:0.##} KB"; + + return $"{bytes} B"; + } +} + public sealed class QueryJob : IDisposable { private readonly ILoadingHandler _handler; diff --git a/Nebula.Shared/Services/ConfigurationService.cs b/Nebula.Shared/Services/ConfigurationService.cs index cb82dc8..b2c59df 100644 --- a/Nebula.Shared/Services/ConfigurationService.cs +++ b/Nebula.Shared/Services/ConfigurationService.cs @@ -32,19 +32,18 @@ public class ConfigurationService ConfigurationApi = fileService.CreateFileApi("config"); } - public void MigrateConfigs(ILoadingHandler loadingHandler) + public void MigrateConfigs(ILoadingHandlerFactory loadingHandlerFactory) { Task.Run(async () => { + var loadingHandler = loadingHandlerFactory.CreateLoadingContext(); foreach (var migration in _migrations) { await migration.DoMigrate(this, _serviceProvider, loadingHandler); } - if (loadingHandler is IDisposable disposable) - { - disposable.Dispose(); - } + loadingHandler.Dispose(); + loadingHandlerFactory.Dispose(); }); } diff --git a/Nebula.Shared/Services/ContentService.Download.cs b/Nebula.Shared/Services/ContentService.Download.cs index 71f79e8..53e9764 100644 --- a/Nebula.Shared/Services/ContentService.Download.cs +++ b/Nebula.Shared/Services/ContentService.Download.cs @@ -1,4 +1,5 @@ using System.Buffers.Binary; +using System.Diagnostics; using System.Globalization; using System.Net.Http.Headers; using System.Numerics; @@ -34,11 +35,10 @@ public partial class ContentService } public async Task EnsureItems(ManifestReader manifestReader, Uri downloadUri, - ILoadingHandler loadingHandler, + ILoadingHandlerFactory loadingFactory, CancellationToken cancellationToken) { List allItems = []; - List items = []; while (manifestReader.TryReadItem(out var item)) { @@ -50,40 +50,44 @@ public partial class ContentService var hashApi = CreateHashApi(allItems); - items = allItems.Where(a=> !hashApi.Has(a)).ToList(); - - loadingHandler.SetLoadingMessage("Download Count:" + items.Count); + var items = allItems.Where(a=> !hashApi.Has(a)).ToList(); + _logger.Log("Download Count:" + items.Count); - await Download(downloadUri, items, hashApi, loadingHandler, cancellationToken); + await Download(downloadUri, items, hashApi, loadingFactory, cancellationToken); return hashApi; } - public async Task EnsureItems(RobustManifestInfo info, ILoadingHandler loadingHandler, + public async Task EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory, CancellationToken cancellationToken) { _logger.Log("Getting manifest: " + info.Hash); - loadingHandler.SetLoadingMessage("Getting manifest: " + info.Hash); + var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater()); + loadingHandler.SetLoadingMessage("Loading manifest"); if (ManifestFileApi.TryOpen(info.Hash, out var stream)) { - _logger.Log("Loading manifest from: " + info.Hash); - return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingHandler, cancellationToken); + _logger.Log("Loading manifest from disk"); + loadingHandler.Dispose(); + return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingFactory, cancellationToken); } SetServerHash(info.ManifestUri.ToString(), info.Hash); _logger.Log("Fetching manifest from: " + info.ManifestUri); - loadingHandler.SetLoadingMessage("Fetching manifest from: " + info.ManifestUri); + loadingHandler.SetLoadingMessage("Fetching manifest from: " + info.ManifestUri.Host); var response = await _http.GetAsync(info.ManifestUri, cancellationToken); - if (!response.IsSuccessStatusCode) throw new Exception(); - + response.EnsureSuccessStatusCode(); + + loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0); await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken); - ManifestFileApi.Save(info.Hash, streamContent); + ManifestFileApi.Save(info.Hash, streamContent, loadingHandler); + loadingHandler.Dispose(); streamContent.Seek(0, SeekOrigin.Begin); + using var manifestReader = new ManifestReader(streamContent); - return await EnsureItems(manifestReader, info.DownloadUri, loadingHandler, cancellationToken); + return await EnsureItems(manifestReader, info.DownloadUri, loadingFactory, cancellationToken); } public void Unpack(HashApi hashApi, IWriteFileApi otherApi, ILoadingHandler loadingHandler) @@ -107,30 +111,22 @@ public partial class ContentService } else { - _logger.Error("OH FUCK!! " + item.Path); + _logger.Error("Error while unpacking thinks " + item.Path); } loadingHandler.AppendResolvedJob(); }); - - if (loadingHandler is IDisposable disposable) - { - disposable.Dispose(); - } } - public async Task Download(Uri contentCdn, List toDownload, HashApi hashApi, ILoadingHandler loadingHandler, + private async Task Download(Uri contentCdn, List toDownload, HashApi hashApi, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken) { if (toDownload.Count == 0 || cancellationToken.IsCancellationRequested) { - _logger.Log("Nothing to download! Fuck this!"); + _logger.Log("Nothing to download! Skip!"); return; } - - var downloadJobWatch = loadingHandler.GetQueryJob(); - - loadingHandler.SetLoadingMessage("Downloading from: " + contentCdn); + _logger.Log("Downloading from: " + contentCdn); var requestBody = new byte[toDownload.Count * 4]; @@ -152,70 +148,56 @@ public partial class ContentService request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("zstd")); var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken); - - if (cancellationToken.IsCancellationRequested) - { - _logger.Log("Downloading cancelled!"); - return; - } - - downloadJobWatch.Dispose(); - response.EnsureSuccessStatusCode(); - var stream = await response.Content.ReadAsStreamAsync(); - var bandwidthStream = new BandwidthStream(stream); - stream = bandwidthStream; + var stream = await response.Content.ReadAsStreamAsync(cancellationToken); if (response.Content.Headers.ContentEncoding.Contains("zstd")) stream = new ZStdDecompressStream(stream); await using var streamDispose = stream; - - // Read flags header - var streamHeader = await stream.ReadExactAsync(4, null); + + var streamHeader = await stream.ReadExactAsync(4, cancellationToken); var streamFlags = (DownloadStreamHeaderFlags)BinaryPrimitives.ReadInt32LittleEndian(streamHeader); var preCompressed = (streamFlags & DownloadStreamHeaderFlags.PreCompressed) != 0; - - // compressContext.SetParameter(ZSTD_cParameter.ZSTD_c_nbWorkers, 4); - // If the stream is pre-compressed we need to decompress the blobs to verify BLAKE2B hash. - // If it isn't, we need to manually try re-compressing individual files to store them. + var compressContext = preCompressed ? null : new ZStdCCtx(); var decompressContext = preCompressed ? new ZStdDCtx() : null; - - // Normal file header: - // uncompressed length - // When preCompressed is set, we add: - // compressed length + var fileHeader = new byte[preCompressed ? 8 : 4]; + var downloadLoadHandler = loadingHandlerFactory.CreateLoadingContext(); + downloadLoadHandler.SetJobsCount(toDownload.Count); + downloadLoadHandler.SetLoadingMessage("Fetching files..."); + if (loadingHandlerFactory is IConnectionSpeedHandler speedHandlerStart) + speedHandlerStart.PasteSpeed(0); + try { - // Buffer for storing compressed ZStd data. var compressBuffer = new byte[1024]; - - // Buffer for storing uncompressed data. var readBuffer = new byte[1024]; var i = 0; - - loadingHandler.AppendJob(toDownload.Count); + var downloadWatchdog = new Stopwatch(); + var lengthAcc = 0; + var timeAcc = 0L; foreach (var item in toDownload) { - if (cancellationToken.IsCancellationRequested) - { - _logger.Log("Downloading cancelled!"); - decompressContext?.Dispose(); - compressContext?.Dispose(); - return; - } - + cancellationToken.ThrowIfCancellationRequested(); + + downloadWatchdog.Restart(); + // Read file header. - await stream.ReadExactAsync(fileHeader, null); + await stream.ReadExactAsync(fileHeader, cancellationToken); var length = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(0, 4)); + + var fileLoadingHandler = loadingHandlerFactory.CreateLoadingContext(new FileLoadingFormater()); + fileLoadingHandler.SetLoadingMessage(item.Path.Split("/").Last()); + var blockFileLoadHandle = length <= 100000; + EnsureBuffer(ref readBuffer, length); var data = readBuffer.AsMemory(0, length); @@ -226,9 +208,10 @@ public partial class ContentService if (compressedLength > 0) { + fileLoadingHandler.AppendJob(compressedLength); EnsureBuffer(ref compressBuffer, compressedLength); var compressedData = compressBuffer.AsMemory(0, compressedLength); - await stream.ReadExactAsync(compressedData, null); + await stream.ReadExactAsync(compressedData, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler); // Decompress so that we can verify hash down below. @@ -239,24 +222,53 @@ public partial class ContentService } else { - await stream.ReadExactAsync(data, null); + fileLoadingHandler.AppendJob(length); + await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler); } } else { - await stream.ReadExactAsync(data, null); + fileLoadingHandler.AppendJob(length); + await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler); } using var fileStream = new MemoryStream(data.ToArray()); - hashApi.Save(item, fileStream); + hashApi.Save(item, fileStream, null); _logger.Log("file saved:" + item.Path); - loadingHandler.AppendResolvedJob(); + fileLoadingHandler.Dispose(); + downloadLoadHandler.AppendResolvedJob(); i += 1; + + if (loadingHandlerFactory is not IConnectionSpeedHandler speedHandler) + continue; + + if (downloadWatchdog.ElapsedMilliseconds + timeAcc < 1000) + { + timeAcc += downloadWatchdog.ElapsedMilliseconds; + lengthAcc += length; + continue; + } + + if (timeAcc != 0) + { + timeAcc += downloadWatchdog.ElapsedMilliseconds; + lengthAcc += length; + + speedHandler.PasteSpeed((int)(lengthAcc / (timeAcc / 1000))); + + timeAcc = 0; + lengthAcc = 0; + + continue; + } + + speedHandler.PasteSpeed((int)(length / (downloadWatchdog.ElapsedMilliseconds / 1000))); } } finally { + downloadLoadHandler.Dispose(); decompressContext?.Dispose(); compressContext?.Dispose(); } diff --git a/Nebula.Shared/Services/ContentService.Migration.cs b/Nebula.Shared/Services/ContentService.Migration.cs index e9ca2fb..7b36590 100644 --- a/Nebula.Shared/Services/ContentService.Migration.cs +++ b/Nebula.Shared/Services/ContentService.Migration.cs @@ -5,7 +5,7 @@ namespace Nebula.Shared.Services; public partial class ContentService { - public bool CheckMigration(ILoadingHandler loadingHandler) + public bool CheckMigration(ILoadingHandlerFactory loadingHandler) { _logger.Log("Checking migration..."); @@ -17,16 +17,13 @@ public partial class ContentService return true; } - private void DoMigration(ILoadingHandler loadingHandler, List migrationList) + private void DoMigration(ILoadingHandlerFactory loadingHandler, List migrationList) { - loadingHandler.SetJobsCount(migrationList.Count); + var mainLoadingHandler = loadingHandler.CreateLoadingContext(); + mainLoadingHandler.SetJobsCount(migrationList.Count); - Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f,loadingHandler)); - - if (loadingHandler is IDisposable disposable) - { - disposable.Dispose(); - } + Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f, mainLoadingHandler) ); + loadingHandler.Dispose(); } private void MigrateFile(string file, ILoadingHandler loadingHandler) diff --git a/Nebula.Shared/Services/EngineService.cs b/Nebula.Shared/Services/EngineService.cs index 833f573..6402f87 100644 --- a/Nebula.Shared/Services/EngineService.cs +++ b/Nebula.Shared/Services/EngineService.cs @@ -108,11 +108,13 @@ public sealed class EngineService return info != null; } - public async Task EnsureEngine(string version) + public async Task EnsureEngine(string version, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default) { _logger.Log("Ensure engine " + version); + using var loadingHandler = loadingHandlerFactory.CreateLoadingContext(new FileLoadingFormater()); + loadingHandler.SetLoadingMessage("Ensuring engine " + version); - if (!TryOpen(version)) await DownloadEngine(version); + if (!TryOpen(version)) await DownloadEngine(version, loadingHandler, cancellationToken); try { @@ -128,15 +130,24 @@ public sealed class EngineService return null; } - public async Task DownloadEngine(string version) + public async Task DownloadEngine(string version, ILoadingHandler loadingHandler, CancellationToken cancellationToken = default) { if (!TryGetVersionInfo(version, out var info)) return; _logger.Log("Downloading engine version " + version); + loadingHandler.SetLoadingMessage("Downloading engine version " + version); + loadingHandler.Clear(); + using var client = new HttpClient(); - var s = await client.GetStreamAsync(info.Url); - _engineFileApi.Save(version, s); + + var response = await client.GetAsync(info.Url, cancellationToken); + response.EnsureSuccessStatusCode(); + loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0); + + await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken); + var s = await client.GetStreamAsync(info.Url, cancellationToken); + _engineFileApi.Save(version, s, loadingHandler); await s.DisposeAsync(); } @@ -176,7 +187,7 @@ public sealed class EngineService { GetEngineInfo(out var modulesInfo, out var engineVersionInfo); - var engineVersionObj = Version.Parse(engineVersion); + var engineVersionObj = Version.Parse(engineVersion.Split("-")[0]); var module = modulesInfo.Modules[moduleName]; var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv }) .Where(kv => engineVersionObj >= kv.Version) @@ -187,15 +198,18 @@ public sealed class EngineService return selectedVersion.Key; } - public async Task EnsureEngineModules(string moduleName, string engineVersion) + public async Task EnsureEngineModules(string moduleName, ILoadingHandlerFactory loadingHandlerFactory, string engineVersion) { var moduleVersion = ResolveModuleVersion(moduleName, engineVersion); if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var buildInfo)) return null; var fileName = ConcatName(moduleName, moduleVersion); + + using var loadingHandler = loadingHandlerFactory.CreateLoadingContext(new FileLoadingFormater()); + loadingHandler.SetLoadingMessage("Ensuring engine module " + fileName); - if (!TryOpen(fileName)) await DownloadEngineModule(moduleName, moduleVersion); + if (!TryOpen(fileName)) await DownloadEngineModule(moduleName, loadingHandler, moduleVersion); try { @@ -210,19 +224,20 @@ public sealed class EngineService } } - public async Task DownloadEngineModule(string moduleName, string moduleVersion) + public async Task DownloadEngineModule(string moduleName, ILoadingHandler loadingHandler, string moduleVersion) { if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var info)) return; _logger.Log("Downloading engine module version " + moduleVersion); + loadingHandler.SetLoadingMessage("Downloading engine module version " + moduleVersion); using var client = new HttpClient(); var s = await client.GetStreamAsync(info.Url); - _engineFileApi.Save(ConcatName(moduleName, moduleVersion), s); + _engineFileApi.Save(ConcatName(moduleName, moduleVersion), s, loadingHandler); await s.DisposeAsync(); } - public string ConcatName(string moduleName, string moduleVersion) + private string ConcatName(string moduleName, string moduleVersion) { return moduleName + "" + moduleVersion; } diff --git a/Nebula.Shared/Services/FileService.cs b/Nebula.Shared/Services/FileService.cs index 10d95d4..a58ace9 100644 --- a/Nebula.Shared/Services/FileService.cs +++ b/Nebula.Shared/Services/FileService.cs @@ -89,14 +89,26 @@ public class FileService } } +public sealed class ConsoleLoadingHandlerFactory : ILoadingHandlerFactory +{ + public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null) + { + return new ConsoleLoadingHandler(); + } + + public void Dispose() + { + } +} + public sealed class ConsoleLoadingHandler : ILoadingHandler { - private int _currJobs; + private long _currJobs; private float _percent; - private int _resolvedJobs; + private long _resolvedJobs; - public void SetJobsCount(int count) + public void SetJobsCount(long count) { _currJobs = count; @@ -104,12 +116,12 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler Draw(); } - public int GetJobsCount() + public long GetJobsCount() { return _currJobs; } - public void SetResolvedJobsCount(int count) + public void SetResolvedJobsCount(long count) { _resolvedJobs = count; @@ -117,7 +129,7 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler Draw(); } - public int GetResolvedJobsCount() + public long GetResolvedJobsCount() { return _resolvedJobs; } @@ -154,4 +166,9 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler Console.Write($"\t {_resolvedJobs}/{_currJobs}"); } + + public void Dispose() + { + + } } \ No newline at end of file diff --git a/Nebula.Shared/Services/RestService.cs b/Nebula.Shared/Services/RestService.cs index 2bc90a7..34ec528 100644 --- a/Nebula.Shared/Services/RestService.cs +++ b/Nebula.Shared/Services/RestService.cs @@ -29,10 +29,7 @@ public class RestService [Pure] public async Task GetAsync(Uri uri, CancellationToken cancellationToken) where T : notnull { - var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri) - { - Version = HttpVersion.Version10, - }; + var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri); var response = await _client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false); return await ReadResult(response, cancellationToken, uri); diff --git a/Nebula.Shared/Utils/BandwidthStream.cs b/Nebula.Shared/Utils/BandwidthStream.cs deleted file mode 100644 index 4778cd8..0000000 --- a/Nebula.Shared/Utils/BandwidthStream.cs +++ /dev/null @@ -1,138 +0,0 @@ -using System.Diagnostics; - -namespace Nebula.Shared.Utils; - -public sealed class BandwidthStream : Stream -{ - private const int NumSeconds = 8; - private const int BucketDivisor = 2; - private const int BucketsPerSecond = 2 << BucketDivisor; - - // TotalBuckets MUST be power of two! - private const int TotalBuckets = NumSeconds * BucketsPerSecond; - private readonly Stream _baseStream; - private readonly long[] _buckets; - - private readonly Stopwatch _stopwatch; - - private long _bucketIndex; - - public BandwidthStream(Stream baseStream) - { - _stopwatch = Stopwatch.StartNew(); - _baseStream = baseStream; - _buckets = new long[TotalBuckets]; - } - - public override bool CanRead => _baseStream.CanRead; - - public override bool CanSeek => _baseStream.CanSeek; - - public override bool CanWrite => _baseStream.CanWrite; - - public override long Length => _baseStream.Length; - - public override long Position - { - get => _baseStream.Position; - set => _baseStream.Position = value; - } - - private void TrackBandwidth(long value) - { - const int bucketMask = TotalBuckets - 1; - - var bucketIdx = CurBucketIdx(); - - // Increment to bucket idx, clearing along the way. - if (bucketIdx != _bucketIndex) - { - var diff = bucketIdx - _bucketIndex; - if (diff > TotalBuckets) - for (var i = _bucketIndex; i < bucketIdx; i++) - _buckets[i & bucketMask] = 0; - else - // We managed to skip so much time the whole buffer is empty. - Array.Clear(_buckets); - - _bucketIndex = bucketIdx; - } - - // Write value. - _buckets[bucketIdx & bucketMask] += value; - } - - private long CurBucketIdx() - { - var elapsed = _stopwatch.Elapsed.TotalSeconds; - return (long)(elapsed / BucketsPerSecond); - } - - public long CalcCurrentAvg() - { - var sum = 0L; - - for (var i = 0; i < TotalBuckets; i++) sum += _buckets[i]; - - return sum >> BucketDivisor; - } - - public override void Flush() - { - _baseStream.Flush(); - } - - public override Task FlushAsync(CancellationToken cancellationToken) - { - return _baseStream.FlushAsync(cancellationToken); - } - - protected override void Dispose(bool disposing) - { - if (disposing) - _baseStream.Dispose(); - } - - public override ValueTask DisposeAsync() - { - return _baseStream.DisposeAsync(); - } - - public override int Read(byte[] buffer, int offset, int count) - { - var read = _baseStream.Read(buffer, offset, count); - TrackBandwidth(read); - return read; - } - - public override async ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) - { - var read = await base.ReadAsync(buffer, cancellationToken); - TrackBandwidth(read); - return read; - } - - public override long Seek(long offset, SeekOrigin origin) - { - return _baseStream.Seek(offset, origin); - } - - public override void SetLength(long value) - { - _baseStream.SetLength(value); - } - - public override void Write(byte[] buffer, int offset, int count) - { - _baseStream.Write(buffer, offset, count); - TrackBandwidth(count); - } - - public override async ValueTask WriteAsync( - ReadOnlyMemory buffer, - CancellationToken cancellationToken = default) - { - await _baseStream.WriteAsync(buffer, cancellationToken); - TrackBandwidth(buffer.Length); - } -} \ No newline at end of file diff --git a/Nebula.Shared/Utils/ContentManifestParser.cs b/Nebula.Shared/Utils/ContentManifestParser.cs new file mode 100644 index 0000000..aa5f9f1 --- /dev/null +++ b/Nebula.Shared/Utils/ContentManifestParser.cs @@ -0,0 +1,43 @@ +namespace Nebula.Shared.Utils; + +public static class ContentManifestParser +{ + public static List ExtractModules(Stream manifestStream) + { + using var reader = new StreamReader(manifestStream); + return ExtractModules(reader.ReadToEnd()); + } + + public static List ExtractModules(string manifestContent) + { + var modules = new List(); + var lines = manifestContent.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries); + + bool inModulesSection = false; + + foreach (var rawLine in lines) + { + var line = rawLine.Trim(); + + if (line.StartsWith("modules:")) + { + inModulesSection = true; + continue; + } + + if (inModulesSection) + { + if (line.StartsWith("- ")) + { + modules.Add(line.Substring(2).Trim()); + } + else if (!line.StartsWith(" ")) + { + break; + } + } + } + + return modules; + } +} \ No newline at end of file diff --git a/Nebula.Shared/Utils/StreamHelper.cs b/Nebula.Shared/Utils/StreamHelper.cs index 1693f95..a49cb1a 100644 --- a/Nebula.Shared/Utils/StreamHelper.cs +++ b/Nebula.Shared/Utils/StreamHelper.cs @@ -1,21 +1,37 @@ using System.Buffers; +using Nebula.Shared.Models; namespace Nebula.Shared.Utils; public static class StreamHelper { - public static async ValueTask ReadExactAsync(this Stream stream, int amount, CancellationToken? cancel) + public static void CopyTo(this Stream input, Stream output, ILoadingHandler loadingHandler) + { + const int bufferSize = 81920; + var buffer = new byte[bufferSize]; + + int bytesRead; + while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0) + { + output.Write(buffer, 0, bytesRead); + loadingHandler.AppendResolvedJob(bytesRead); + } + } + + public static async ValueTask ReadExactAsync(this Stream stream, int amount, CancellationToken cancel = default) { var data = new byte[amount]; await ReadExactAsync(stream, data, cancel); return data; } - public static async ValueTask ReadExactAsync(this Stream stream, Memory into, CancellationToken? cancel) + public static async ValueTask ReadExactAsync(this Stream stream, Memory into, CancellationToken cancel = default, ILoadingHandler? loadingHandler = null) { while (into.Length > 0) { - var read = await stream.ReadAsync(into); + var read = await stream.ReadAsync(into, cancel); + + loadingHandler?.AppendResolvedJob(read); // Check EOF. if (read == 0) @@ -24,31 +40,4 @@ public static class StreamHelper into = into[read..]; } } - - public static async Task CopyAmountToAsync( - this Stream stream, - Stream to, - int amount, - int bufferSize, - CancellationToken cancel) - { - var buffer = ArrayPool.Shared.Rent(bufferSize); - - while (amount > 0) - { - Memory readInto = buffer; - if (amount < readInto.Length) - readInto = readInto[..amount]; - - var read = await stream.ReadAsync(readInto, cancel); - if (read == 0) - throw new EndOfStreamException(); - - amount -= read; - - readInto = readInto[..read]; - - await to.WriteAsync(readInto, cancel); - } - } } \ No newline at end of file diff --git a/Nebula.sln.DotSettings.user b/Nebula.sln.DotSettings.user index 2a9f965..8c31f8a 100644 --- a/Nebula.sln.DotSettings.user +++ b/Nebula.sln.DotSettings.user @@ -7,6 +7,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -25,6 +26,7 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -37,10 +39,13 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded @@ -66,7 +71,10 @@ ForceIncluded ForceIncluded ForceIncluded + ForceIncluded + ForceIncluded ForceIncluded + ForceIncluded ForceIncluded ForceIncluded ForceIncluded