Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a4b39aa9 | |||
| 8bf665d1f1 | |||
| 7a77af2d80 | |||
| 830cb38d9f | |||
| 8f66bf9f09 | |||
| 755fa51adc | |||
| a15d187550 | |||
| 6e6ebffb62 | |||
| c2ab550329 | |||
| ff31412719 | |||
| 15e4e3fbd7 | |||
| 5306a86d13 | |||
| c304ac94fe | |||
| e0a16f7fb6 | |||
| f7cec5d093 | |||
|
|
0c6bbaadac | ||
| d7f775e80c |
2
.github/workflows/publish_manifest.yml
vendored
2
.github/workflows/publish_manifest.yml
vendored
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3.2.0
|
uses: actions/setup-dotnet@v3.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Set version
|
- name: Set version
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- name: Setup .NET Core
|
- name: Setup .NET Core
|
||||||
uses: actions/setup-dotnet@v3.2.0
|
uses: actions/setup-dotnet@v3.2.0
|
||||||
with:
|
with:
|
||||||
dotnet-version: 9.0.x
|
dotnet-version: 10.0.x
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Create build
|
- name: Create build
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@ obj/
|
|||||||
riderModule.iml
|
riderModule.iml
|
||||||
/_ReSharper.Caches/
|
/_ReSharper.Caches/
|
||||||
release/
|
release/
|
||||||
publish/
|
publish/
|
||||||
|
/.vs
|
||||||
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
@@ -2,5 +2,6 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="VcsDirectoryMappings">
|
<component name="VcsDirectoryMappings">
|
||||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
<mapping directory="$PROJECT_DIR$/Robust.LoaderApi" vcs="Git" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
6
Directory.Build.props
Normal file
6
Directory.Build.props
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
33
Directory.Packages.props
Normal file
33
Directory.Packages.props
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<Project>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageVersion Include="Avalonia" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Desktop" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.11" />
|
||||||
|
<PackageVersion Include="Avalonia.Svg.Skia" Version="11.3.0" />
|
||||||
|
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" />
|
||||||
|
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
|
||||||
|
<PackageVersion Include="Fluent.Net" Version="1.0.63" />
|
||||||
|
<PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
|
||||||
|
<PackageVersion Include="libsodium" Version="1.0.20" />
|
||||||
|
<PackageVersion Include="Robust.Natives" Version="0.2.3" />
|
||||||
|
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
|
||||||
|
<PackageVersion Include="Lib.Harmony" Version="2.4.2" />
|
||||||
|
<PackageVersion Include="SharpZstd.Interop" Version="1.5.6" />
|
||||||
|
<PackageVersion Include="coverlet.collector" Version="6.0.4">
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
</PackageVersion>
|
||||||
|
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
|
<PackageVersion Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageVersion Include="NUnit" Version="3.14.0" />
|
||||||
|
<PackageVersion Include="NUnit.Analyzers" Version="3.9.0" />
|
||||||
|
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
|
||||||
|
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -70,3 +70,5 @@ popup-login-credentials-warning-cancel = Cancel
|
|||||||
popup-login-credentials-warning-proceed = Proceed
|
popup-login-credentials-warning-proceed = Proceed
|
||||||
|
|
||||||
goto-path-home = Root folder
|
goto-path-home = Root folder
|
||||||
|
tab-favorite = Favorite
|
||||||
|
server-list-loading = Loading server list.. Please wait
|
||||||
@@ -69,4 +69,6 @@ popup-login-credentials-warning-go-auth = Перейти на страницу
|
|||||||
popup-login-credentials-warning-cancel = Отмена
|
popup-login-credentials-warning-cancel = Отмена
|
||||||
popup-login-credentials-warning-proceed = Продолжить
|
popup-login-credentials-warning-proceed = Продолжить
|
||||||
|
|
||||||
goto-path-home = Корн. папка
|
goto-path-home = Корн. папка
|
||||||
|
tab-favorite = Избранное
|
||||||
|
server-list-loading = Загрузка списка серверов. Пожалуйста, подождите...
|
||||||
@@ -14,7 +14,7 @@ public class LocalizedLabel : Label
|
|||||||
set
|
set
|
||||||
{
|
{
|
||||||
SetValue(LocalIdProperty, value);
|
SetValue(LocalIdProperty, value);
|
||||||
Content = LocalisationService.GetString(value);
|
Content = LocalizationService.GetString(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,118 +0,0 @@
|
|||||||
using Avalonia.Controls;
|
|
||||||
using Nebula.Launcher.Models;
|
|
||||||
using Nebula.Launcher.ServerListProviders;
|
|
||||||
using Nebula.Launcher.ViewModels;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Controls;
|
|
||||||
|
|
||||||
public partial class ServerListView : UserControl
|
|
||||||
{
|
|
||||||
private IServerListProvider _provider = default!;
|
|
||||||
private ServerFilter? _currentFilter;
|
|
||||||
|
|
||||||
public bool IsLoading { get; private set; }
|
|
||||||
|
|
||||||
public ServerListView()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ServerListView TakeFrom(IServerListProvider provider)
|
|
||||||
{
|
|
||||||
var serverListView = new ServerListView();
|
|
||||||
if (provider is IServerListDirtyInvoker invoker)
|
|
||||||
{
|
|
||||||
invoker.Dirty += serverListView.OnDirty;
|
|
||||||
}
|
|
||||||
serverListView._provider = provider;
|
|
||||||
serverListView.RefreshFromProvider();
|
|
||||||
return serverListView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RefreshFromProvider()
|
|
||||||
{
|
|
||||||
if (IsLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
Clear();
|
|
||||||
StartLoading();
|
|
||||||
|
|
||||||
_provider.LoadServerList();
|
|
||||||
|
|
||||||
if (_provider.IsLoaded) PasteServersFromList();
|
|
||||||
else _provider.OnLoaded += RefreshRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RequireStatusUpdate()
|
|
||||||
{
|
|
||||||
foreach (var rawView in ServerList.Items)
|
|
||||||
{
|
|
||||||
if (rawView is ServerEntryModelView serverEntryModelView)
|
|
||||||
{
|
|
||||||
//serverEntryModelView.UpdateStatusIfNecessary();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ApplyFilter(ServerFilter? filter)
|
|
||||||
{
|
|
||||||
_currentFilter = filter;
|
|
||||||
|
|
||||||
if(IsLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var serverView in ServerList.Items)
|
|
||||||
{
|
|
||||||
if(serverView is IFilterConsumer filterConsumer)
|
|
||||||
filterConsumer.ProcessFilter(filter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDirty()
|
|
||||||
{
|
|
||||||
RefreshFromProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Clear()
|
|
||||||
{
|
|
||||||
ErrorList.Items.Clear();
|
|
||||||
ServerList.Items.Clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PasteServersFromList()
|
|
||||||
{
|
|
||||||
foreach (var serverEntry in _provider.GetServers())
|
|
||||||
{
|
|
||||||
ServerList.Items.Add(serverEntry);
|
|
||||||
if(serverEntry is IFilterConsumer serverFilter)
|
|
||||||
serverFilter.ProcessFilter(_currentFilter);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var error in _provider.GetErrors())
|
|
||||||
{
|
|
||||||
ErrorList.Items.Add(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshRequired()
|
|
||||||
{
|
|
||||||
PasteServersFromList();
|
|
||||||
_provider.OnLoaded -= RefreshRequired;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void StartLoading()
|
|
||||||
{
|
|
||||||
Clear();
|
|
||||||
IsLoading = true;
|
|
||||||
LoadingLabel.IsVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EndLoading()
|
|
||||||
{
|
|
||||||
IsLoading = false;
|
|
||||||
LoadingLabel.IsVisible = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
157
Nebula.Launcher/Controls/SimpleGraph.cs
Normal file
157
Nebula.Launcher/Controls/SimpleGraph.cs
Normal file
@@ -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<ObservableCollection<double>> ValuesProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph,ObservableCollection<double>>(nameof(Values));
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> GraphBrushProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph, IBrush>(nameof(GraphBrush), Brushes.CornflowerBlue);
|
||||||
|
|
||||||
|
public static readonly StyledProperty<IBrush> GridBrushProperty =
|
||||||
|
AvaloniaProperty.Register<SimpleGraph, IBrush>(nameof(GridBrush), Brushes.LightGray);
|
||||||
|
|
||||||
|
static SimpleGraph()
|
||||||
|
{
|
||||||
|
ValuesProperty.Changed.Subscribe(
|
||||||
|
new AnonymousObserver<AvaloniaPropertyChangedEventArgs<ObservableCollection<double>>>(args =>
|
||||||
|
{
|
||||||
|
if (args.Sender is not SimpleGraph g)
|
||||||
|
return;
|
||||||
|
|
||||||
|
g.InvalidateVisual();
|
||||||
|
g.Values.CollectionChanged += g.ValuesOnCollectionChanged;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleGraph()
|
||||||
|
{
|
||||||
|
Values = new ObservableCollection<double>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ValuesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(InvalidateVisual);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<double> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using Avalonia.Data.Converters;
|
using Avalonia.Data.Converters;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Color = System.Drawing.Color;
|
using Color = System.Drawing.Color;
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ public static class LauncherConVar
|
|||||||
new AuthServerCredentials(
|
new AuthServerCredentials(
|
||||||
"WizDen",
|
"WizDen",
|
||||||
[
|
[
|
||||||
"https://harpy.durenko.tatar/auth-api/",
|
"https://feline.durenko.tatar/auth-api/",
|
||||||
"https://auth.spacestation14.com/",
|
"https://auth.spacestation14.com/",
|
||||||
"https://auth.fallback.spacestation14.com/",
|
"https://auth.fallback.spacestation14.com/",
|
||||||
]),
|
]),
|
||||||
@@ -48,11 +48,13 @@ public static class LauncherConVar
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
|
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
|
||||||
new ServerHubRecord("WizDen", "https://harpy.durenko.tatar/hub-api/api/servers"),
|
new ServerHubRecord("WizDen", "https://feline.durenko.tatar/hub-api/api/servers"),
|
||||||
new ServerHubRecord("AltHub","https://hub.singularity14.co.uk/api/servers")
|
new ServerHubRecord("AltHub","https://hub.singularity14.co.uk/api/servers")
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", CultureInfo.CurrentCulture.Name);
|
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", CultureInfo.CurrentCulture.Name);
|
||||||
public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url",
|
public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url",
|
||||||
"https://github.com/icsharpcode/ILSpy/releases/download/v9.0/ILSpy_binaries_9.0.0.7889-x64.zip");
|
"https://feline.durenko.tatar/ILSpy_selfcontained_10.0.0.8330-x64.zip");
|
||||||
|
|
||||||
|
public static readonly ConVar<string> ILSpyVersion = ConVarBuilder.Build<string>("dotnet.version", "10");
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using Nebula.Launcher.ProcessHelper;
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
@@ -6,27 +9,59 @@ namespace Nebula.Launcher.Models;
|
|||||||
|
|
||||||
public sealed class ContentLogConsumer : IProcessLogConsumer
|
public sealed class ContentLogConsumer : IProcessLogConsumer
|
||||||
{
|
{
|
||||||
private readonly LogPopupModelView _currLog;
|
|
||||||
private readonly PopupMessageService _popupMessageService;
|
private readonly PopupMessageService _popupMessageService;
|
||||||
|
private readonly List<string> _outMessages = [];
|
||||||
|
|
||||||
|
private LogPopupModelView? _currentLogPopup;
|
||||||
|
|
||||||
|
public int MaxMessages { get; set; } = 100;
|
||||||
|
|
||||||
public ContentLogConsumer(LogPopupModelView currLog, PopupMessageService popupMessageService)
|
public ContentLogConsumer(PopupMessageService popupMessageService)
|
||||||
{
|
{
|
||||||
_currLog = currLog;
|
|
||||||
_popupMessageService = popupMessageService;
|
_popupMessageService = popupMessageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Popup()
|
||||||
|
{
|
||||||
|
if(_currentLogPopup is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentLogPopup = new LogPopupModelView(_popupMessageService);
|
||||||
|
_currentLogPopup.OnDisposing += OnLogPopupDisposing;
|
||||||
|
|
||||||
|
foreach (var message in _outMessages.ToArray())
|
||||||
|
{
|
||||||
|
_currentLogPopup.Append(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_popupMessageService.Popup(_currentLogPopup);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogPopupDisposing(PopupViewModelBase obj)
|
||||||
|
{
|
||||||
|
if(_currentLogPopup == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_currentLogPopup.OnDisposing -= OnLogPopupDisposing;
|
||||||
|
_currentLogPopup = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Out(string text)
|
public void Out(string text)
|
||||||
{
|
{
|
||||||
_currLog.Append(text);
|
_outMessages.Add(text);
|
||||||
|
if(_outMessages.Count >= MaxMessages)
|
||||||
|
_outMessages.RemoveAt(0);
|
||||||
|
|
||||||
|
_currentLogPopup?.Append(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Error(string text)
|
public void Error(string text)
|
||||||
{
|
{
|
||||||
_currLog.Append(text);
|
Out(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Fatal(string text)
|
public void Fatal(string text)
|
||||||
{
|
{
|
||||||
_popupMessageService.Popup("Fatal error while stop instance:" + text);
|
_popupMessageService.Popup(new ExceptionCompound("Error while running program", text));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace Nebula.Launcher.Models;
|
||||||
|
|
||||||
|
public interface IRunningSignalConsumer
|
||||||
|
{
|
||||||
|
public void ProcessRunningSignal(bool isRunning);
|
||||||
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||||
@@ -15,38 +14,24 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.3.0"/>
|
<PackageReference Include="AsyncImageLoader.Avalonia"/>
|
||||||
<PackageReference Include="Avalonia" Version="11.2.1"/>
|
<PackageReference Include="Avalonia"/>
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Desktop"/>
|
||||||
<PackageReference Include="Avalonia.Svg.Skia" Version="11.2.0.2" />
|
<PackageReference Include="Avalonia.Svg.Skia"/>
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Themes.Fluent"/>
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1"/>
|
<PackageReference Include="Avalonia.Fonts.Inter"/>
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
|
<PackageReference Include="Avalonia.Diagnostics">
|
||||||
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
|
||||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1"/>
|
<PackageReference Include="CommunityToolkit.Mvvm"/>
|
||||||
<PackageReference Include="Fluent.Net" Version="1.0.63" />
|
<PackageReference Include="Fluent.Net"/>
|
||||||
<PackageReference Include="JetBrains.Annotations" Version="2024.3.0"/>
|
<PackageReference Include="JetBrains.Annotations"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
|
||||||
<PackageReference Include="libsodium" Version="1.0.20"/>
|
<PackageReference Include="libsodium"/>
|
||||||
<PackageReference Include="Robust.Natives" Version="0.2.3" />
|
<PackageReference Include="Robust.Natives"/>
|
||||||
</ItemGroup>
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater"/>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Views\Tabs\ServerListTab.axaml.cs">
|
|
||||||
<DependentUpon>ServerListTab.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Views\Popup\AddFavoriteView.axaml.cs">
|
|
||||||
<DependentUpon>AddFavoriteView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Controls\ServerListView.axaml.cs">
|
|
||||||
<DependentUpon>ServerListView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<Target Name="BuildCheck" AfterTargets="AfterBuild">
|
<Target Name="BuildCheck" AfterTargets="AfterBuild">
|
||||||
@@ -77,9 +62,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
|
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
|
||||||
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\Nebula.Runner\Nebula.Runner.csproj"
|
||||||
|
ReferenceOutputAssembly="false" />
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="Controls\ServerListView.axaml" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
@@ -9,11 +10,11 @@ public abstract class DotnetProcessStartInfoProviderBase(DotnetResolverService r
|
|||||||
{
|
{
|
||||||
protected abstract string GetDllPath();
|
protected abstract string GetDllPath();
|
||||||
|
|
||||||
public virtual async Task<ProcessStartInfo> GetProcessStartInfo()
|
public virtual async Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return new ProcessStartInfo
|
return new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = await resolverService.EnsureDotnet(),
|
FileName = await resolverService.EnsureDotnet(cancellationToken),
|
||||||
Arguments = GetDllPath(),
|
Arguments = GetDllPath(),
|
||||||
CreateNoWindow = true,
|
CreateNoWindow = true,
|
||||||
UseShellExecute = false,
|
UseShellExecute = false,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
@@ -31,9 +32,9 @@ public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverS
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<ProcessStartInfo> GetProcessStartInfo()
|
public override async Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var baseStart = await base.GetProcessStartInfo();
|
var baseStart = await base.GetProcessStartInfo(cancellationToken);
|
||||||
|
|
||||||
var authProv = accountInfoViewModel.Credentials.Value;
|
var authProv = accountInfoViewModel.Credentials.Value;
|
||||||
if(authProv is null)
|
if(authProv is null)
|
||||||
|
|||||||
@@ -5,29 +5,41 @@ using Microsoft.Extensions.DependencyInjection;
|
|||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ProcessHelper;
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService contentService, EngineService engineService)
|
public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService contentService, EngineService engineService)
|
||||||
{
|
{
|
||||||
public async Task<ProcessRunHandler<GameProcessStartInfoProvider>> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandler loadingHandler, CancellationToken cancellationToken = default)
|
public async Task<GameProcessStartInfoProvider> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var buildInfo = await contentService.GetBuildInfo(address, cancellationToken);
|
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)
|
if (engine is null)
|
||||||
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
|
||||||
await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken);
|
var hashApi = await contentService.EnsureItems(buildInfo, loadingHandlerFactory, cancellationToken);
|
||||||
await engineService.EnsureEngineModules("Robust.Client.WebView", buildInfo.BuildInfo.Build.EngineVersion);
|
|
||||||
|
if (hashApi.TryOpen("manifest.yml", out var stream))
|
||||||
|
{
|
||||||
|
var modules = ContentManifestParser.ExtractModules(stream);
|
||||||
|
|
||||||
var gameInfo =
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
provider.GetService<GameProcessStartInfoProvider>()!.WithBuildInfo(buildInfo.BuildInfo.Auth.PublicKey,
|
provider.GetService<GameProcessStartInfoProvider>()!.WithBuildInfo(buildInfo.BuildInfo.Auth.PublicKey,
|
||||||
address);
|
address);
|
||||||
var gameProcessRunHandler = new ProcessRunHandler<GameProcessStartInfoProvider>(gameInfo);
|
|
||||||
|
|
||||||
return gameProcessRunHandler;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ProcessHelper;
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
public interface IProcessStartInfoProvider
|
public interface IProcessStartInfoProvider
|
||||||
{
|
{
|
||||||
public Task<ProcessStartInfo> GetProcessStartInfo();
|
public Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default);
|
||||||
}
|
}
|
||||||
@@ -1,55 +1,47 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ProcessHelper;
|
namespace Nebula.Launcher.ProcessHelper;
|
||||||
|
|
||||||
public class ProcessRunHandler<T> : IProcessConsumerCollection, IDisposable where T: IProcessStartInfoProvider
|
public class ProcessRunHandler : IDisposable
|
||||||
{
|
{
|
||||||
private ProcessStartInfo? _processInfo;
|
|
||||||
private Task<ProcessStartInfo>? _processInfoTask;
|
|
||||||
|
|
||||||
private Process? _process;
|
private Process? _process;
|
||||||
private ProcessLogConsumerCollection _consumerCollection = new();
|
private readonly IProcessLogConsumer _logConsumer;
|
||||||
|
|
||||||
private string _lastError = string.Empty;
|
private StringBuilder _lastErrorBuilder = new StringBuilder();
|
||||||
private readonly T _currentProcessStartInfoProvider;
|
|
||||||
|
|
||||||
public T GetCurrentProcessStartInfo() => _currentProcessStartInfoProvider;
|
public bool IsRunning => _process is not null;
|
||||||
public bool IsRunning => _processInfo is not null;
|
public Action<ProcessRunHandler>? OnProcessExited;
|
||||||
public Action<ProcessRunHandler<T>>? OnProcessExited;
|
|
||||||
|
|
||||||
public void RegisterLogger(IProcessLogConsumer consumer)
|
public AsyncValueCache<ProcessStartInfo> ProcessStartInfoProvider { get; }
|
||||||
{
|
|
||||||
_consumerCollection.RegisterLogger(consumer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProcessRunHandler(T processStartInfoProvider)
|
public bool Disposed { get; private set; }
|
||||||
|
|
||||||
|
public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer)
|
||||||
{
|
{
|
||||||
_currentProcessStartInfoProvider = processStartInfoProvider;
|
_logConsumer = logConsumer;
|
||||||
_processInfoTask = _currentProcessStartInfoProvider.GetProcessStartInfo();
|
|
||||||
_processInfoTask.GetAwaiter().OnCompleted(OnInfoProvided);
|
ProcessStartInfoProvider = new AsyncValueCache<ProcessStartInfo>(processStartInfoProvider.GetProcessStartInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInfoProvided()
|
private void CheckIfDisposed()
|
||||||
{
|
{
|
||||||
if (_processInfoTask == null)
|
if (!Disposed) return;
|
||||||
return;
|
throw new ObjectDisposedException(nameof(ProcessRunHandler));
|
||||||
|
|
||||||
_processInfo = _processInfoTask.GetAwaiter().GetResult();
|
|
||||||
_processInfoTask = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Start()
|
public void Start()
|
||||||
{
|
{
|
||||||
if (_processInfoTask != null)
|
CheckIfDisposed();
|
||||||
{
|
if(_process is not null)
|
||||||
_processInfoTask.Wait();
|
throw new InvalidOperationException("Already running");
|
||||||
}
|
|
||||||
|
|
||||||
_process = Process.Start(_processInfo!);
|
_process = Process.Start(ProcessStartInfoProvider.GetValue());
|
||||||
|
|
||||||
if (_process is null) return;
|
if (_process is null) return;
|
||||||
|
|
||||||
@@ -66,7 +58,8 @@ public class ProcessRunHandler<T> : IProcessConsumerCollection, IDisposable wher
|
|||||||
|
|
||||||
public void Stop()
|
public void Stop()
|
||||||
{
|
{
|
||||||
_process?.CloseMainWindow();
|
CheckIfDisposed();
|
||||||
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnExited(object? sender, EventArgs e)
|
private void OnExited(object? sender, EventArgs e)
|
||||||
@@ -77,37 +70,48 @@ public class ProcessRunHandler<T> : IProcessConsumerCollection, IDisposable wher
|
|||||||
_process.ErrorDataReceived -= OnErrorDataReceived;
|
_process.ErrorDataReceived -= OnErrorDataReceived;
|
||||||
_process.Exited -= OnExited;
|
_process.Exited -= OnExited;
|
||||||
|
|
||||||
|
|
||||||
if (_process.ExitCode != 0)
|
if (_process.ExitCode != 0)
|
||||||
_consumerCollection.Fatal(_lastError);
|
_logConsumer.Fatal(_lastErrorBuilder.ToString());
|
||||||
|
|
||||||
_process.Dispose();
|
_process.Dispose();
|
||||||
_process = null;
|
_process = null;
|
||||||
|
|
||||||
OnProcessExited?.Invoke(this);
|
OnProcessExited?.Invoke(this);
|
||||||
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
|
private void OnErrorDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Data != null)
|
if (e.Data == null) return;
|
||||||
{
|
|
||||||
_lastError = e.Data;
|
if (!e.Data.StartsWith(" "))
|
||||||
_consumerCollection.Error(e.Data);
|
_lastErrorBuilder.Clear();
|
||||||
}
|
|
||||||
|
_lastErrorBuilder.AppendLine(e.Data);
|
||||||
|
_logConsumer.Error(e.Data);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
|
private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Data != null)
|
if (e.Data != null)
|
||||||
{
|
{
|
||||||
_consumerCollection.Out(e.Data);
|
_logConsumer.Out(e.Data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
_processInfoTask?.Dispose();
|
if (_process is not null)
|
||||||
_process?.Dispose();
|
{
|
||||||
|
_process.CloseMainWindow();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProcessStartInfoProvider.Invalidate();
|
||||||
|
|
||||||
|
CheckIfDisposed();
|
||||||
|
|
||||||
|
Disposed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,4 +138,76 @@ public sealed class DebugLoggerBridge : IProcessLogConsumer
|
|||||||
{
|
{
|
||||||
_logger.Log(LoggerCategory.Error, text);
|
_logger.Log(LoggerCategory.Error, text);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class AsyncValueCache<T>
|
||||||
|
{
|
||||||
|
private readonly Func<CancellationToken, Task<T>> _valueFactory;
|
||||||
|
private readonly SemaphoreSlim _semaphore = new(1, 1);
|
||||||
|
private readonly CancellationTokenSource _cacheCts = new();
|
||||||
|
|
||||||
|
private Lazy<Task<T>> _lazyTask = null!;
|
||||||
|
private T _cachedValue = default!;
|
||||||
|
private bool _isCacheValid;
|
||||||
|
|
||||||
|
public AsyncValueCache(Func<CancellationToken, Task<T>> valueFactory)
|
||||||
|
{
|
||||||
|
_valueFactory = valueFactory ?? throw new ArgumentNullException(nameof(valueFactory));
|
||||||
|
ResetLazyTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public T GetValue()
|
||||||
|
{
|
||||||
|
if (_isCacheValid) return _cachedValue;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
if (_isCacheValid) return _cachedValue;
|
||||||
|
|
||||||
|
_cachedValue = _lazyTask.Value
|
||||||
|
.ConfigureAwait(false)
|
||||||
|
.GetAwaiter()
|
||||||
|
.GetResult();
|
||||||
|
|
||||||
|
_isCacheValid = true;
|
||||||
|
return _cachedValue;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Invalidate()
|
||||||
|
{
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_semaphore.Wait();
|
||||||
|
_isCacheValid = false;
|
||||||
|
_cacheCts.Cancel();
|
||||||
|
_cacheCts.Dispose();
|
||||||
|
ResetLazyTask();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetLazyTask()
|
||||||
|
{
|
||||||
|
_lazyTask = new Lazy<Task<T>>(() =>
|
||||||
|
_valueFactory(_cacheCts.Token)
|
||||||
|
.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.IsCanceled || t.IsFaulted)
|
||||||
|
{
|
||||||
|
_isCacheValid = false;
|
||||||
|
throw t.Exception ?? new Exception();
|
||||||
|
}
|
||||||
|
return t.Result;
|
||||||
|
}, TaskContinuationOptions.ExecuteSynchronously));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
@@ -17,49 +19,27 @@ using Nebula.Shared.Utils;
|
|||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
[ServiceRegister, ConstructGenerator]
|
[ServiceRegister, ConstructGenerator]
|
||||||
public sealed partial class FavoriteServerListProvider : IServerListProvider, IServerListDirtyInvoker
|
public sealed partial class FavoriteServerListProvider : IServerListProvider
|
||||||
{
|
{
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
|
||||||
private List<IListEntryModelView> _serverLists = [];
|
public Action? OnRefreshRequired;
|
||||||
private string[] rawServerLists = [];
|
|
||||||
|
|
||||||
public bool IsLoaded { get; private set; }
|
private string[] _rawServerLists = [];
|
||||||
public Action? OnLoaded { get; set; }
|
|
||||||
public Action? Dirty { get; set; }
|
|
||||||
public IEnumerable<IListEntryModelView> GetServers()
|
|
||||||
{
|
|
||||||
return _serverLists;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Exception> GetErrors()
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadServerList()
|
|
||||||
{
|
|
||||||
IsLoaded = false;
|
|
||||||
_serverLists.Clear();
|
|
||||||
var servers = GetFavoriteEntries();
|
|
||||||
|
|
||||||
var serverEntries = servers.Select(s =>
|
|
||||||
ServerViewContainer.Get(s.ToRobustUrl())
|
|
||||||
);
|
|
||||||
|
|
||||||
_serverLists.AddRange(serverEntries);
|
|
||||||
|
|
||||||
_serverLists.Add(new AddFavoriteButton(ServiceProvider));
|
|
||||||
|
|
||||||
IsLoaded = true;
|
|
||||||
OnLoaded?.Invoke();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddFavorite(ServerEntryModelView entryModelView)
|
public void LoadServerList(
|
||||||
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
AddFavorite(entryModelView.Address);
|
foreach (var server in _rawServerLists)
|
||||||
|
{
|
||||||
|
var container = ServerViewContainer.Get(server);
|
||||||
|
servers.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
servers.Add(new AddFavoriteButton(ServiceProvider));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AddFavorite(RobustUrl robustUrl)
|
public void AddFavorite(RobustUrl robustUrl)
|
||||||
@@ -70,10 +50,10 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
|
|||||||
if(ServerViewContainer.Get(robustUrl) is IFavoriteEntryModelView favoriteView) favoriteView.IsFavorite = true;
|
if(ServerViewContainer.Get(robustUrl) is IFavoriteEntryModelView favoriteView) favoriteView.IsFavorite = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveFavorite(ServerEntryModelView entryModelView)
|
public void RemoveFavorite(ServerEntryViewModel entryViewModel)
|
||||||
{
|
{
|
||||||
var servers = GetFavoriteEntries();
|
var servers = GetFavoriteEntries();
|
||||||
servers.Remove(entryModelView.Address.ToString());
|
servers.Remove(entryViewModel.Address.ToString());
|
||||||
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +66,7 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
|
|||||||
|
|
||||||
private List<string> GetFavoriteEntries()
|
private List<string> GetFavoriteEntries()
|
||||||
{
|
{
|
||||||
return rawServerLists.ToList();
|
return _rawServerLists.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise()
|
private void Initialise()
|
||||||
@@ -98,21 +78,20 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
|
|||||||
{
|
{
|
||||||
if (value == null)
|
if (value == null)
|
||||||
{
|
{
|
||||||
rawServerLists = [];
|
_rawServerLists = [];
|
||||||
Dirty?.Invoke();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawServerLists = value;
|
_rawServerLists = value;
|
||||||
Dirty?.Invoke();
|
OnRefreshRequired?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class AddFavoriteButton: Border, IListEntryModelView{
|
public sealed class AddFavoriteButton: Border, IListEntryModelView{
|
||||||
|
|
||||||
private Button _addFavoriteButton = new Button();
|
private readonly Button _addFavoriteButton = new();
|
||||||
public AddFavoriteButton(IServiceProvider serviceProvider)
|
public AddFavoriteButton(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
Margin = new Thickness(5, 5, 5, 20);
|
Margin = new Thickness(5, 5, 5, 20);
|
||||||
@@ -127,5 +106,5 @@ public class AddFavoriteButton: Border, IListEntryModelView{
|
|||||||
_addFavoriteButton.Content = "Add Favorite";
|
_addFavoriteButton.Content = "Add Favorite";
|
||||||
Child = _addFavoriteButton;
|
Child = _addFavoriteButton;
|
||||||
}
|
}
|
||||||
public bool IsFavorite { get; set; }
|
public void Dispose(){}
|
||||||
}
|
}
|
||||||
@@ -1,86 +1,125 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Avalonia.Collections;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Utils;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
[ServiceRegister(null, false), ConstructGenerator]
|
[ServiceRegister(null, false), ConstructGenerator]
|
||||||
public sealed partial class HubServerListProvider : IServerListProvider
|
public sealed partial class HubServerListProvider : IServerListProvider, IDisposable
|
||||||
{
|
{
|
||||||
|
private CancellationTokenSource? _cts;
|
||||||
|
private readonly SemaphoreSlim _loadLock = new(1, 1);
|
||||||
|
|
||||||
[GenerateProperty] private RestService RestService { get; }
|
[GenerateProperty] private RestService RestService { get; }
|
||||||
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
|
||||||
public string HubUrl { get; set; }
|
|
||||||
|
|
||||||
public bool IsLoaded { get; private set; }
|
|
||||||
public Action? OnLoaded { get; set; }
|
|
||||||
|
|
||||||
private CancellationTokenSource? _cts;
|
private string _hubUrl;
|
||||||
private readonly List<IListEntryModelView> _servers = [];
|
|
||||||
private readonly List<Exception> _errors = [];
|
|
||||||
|
|
||||||
public HubServerListProvider With(string hubUrl)
|
public HubServerListProvider With(string hubUrl)
|
||||||
{
|
{
|
||||||
HubUrl = hubUrl;
|
_hubUrl = hubUrl;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<IListEntryModelView> GetServers()
|
public void LoadServerList(
|
||||||
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
return _servers;
|
servers.Add(new LoadingServerEntry());
|
||||||
|
Task.Run(() => LoadServerListAsync(servers, exceptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IEnumerable<Exception> GetErrors()
|
private void SyncServers(List<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<IListEntryModelView> collection)
|
||||||
{
|
{
|
||||||
return _errors;
|
collection.Clear();
|
||||||
|
collection.AddRange(servers);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void LoadServerList()
|
private async Task LoadServerListAsync(
|
||||||
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
{
|
||||||
if (_cts != null)
|
CancellationTokenSource localCts;
|
||||||
{
|
|
||||||
await _cts.CancelAsync();
|
|
||||||
_cts = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
_servers.Clear();
|
await _loadLock.WaitAsync();
|
||||||
_errors.Clear();
|
try
|
||||||
IsLoaded = false;
|
{
|
||||||
_cts = new CancellationTokenSource();
|
_cts?.Cancel();
|
||||||
|
_cts?.Dispose();
|
||||||
|
|
||||||
|
_cts = new CancellationTokenSource();
|
||||||
|
localCts = _cts;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loadLock.Release();
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var servers =
|
var serversRaw = await RestService.GetAsync<List<ServerHubInfo>>(
|
||||||
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(HubUrl), _cts.Token);
|
new Uri(_hubUrl),
|
||||||
|
localCts.Token
|
||||||
servers.Sort(new ServerComparer());
|
|
||||||
|
|
||||||
if(_cts.Token.IsCancellationRequested) return;
|
|
||||||
|
|
||||||
_servers.AddRange(
|
|
||||||
servers.Select(h=>
|
|
||||||
ServerViewContainer.Get(h.Address.ToRobustUrl(), h.StatusData)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
serversRaw.Sort(new ServerComparer());
|
||||||
|
|
||||||
|
localCts.Token.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
Dispatcher.UIThread.Invoke(() =>
|
||||||
|
{
|
||||||
|
var serverList = new List<IListEntryModelView>();
|
||||||
|
|
||||||
|
foreach (var info in serversRaw)
|
||||||
|
{
|
||||||
|
serverList.Add(ServerViewContainer.Get(info.Address, info.StatusData));
|
||||||
|
}
|
||||||
|
SyncServers(serverList, servers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Ignore cancel think
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_errors.Add(new Exception($"Some error while loading server list from {HubUrl}. See inner exception", e));
|
Console.WriteLine(e);
|
||||||
_errors.Add(e);
|
exceptions.Add(
|
||||||
|
new Exception(
|
||||||
|
$"Some error while loading server list from {_hubUrl}. See inner exception",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoaded = true;
|
|
||||||
OnLoaded?.Invoke();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Initialise(){}
|
private void Initialise(){}
|
||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_cts?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LoadingServerEntry : Label, IListEntryModelView
|
||||||
|
{
|
||||||
|
public LoadingServerEntry()
|
||||||
|
{
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
Content = LocalizationService.GetString("server-list-loading");
|
||||||
|
}
|
||||||
|
public void Dispose()
|
||||||
|
{}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using Avalonia.Collections;
|
||||||
using Nebula.Launcher.ViewModels;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ServerListProviders;
|
namespace Nebula.Launcher.ServerListProviders;
|
||||||
|
|
||||||
public interface IServerListProvider
|
public interface IServerListProvider
|
||||||
{
|
{
|
||||||
public bool IsLoaded { get; }
|
public void LoadServerList(
|
||||||
public Action? OnLoaded { get; set; }
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
|
AvaloniaList<Exception> exceptions);
|
||||||
public IEnumerable<IListEntryModelView> GetServers();
|
|
||||||
public IEnumerable<Exception> GetErrors();
|
|
||||||
|
|
||||||
public void LoadServerList();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface IServerListDirtyInvoker
|
|
||||||
{
|
|
||||||
public Action? Dirty { get; set; }
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.ObjectModel;
|
||||||
using Nebula.Launcher.Controls;
|
using Avalonia.Collections;
|
||||||
using Nebula.Launcher.ViewModels;
|
using Nebula.Launcher.ViewModels;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -8,20 +8,14 @@ namespace Nebula.Launcher.ServerListProviders;
|
|||||||
|
|
||||||
public sealed class TestServerList : IServerListProvider
|
public sealed class TestServerList : IServerListProvider
|
||||||
{
|
{
|
||||||
public bool IsLoaded => true;
|
public void LoadServerList(
|
||||||
public Action? OnLoaded { get; set; }
|
AvaloniaList<IListEntryModelView> servers,
|
||||||
public IEnumerable<IListEntryModelView> GetServers()
|
AvaloniaList<Exception> exceptions)
|
||||||
{
|
|
||||||
return [new ServerEntryModelView(),new ServerEntryModelView()];
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<Exception> GetErrors()
|
|
||||||
{
|
|
||||||
return [new Exception("On no!")];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void LoadServerList()
|
|
||||||
{
|
{
|
||||||
|
|
||||||
|
//servers.Add(new ServerEntryViewModel());
|
||||||
|
//servers.Add(new ServerEntryViewModel());
|
||||||
|
|
||||||
|
exceptions.Add(new Exception("Oh no!"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,10 @@ using System.Collections.Generic;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.IO.Pipelines;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
@@ -14,6 +16,8 @@ using Nebula.Shared.FileApis.Interfaces;
|
|||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Services;
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
@@ -25,35 +29,32 @@ public sealed partial class DecompilerService
|
|||||||
[GenerateProperty] private ViewHelperService ViewHelperService {get;}
|
[GenerateProperty] private ViewHelperService ViewHelperService {get;}
|
||||||
[GenerateProperty] private ContentService ContentService {get;}
|
[GenerateProperty] private ContentService ContentService {get;}
|
||||||
[GenerateProperty] private FileService FileService {get;}
|
[GenerateProperty] private FileService FileService {get;}
|
||||||
[GenerateProperty] private CancellationService CancellationService {get;}
|
|
||||||
[GenerateProperty] private EngineService EngineService {get;}
|
[GenerateProperty] private EngineService EngineService {get;}
|
||||||
[GenerateProperty] private DebugService DebugService {get;}
|
[GenerateProperty] private DebugService DebugService {get;}
|
||||||
|
|
||||||
private HttpClient _httpClient = new HttpClient();
|
private readonly HttpClient _httpClient = new();
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
|
|
||||||
private static string fullPath = Path.Join(FileService.RootPath,"ILSpy");
|
private string FullPath => Path.Join(AppDataPath.RootPath, $"ILSpy.{ConfigurationService.GetConfigValue(LauncherConVar.ILSpyVersion)}");
|
||||||
private static string executePath = Path.Join(fullPath, "ILSpy.exe");
|
private string ExecutePath => Path.Join(FullPath, "ILSpy.exe");
|
||||||
|
|
||||||
public async void OpenDecompiler(string arguments){
|
public async void OpenDecompiler(string arguments){
|
||||||
await EnsureILSpy();
|
await EnsureILSpy();
|
||||||
var startInfo = new ProcessStartInfo(){
|
var startInfo = new ProcessStartInfo(){
|
||||||
FileName = executePath,
|
FileName = ExecutePath,
|
||||||
Arguments = arguments
|
Arguments = arguments
|
||||||
};
|
};
|
||||||
Process.Start(startInfo);
|
Process.Start(startInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OpenServerDecompiler(RobustUrl url)
|
public async void OpenServerDecompiler(RobustUrl url, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var myTempDir = FileService.EnsureTempDir(out var tmpDir);
|
var myTempDir = FileService.EnsureTempDir(out var tmpDir);
|
||||||
|
|
||||||
ILoadingHandler loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
using var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
|
||||||
var buildInfo =
|
var buildInfo =
|
||||||
await ContentService.GetBuildInfo(url, CancellationService.Token);
|
await ContentService.GetBuildInfo(url, cancellationToken);
|
||||||
var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion);
|
var engine = await EngineService.EnsureEngine(buildInfo.BuildInfo.Build.EngineVersion, loadingHandler, cancellationToken);
|
||||||
|
|
||||||
if (engine is null)
|
if (engine is null)
|
||||||
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
|
||||||
@@ -63,18 +64,16 @@ public sealed partial class DecompilerService
|
|||||||
myTempDir.Save(file, stream);
|
myTempDir.Save(file, stream);
|
||||||
await stream.DisposeAsync();
|
await stream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
var hashApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, CancellationService.Token);
|
var hashApi = await ContentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
|
||||||
|
|
||||||
foreach (var (file, hash) in hashApi.Manifest)
|
foreach (var file in hashApi.AllFiles)
|
||||||
{
|
{
|
||||||
if(!file.Contains(".dll") || !hashApi.TryOpen(hash, out var stream)) continue;
|
if(!file.Contains(".dll") || !hashApi.TryOpen(file, out var stream)) continue;
|
||||||
myTempDir.Save(Path.GetFileName(file), stream);
|
myTempDir.Save(Path.GetFileName(file), stream);
|
||||||
await stream.DisposeAsync();
|
await stream.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
((IDisposable)loadingHandler).Dispose();
|
|
||||||
|
|
||||||
_logger.Log("File extracted. " + tmpDir);
|
_logger.Log("File extracted. " + tmpDir);
|
||||||
|
|
||||||
OpenDecompiler(string.Join(' ', myTempDir.AllFiles.Select(f=>Path.Join(tmpDir, f))) + " --newinstance");
|
OpenDecompiler(string.Join(' ', myTempDir.AllFiles.Select(f=>Path.Join(tmpDir, f))) + " --newinstance");
|
||||||
@@ -87,18 +86,25 @@ public sealed partial class DecompilerService
|
|||||||
private void InitialiseInDesignMode(){}
|
private void InitialiseInDesignMode(){}
|
||||||
|
|
||||||
private async Task EnsureILSpy(){
|
private async Task EnsureILSpy(){
|
||||||
if(!Directory.Exists(fullPath))
|
if(!Directory.Exists(FullPath))
|
||||||
await Download();
|
await Download();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Download(){
|
private async Task Download(){
|
||||||
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loading.LoadingName = "Download ILSpy";
|
loading.LoadingName = "Download ILSpy";
|
||||||
loading.SetJobsCount(1);
|
var context = loading.CreateLoadingContext();
|
||||||
PopupMessageService.Popup(loading);
|
PopupMessageService.Popup(loading);
|
||||||
using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl));
|
using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl));
|
||||||
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
|
Console.WriteLine(response.StatusCode);
|
||||||
Directory.CreateDirectory(fullPath);
|
context.SetJobsCount(response.Content.Headers.ContentLength ?? 1000);
|
||||||
zipArchive.ExtractToDirectory(fullPath);
|
|
||||||
|
using var stream = await response.Content.ReadAsStreamAsync();
|
||||||
|
using var memoryStream = new MemoryStream();
|
||||||
|
stream.CopyTo(memoryStream, context);
|
||||||
|
|
||||||
|
using var zipArchive = new ZipArchive(memoryStream);
|
||||||
|
Directory.CreateDirectory(FullPath);
|
||||||
|
zipArchive.ExtractToDirectory(FullPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Services.Logging;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public class GameRunnerService
|
||||||
|
{
|
||||||
|
private readonly PopupMessageService _popupMessageService;
|
||||||
|
private readonly ViewHelperService _viewHelperService;
|
||||||
|
private readonly GameRunnerPreparer _gameRunnerPreparer;
|
||||||
|
private readonly InstanceRunningContainer _instanceRunningContainer;
|
||||||
|
private readonly AccountInfoViewModel _accountInfoViewModel;
|
||||||
|
private readonly ServerViewContainer _container;
|
||||||
|
private readonly MainViewModel _mainViewModel;
|
||||||
|
private readonly FavoriteServerListProvider _favoriteServerListProvider;
|
||||||
|
private readonly RestService _restService;
|
||||||
|
private readonly CancellationService _cancellationService;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
|
private readonly Dictionary<InstanceKey, RobustUrl> _robustUrls = new();
|
||||||
|
private readonly Dictionary<RobustUrl, InstanceKey> _robustKeys = new();
|
||||||
|
|
||||||
|
public GameRunnerService(PopupMessageService popupMessageService,
|
||||||
|
DebugService debugService,
|
||||||
|
ViewHelperService viewHelperService,
|
||||||
|
GameRunnerPreparer gameRunnerPreparer,
|
||||||
|
InstanceRunningContainer instanceRunningContainer,
|
||||||
|
AccountInfoViewModel accountInfoViewModel,
|
||||||
|
ServerViewContainer container,
|
||||||
|
MainViewModel mainViewModel,
|
||||||
|
FavoriteServerListProvider favoriteServerListProvider,
|
||||||
|
RestService restService,
|
||||||
|
CancellationService cancellationService)
|
||||||
|
{
|
||||||
|
_popupMessageService = popupMessageService;
|
||||||
|
_viewHelperService = viewHelperService;
|
||||||
|
_gameRunnerPreparer = gameRunnerPreparer;
|
||||||
|
_instanceRunningContainer = instanceRunningContainer;
|
||||||
|
_accountInfoViewModel = accountInfoViewModel;
|
||||||
|
_container = container;
|
||||||
|
_mainViewModel = mainViewModel;
|
||||||
|
_favoriteServerListProvider = favoriteServerListProvider;
|
||||||
|
_restService = restService;
|
||||||
|
_cancellationService = cancellationService;
|
||||||
|
|
||||||
|
_logger = debugService.GetLogger("GameRunnerService");
|
||||||
|
_instanceRunningContainer.IsRunningChanged += IsRunningChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IsRunningChanged(InstanceKey key, bool isRunning)
|
||||||
|
{
|
||||||
|
_logger.Debug($"IsRunningChanged {key}: {isRunning}");
|
||||||
|
if (!_robustUrls.TryGetValue(key, out var robustUrl)) return;
|
||||||
|
|
||||||
|
if (_container.Get(robustUrl) is IRunningSignalConsumer signalConsumer)
|
||||||
|
{
|
||||||
|
_logger.Debug($"IsRunningChanged conf {robustUrl}: {isRunning}");
|
||||||
|
signalConsumer.ProcessRunningSignal(isRunning);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRunning)
|
||||||
|
{
|
||||||
|
_robustKeys.Remove(robustUrl);
|
||||||
|
_robustUrls.Remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInstance(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||||
|
{
|
||||||
|
_instanceRunningContainer.Stop(instanceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReadInstanceLog(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||||
|
{
|
||||||
|
_instanceRunningContainer.Popup(instanceKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenContentViewer(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_mainViewModel.RequirePage<ContentBrowserViewModel>().Go(robustUrl, ContentPath.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddFavorite(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_favoriteServerListProvider.AddFavorite(robustUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveFavorite(RobustUrl robustUrl)
|
||||||
|
{
|
||||||
|
_favoriteServerListProvider.RemoveFavorite(robustUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditName(RobustUrl robustUrl, string? oldName)
|
||||||
|
{
|
||||||
|
var popup = _viewHelperService.GetViewModel<EditServerNameViewModel>();
|
||||||
|
popup.IpInput = robustUrl.ToString();
|
||||||
|
popup.NameInput = oldName ?? string.Empty;
|
||||||
|
_popupMessageService.Popup(popup);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<InstanceKey?> RunInstanceAsync(ServerEntryViewModel serverEntryViewModel, CancellationToken cancellationToken, bool ignoreLoginCredentials = false)
|
||||||
|
{
|
||||||
|
_logger.Log("Running instance..." + serverEntryViewModel.RealName);
|
||||||
|
if (!ignoreLoginCredentials && _accountInfoViewModel.Credentials.Value is null)
|
||||||
|
{
|
||||||
|
var warningContext = _viewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
|
||||||
|
.WithServerEntry(serverEntryViewModel);
|
||||||
|
|
||||||
|
_popupMessageService.Popup(warningContext);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var viewModelLoading = _viewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
|
viewModelLoading.LoadingName = "Loading instance...";
|
||||||
|
|
||||||
|
_popupMessageService.Popup(viewModelLoading);
|
||||||
|
var currProcessStartProvider =
|
||||||
|
await _gameRunnerPreparer.GetGameProcessStartInfoProvider(serverEntryViewModel.Address, viewModelLoading, cancellationToken);
|
||||||
|
_logger.Log("Preparing instance...");
|
||||||
|
var instanceKey = _instanceRunningContainer.RegisterInstance(currProcessStartProvider);
|
||||||
|
_robustUrls.Add(instanceKey, serverEntryViewModel.Address);
|
||||||
|
_robustKeys.Add(serverEntryViewModel.Address, instanceKey);
|
||||||
|
_instanceRunningContainer.Run(instanceKey);
|
||||||
|
_logger.Log($"Starting instance... {instanceKey.Id} " + serverEntryViewModel.RealName);
|
||||||
|
return instanceKey;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var error = new Exception("Error while attempt run instance", e);
|
||||||
|
_logger.Error(error);
|
||||||
|
_popupMessageService.Popup(error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEntryViewModel GetServerEntry(RobustUrl url, string customName, ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
return new ServerEntryViewModel(_restService, _cancellationService, this)
|
||||||
|
.WithData(url, customName, serverStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
91
Nebula.Launcher/Services/InstanceRunningContainer.cs
Normal file
91
Nebula.Launcher/Services/InstanceRunningContainer.cs
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ProcessHelper;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
|
[ServiceRegister]
|
||||||
|
public sealed class InstanceRunningContainer(
|
||||||
|
PopupMessageService popupMessageService,
|
||||||
|
DebugService debugService
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private readonly InstanceKeyPool _keyPool = new();
|
||||||
|
private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new();
|
||||||
|
private readonly Dictionary<InstanceKey, ContentLogConsumer> _contentLoggerCache = new();
|
||||||
|
private readonly Dictionary<ProcessRunHandler, InstanceKey> _keyCache = new();
|
||||||
|
|
||||||
|
public Action<InstanceKey, bool>? IsRunningChanged;
|
||||||
|
|
||||||
|
public InstanceKey RegisterInstance(IProcessStartInfoProvider provider)
|
||||||
|
{
|
||||||
|
var id = _keyPool.Take();
|
||||||
|
|
||||||
|
var currentContentLogConsumer = new ContentLogConsumer(popupMessageService);
|
||||||
|
var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id));
|
||||||
|
var logContainer = new ProcessLogConsumerCollection();
|
||||||
|
logContainer.RegisterLogger(currentContentLogConsumer);
|
||||||
|
logContainer.RegisterLogger(logBridge);
|
||||||
|
|
||||||
|
var handler = new ProcessRunHandler(provider, logContainer);
|
||||||
|
handler.OnProcessExited += OnProcessExited;
|
||||||
|
|
||||||
|
_processCache[id] = handler;
|
||||||
|
_contentLoggerCache[id] = currentContentLogConsumer;
|
||||||
|
_keyCache[handler] = id;
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Popup(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler))
|
||||||
|
return;
|
||||||
|
|
||||||
|
handler.Popup();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Run(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_processCache.TryGetValue(instanceKey, out var process))
|
||||||
|
return;
|
||||||
|
|
||||||
|
process.Start();
|
||||||
|
IsRunningChanged?.Invoke(instanceKey, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Stop(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
if(!_processCache.TryGetValue(instanceKey, out var process))
|
||||||
|
return;
|
||||||
|
|
||||||
|
process.Stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsRunning(InstanceKey instanceKey)
|
||||||
|
{
|
||||||
|
return _processCache.ContainsKey(instanceKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveProcess(ProcessRunHandler handler)
|
||||||
|
{
|
||||||
|
if(handler.Disposed) return;
|
||||||
|
|
||||||
|
var key = _keyCache[handler];
|
||||||
|
IsRunningChanged?.Invoke(key, false);
|
||||||
|
_processCache.Remove(key);
|
||||||
|
_keyCache.Remove(handler);
|
||||||
|
_contentLoggerCache.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnProcessExited(ProcessRunHandler obj)
|
||||||
|
{
|
||||||
|
obj.OnProcessExited -= OnProcessExited;
|
||||||
|
RemoveProcess(obj);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ using Nebula.Shared.Services;
|
|||||||
namespace Nebula.Launcher.Services;
|
namespace Nebula.Launcher.Services;
|
||||||
|
|
||||||
[ConstructGenerator, ServiceRegister]
|
[ConstructGenerator, ServiceRegister]
|
||||||
public partial class LocalisationService
|
public sealed partial class LocalizationService
|
||||||
{
|
{
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; }
|
[GenerateProperty] private DebugService DebugService { get; }
|
||||||
@@ -40,7 +40,6 @@ public partial class LocalisationService
|
|||||||
Console.WriteLine(error);
|
Console.WriteLine(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
_currentMessageContext = mc;
|
_currentMessageContext = mc;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
DebugService.GetLogger("localisationService").Error(e);
|
DebugService.GetLogger("localisationService").Error(e);
|
||||||
@@ -74,6 +73,6 @@ public class LocaledText : MarkupExtension
|
|||||||
|
|
||||||
public override object ProvideValue(IServiceProvider serviceProvider)
|
public override object ProvideValue(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
return LocalisationService.GetString(Key, Options);
|
return LocalizationService.GetString(Key, Options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
public static class ColorUtils
|
public static class ColorUtils
|
||||||
{
|
{
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using Nebula.Shared;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.Services;
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
|
||||||
public static class ExplorerHelper
|
public static class ExplorerUtils
|
||||||
{
|
{
|
||||||
public static void OpenFolder(string path)
|
public static void OpenFolder(string path)
|
||||||
{
|
{
|
||||||
29
Nebula.Launcher/Utils/VCRuntimeDllChecker.cs
Normal file
29
Nebula.Launcher/Utils/VCRuntimeDllChecker.cs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Utils;
|
||||||
|
|
||||||
|
public static class VCRuntimeDllChecker
|
||||||
|
{
|
||||||
|
public static bool AreVCRuntimeDllsPresent()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows()) return true;
|
||||||
|
|
||||||
|
string systemDir = Environment.SystemDirectory;
|
||||||
|
string[] requiredDlls = {
|
||||||
|
"msvcp140.dll",
|
||||||
|
"vcruntime140.dll"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var dll in requiredDlls)
|
||||||
|
{
|
||||||
|
var path = Path.Combine(systemDir, dll);
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
Nebula.Launcher/ViewModels/ExceptionCompound.cs
Normal file
39
Nebula.Launcher/ViewModels/ExceptionCompound.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ExceptionView), false)]
|
||||||
|
public class ExceptionCompound : ViewModelBase
|
||||||
|
{
|
||||||
|
public ExceptionCompound()
|
||||||
|
{
|
||||||
|
Message = "Test exception";
|
||||||
|
StackTrace = "Stack trace";
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionCompound(string message, string stackTrace)
|
||||||
|
{
|
||||||
|
Message = message;
|
||||||
|
StackTrace = stackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ExceptionCompound(Exception ex)
|
||||||
|
{
|
||||||
|
Message = ex.Message;
|
||||||
|
StackTrace = ex.StackTrace;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string? StackTrace { get; set; }
|
||||||
|
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
public record struct InstanceKey(int Id):
|
||||||
|
IEquatable<int>,
|
||||||
|
IComparable<InstanceKey>
|
||||||
|
{
|
||||||
|
public static implicit operator InstanceKey(int id) => new InstanceKey(id);
|
||||||
|
public static implicit operator int(InstanceKey id) => id.Id;
|
||||||
|
public bool Equals(int other) => Id == other;
|
||||||
|
public int CompareTo(InstanceKey other) => Id.CompareTo(other.Id);
|
||||||
|
};
|
||||||
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
public sealed class InstanceKeyPool
|
||||||
|
{
|
||||||
|
private int _nextId = 1;
|
||||||
|
|
||||||
|
public InstanceKey Take()
|
||||||
|
{
|
||||||
|
return new InstanceKey(_nextId++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Free(InstanceKey id)
|
||||||
|
{
|
||||||
|
// TODO: make some free logic later
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,11 +3,10 @@ using System.Collections.Generic;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using Avalonia.Logging;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
@@ -15,6 +14,7 @@ using Nebula.Shared.Services;
|
|||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
using Nebula.Shared.ViewHelper;
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isPopupClosable = true;
|
[ObservableProperty] private bool _isPopupClosable = true;
|
||||||
[ObservableProperty] private bool _popup;
|
[ObservableProperty] private bool _popup;
|
||||||
[ObservableProperty] private ListItemTemplate? _selectedListItem;
|
[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 AccountInfoViewModel AccountInfoViewModel { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
||||||
@@ -59,7 +59,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
Items = new ObservableCollection<ListItemTemplate>(_templates.Select(a=>
|
Items = new ObservableCollection<ListItemTemplate>(_templates.Select(a=>
|
||||||
{
|
{
|
||||||
return a with { Label = LocalisationService.GetString(a.Label) };
|
return a with { Label = LocalizationService.GetString(a.Label) };
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
RequirePage<AccountInfoViewModel>();
|
RequirePage<AccountInfoViewModel>();
|
||||||
@@ -92,13 +92,13 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
CheckMigration();
|
CheckMigration();
|
||||||
|
|
||||||
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loadingHandler.LoadingName = LocalisationService.GetString("migration-config-task");
|
loadingHandler.LoadingName = LocalizationService.GetString("migration-config-task");
|
||||||
loadingHandler.IsCancellable = false;
|
loadingHandler.IsCancellable = false;
|
||||||
ConfigurationService.MigrateConfigs(loadingHandler);
|
ConfigurationService.MigrateConfigs(loadingHandler);
|
||||||
|
|
||||||
if (!VCRuntimeDllChecker.AreVCRuntimeDllsPresent())
|
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");
|
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)
|
if(AccountInfoViewModel.Credentials.HasValue)
|
||||||
{
|
{
|
||||||
LoginText =
|
LoginText =
|
||||||
LocalisationService.GetString("auth-current-login-name",
|
LocalizationService.GetString("auth-current-login-name",
|
||||||
new Dictionary<string, object>
|
new Dictionary<string, object>
|
||||||
{
|
{
|
||||||
{ "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" },
|
{ "login", AccountInfoViewModel.Credentials.Value?.Login ?? "" },
|
||||||
@@ -120,7 +120,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
else
|
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;
|
return;
|
||||||
|
|
||||||
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
var loadingHandler = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loadingHandler.LoadingName = LocalisationService.GetString("migration-label-task");
|
loadingHandler.LoadingName = LocalizationService.GetString("migration-label-task");
|
||||||
loadingHandler.IsCancellable = false;
|
loadingHandler.IsCancellable = false;
|
||||||
|
|
||||||
if (!ContentService.CheckMigration(loadingHandler))
|
if (!ContentService.CheckMigration(loadingHandler))
|
||||||
@@ -208,7 +208,7 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
public void OpenRootPath()
|
public void OpenRootPath()
|
||||||
{
|
{
|
||||||
ExplorerHelper.OpenFolder(FileService.RootPath);
|
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OpenLink()
|
public void OpenLink()
|
||||||
@@ -230,6 +230,11 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
case PopupViewModelBase @base:
|
case PopupViewModelBase @base:
|
||||||
PopupMessage(@base);
|
PopupMessage(@base);
|
||||||
break;
|
break;
|
||||||
|
case ExceptionCompound error:
|
||||||
|
var errViewModel = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
||||||
|
errViewModel.AppendError(error);
|
||||||
|
PopupMessage(errViewModel);
|
||||||
|
break;
|
||||||
case Exception error:
|
case Exception error:
|
||||||
var err = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
var err = ViewHelperService.GetViewModel<ExceptionListViewModel>();
|
||||||
_logger.Error(error);
|
_logger.Error(error);
|
||||||
@@ -248,16 +253,18 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
else
|
else
|
||||||
_viewQueue.Remove(viewModelBase);
|
_viewQueue.Remove(viewModelBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void TriggerPane()
|
||||||
[RelayCommand]
|
|
||||||
private void TriggerPane()
|
|
||||||
{
|
{
|
||||||
IsPaneOpen = !IsPaneOpen;
|
IsPaneOpen = !IsPaneOpen;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
public void CloseCurrentPopup()
|
||||||
public void ClosePopup()
|
{
|
||||||
|
CurrentPopup?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClosePopup()
|
||||||
{
|
{
|
||||||
var viewModelBase = _viewQueue.FirstOrDefault();
|
var viewModelBase = _viewQueue.FirstOrDefault();
|
||||||
if (viewModelBase is null)
|
if (viewModelBase is null)
|
||||||
@@ -272,29 +279,4 @@ public partial class MainViewModel : ViewModelBase
|
|||||||
|
|
||||||
CurrentPopup = viewModelBase;
|
CurrentPopup = viewModelBase;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static class VCRuntimeDllChecker
|
|
||||||
{
|
|
||||||
public static bool AreVCRuntimeDllsPresent()
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows()) return true;
|
|
||||||
|
|
||||||
string systemDir = Environment.SystemDirectory;
|
|
||||||
string[] requiredDlls = {
|
|
||||||
"msvcp140.dll",
|
|
||||||
"vcruntime140.dll"
|
|
||||||
};
|
|
||||||
|
|
||||||
foreach (var dll in requiredDlls)
|
|
||||||
{
|
|
||||||
var path = Path.Combine(systemDir, dll);
|
|
||||||
if (!File.Exists(path))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -32,6 +32,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private bool _isLogged;
|
[ObservableProperty] private bool _isLogged;
|
||||||
[ObservableProperty] private bool _doRetryAuth;
|
[ObservableProperty] private bool _doRetryAuth;
|
||||||
[ObservableProperty] private AuthServerCredentials _authItemSelect;
|
[ObservableProperty] private AuthServerCredentials _authItemSelect;
|
||||||
|
[ObservableProperty] private string _authServerName;
|
||||||
|
|
||||||
private bool _isProfilesEmpty;
|
private bool _isProfilesEmpty;
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] private PopupMessageService PopupMessageService { get; }
|
||||||
@@ -68,7 +69,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
public void DoAuth(string? code = null)
|
public void DoAuth(string? code = null)
|
||||||
{
|
{
|
||||||
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = LocalisationService.GetString("auth-processing");
|
message.InfoText = LocalizationService.GetString("auth-processing");
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
PopupMessageService.Popup(message);
|
PopupMessageService.Popup(message);
|
||||||
|
|
||||||
@@ -95,7 +96,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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");
|
_logger.Log("TFA required");
|
||||||
break;
|
break;
|
||||||
case AuthenticateDenyCode.InvalidCredentials:
|
case AuthenticateDenyCode.InvalidCredentials:
|
||||||
PopupError(LocalisationService.GetString("auth-invalid-credentials"), e);
|
PopupError(LocalizationService.GetString("auth-invalid-credentials"), e);
|
||||||
break;
|
break;
|
||||||
case AuthenticateDenyCode.AccountLocked:
|
case AuthenticateDenyCode.AccountLocked:
|
||||||
PopupError(LocalisationService.GetString("auth-account-locked"), e);
|
PopupError(LocalizationService.GetString("auth-account-locked"), e);
|
||||||
break;
|
break;
|
||||||
case AuthenticateDenyCode.AccountUnconfirmed:
|
case AuthenticateDenyCode.AccountUnconfirmed:
|
||||||
PopupError(LocalisationService.GetString("auth-account-unconfirmed"), e);
|
PopupError(LocalizationService.GetString("auth-account-unconfirmed"), e);
|
||||||
break;
|
break;
|
||||||
case AuthenticateDenyCode.None:
|
case AuthenticateDenyCode.None:
|
||||||
PopupError(LocalisationService.GetString("auth-none"),e);
|
PopupError(LocalizationService.GetString("auth-none"),e);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
PopupError(LocalisationService.GetString("auth-error-fuck"), e);
|
PopupError(LocalizationService.GetString("auth-error-fuck"), e);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,46 +151,46 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
switch (e.HttpRequestError)
|
switch (e.HttpRequestError)
|
||||||
{
|
{
|
||||||
case HttpRequestError.ConnectionError:
|
case HttpRequestError.ConnectionError:
|
||||||
PopupError(LocalisationService.GetString("auth-connection-error"), e);
|
PopupError(LocalizationService.GetString("auth-connection-error"), e);
|
||||||
DoRetryAuth = true;
|
DoRetryAuth = true;
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.NameResolutionError:
|
case HttpRequestError.NameResolutionError:
|
||||||
PopupError(LocalisationService.GetString("auth-name-resolution-error"), e);
|
PopupError(LocalizationService.GetString("auth-name-resolution-error"), e);
|
||||||
DoRetryAuth = true;
|
DoRetryAuth = true;
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.SecureConnectionError:
|
case HttpRequestError.SecureConnectionError:
|
||||||
PopupError(LocalisationService.GetString("auth-secure-error"), e);
|
PopupError(LocalizationService.GetString("auth-secure-error"), e);
|
||||||
DoRetryAuth = true;
|
DoRetryAuth = true;
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.UserAuthenticationError:
|
case HttpRequestError.UserAuthenticationError:
|
||||||
PopupError(LocalisationService.GetString("auth-user-authentication-error"), e);
|
PopupError(LocalizationService.GetString("auth-user-authentication-error"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.Unknown:
|
case HttpRequestError.Unknown:
|
||||||
PopupError(LocalisationService.GetString("auth-unknown"), e);
|
PopupError(LocalizationService.GetString("auth-unknown"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.HttpProtocolError:
|
case HttpRequestError.HttpProtocolError:
|
||||||
PopupError(LocalisationService.GetString("auth-http-protocol-error"), e);
|
PopupError(LocalizationService.GetString("auth-http-protocol-error"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.ExtendedConnectNotSupported:
|
case HttpRequestError.ExtendedConnectNotSupported:
|
||||||
PopupError(LocalisationService.GetString("auth-extended-connect-not-support"), e);
|
PopupError(LocalizationService.GetString("auth-extended-connect-not-support"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.VersionNegotiationError:
|
case HttpRequestError.VersionNegotiationError:
|
||||||
PopupError(LocalisationService.GetString("auth-version-negotiation-error"), e);
|
PopupError(LocalizationService.GetString("auth-version-negotiation-error"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.ProxyTunnelError:
|
case HttpRequestError.ProxyTunnelError:
|
||||||
PopupError(LocalisationService.GetString("auth-proxy-tunnel-error"), e);
|
PopupError(LocalizationService.GetString("auth-proxy-tunnel-error"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.InvalidResponse:
|
case HttpRequestError.InvalidResponse:
|
||||||
PopupError(LocalisationService.GetString("auth-invalid-response"), e);
|
PopupError(LocalizationService.GetString("auth-invalid-response"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.ResponseEnded:
|
case HttpRequestError.ResponseEnded:
|
||||||
PopupError(LocalisationService.GetString("auth-response-ended"), e);
|
PopupError(LocalizationService.GetString("auth-response-ended"), e);
|
||||||
break;
|
break;
|
||||||
case HttpRequestError.ConfigurationLimitExceeded:
|
case HttpRequestError.ConfigurationLimitExceeded:
|
||||||
PopupError(LocalisationService.GetString("auth-configuration-limit-exceeded"), e);
|
PopupError(LocalizationService.GetString("auth-configuration-limit-exceeded"), e);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
var authError = new Exception(LocalisationService.GetString("auth-error"), e);
|
var authError = new Exception(LocalizationService.GetString("auth-error"), e);
|
||||||
_logger.Error(authError);
|
_logger.Error(authError);
|
||||||
PopupMessageService.Popup(authError);
|
PopupMessageService.Popup(authError);
|
||||||
break;
|
break;
|
||||||
@@ -245,7 +246,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
private async Task ReadAuthConfig()
|
private async Task ReadAuthConfig()
|
||||||
{
|
{
|
||||||
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = LocalisationService.GetString("auth-config-read");
|
message.InfoText = LocalizationService.GetString("auth-config-read");
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
PopupMessageService.Popup(message);
|
PopupMessageService.Popup(message);
|
||||||
|
|
||||||
@@ -318,7 +319,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
var unexpectedError = new Exception(LocalisationService.GetString("auth-error"), e);
|
var unexpectedError = new Exception(LocalizationService.GetString("auth-error"), e);
|
||||||
_logger.Error(unexpectedError);
|
_logger.Error(unexpectedError);
|
||||||
return authTokenCredentials;
|
return authTokenCredentials;
|
||||||
}
|
}
|
||||||
@@ -345,7 +346,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
|
|
||||||
private void PopupError(string message, Exception e)
|
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));
|
_logger.Error(new Exception(message, e));
|
||||||
|
|
||||||
var messageView = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var messageView = ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
@@ -385,7 +386,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var message = accountInfoViewModel.ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
var message = accountInfoViewModel.ViewHelperService.GetViewModel<InfoPopupViewModel>();
|
||||||
message.InfoText = LocalisationService.GetString("auth-try-auth-config");
|
message.InfoText = LocalizationService.GetString("auth-try-auth-config");
|
||||||
message.IsInfoClosable = false;
|
message.IsInfoClosable = false;
|
||||||
accountInfoViewModel.PopupMessageService.Popup(message);
|
accountInfoViewModel.PopupMessageService.Popup(message);
|
||||||
|
|
||||||
@@ -423,7 +424,7 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
accountInfoViewModel.CurrentLogin = currProfile.Login;
|
accountInfoViewModel.CurrentLogin = currProfile.Login;
|
||||||
accountInfoViewModel.CurrentAuthServer = currProfile.AuthServer;
|
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._logger.Error(unexpectedError);
|
||||||
accountInfoViewModel.PopupMessageService.Popup(unexpectedError);
|
accountInfoViewModel.PopupMessageService.Popup(unexpectedError);
|
||||||
errorRun = true;
|
errorRun = true;
|
||||||
@@ -436,6 +437,8 @@ public partial class AccountInfoViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
accountInfoViewModel.IsLogged = true;
|
accountInfoViewModel.IsLogged = true;
|
||||||
|
|
||||||
|
accountInfoViewModel.AuthServerName = accountInfoViewModel.GetServerAuthName(currProfile.AuthServer);
|
||||||
|
|
||||||
return currProfile;
|
return currProfile;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ using System.IO;
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Configurations;
|
using Nebula.Shared.Configurations;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.ViewHelper;
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -69,17 +71,17 @@ public partial class ConfigurationViewModel : ViewModelBase
|
|||||||
|
|
||||||
public void OpenDataFolder()
|
public void OpenDataFolder()
|
||||||
{
|
{
|
||||||
ExplorerHelper.OpenFolder(FileService.RootPath);
|
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ExportLogs()
|
public void ExportLogs()
|
||||||
{
|
{
|
||||||
var logPath = Path.Join(FileService.RootPath, "log");
|
var logPath = Path.Join(AppDataPath.RootPath, "log");
|
||||||
var path = Path.Combine(Path.GetTempPath(), "tempThink"+Path.GetRandomFileName());
|
var path = Path.Combine(Path.GetTempPath(), "tempThink"+Path.GetRandomFileName());
|
||||||
Directory.CreateDirectory(path);
|
Directory.CreateDirectory(path);
|
||||||
|
|
||||||
ZipFile.CreateFromDirectory(logPath, Path.Join(path, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"));
|
ZipFile.CreateFromDirectory(logPath, Path.Join(path, DateTime.Now.ToString("yyyy-MM-dd") + ".zip"));
|
||||||
ExplorerHelper.OpenFolder(path);
|
ExplorerUtils.OpenFolder(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveAllContent()
|
public void RemoveAllContent()
|
||||||
@@ -89,7 +91,7 @@ public partial class ConfigurationViewModel : ViewModelBase
|
|||||||
using var loader = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
using var loader = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||||
loader.LoadingName = "Removing content";
|
loader.LoadingName = "Removing content";
|
||||||
PopupService.Popup(loader);
|
PopupService.Popup(loader);
|
||||||
ContentService.RemoveAllContent(loader, CancellationService.Token);
|
ContentService.RemoveAllContent(loader.CreateLoadingContext(), CancellationService.Token);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,15 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.Utils;
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
using Nebula.Launcher.Views;
|
using Nebula.Launcher.Views;
|
||||||
using Nebula.Launcher.Views.Pages;
|
using Nebula.Launcher.Views.Pages;
|
||||||
using Nebula.Shared.FileApis;
|
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
using Nebula.Shared.ViewHelper;
|
using Nebula.Shared.ViewHelper;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
[GenerateProperty] private FileService FileService { get; } = default!;
|
[GenerateProperty] private FileService FileService { get; } = default!;
|
||||||
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
|
[GenerateProperty] private PopupMessageService PopupService { get; } = default!;
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
|
[GenerateProperty] private CancellationService CancellationService { get; set; } = default!;
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } = default!;
|
||||||
|
|
||||||
|
|
||||||
@@ -57,8 +59,12 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
loading.LoadingName = "Unpacking entry";
|
loading.LoadingName = "Unpacking entry";
|
||||||
PopupService.Popup(loading);
|
PopupService.Popup(loading);
|
||||||
|
|
||||||
Task.Run(() => ContentService.Unpack(serverEntry.FileApi, myTempDir, loading));
|
Task.Run(() =>
|
||||||
ExplorerHelper.OpenFolder(tmpDir);
|
{
|
||||||
|
ContentService.Unpack(serverEntry.FileApi, myTempDir, loading.CreateLoadingContext());
|
||||||
|
loading.Dispose();
|
||||||
|
});
|
||||||
|
ExplorerUtils.OpenFolder(tmpDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnGoEnter()
|
public void OnGoEnter()
|
||||||
@@ -74,11 +80,8 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol
|
|||||||
{
|
{
|
||||||
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
|
var cur = ServiceProvider.GetService<ServerFolderContentEntry>()!;
|
||||||
cur.Init(this, ServerText.ToRobustUrl());
|
cur.Init(this, ServerText.ToRobustUrl());
|
||||||
var curContent = cur.Go(new ContentPath(SearchText));
|
var curContent = cur.Go(new ContentPath(SearchText), CancellationService.Token);
|
||||||
if(curContent == null)
|
CurrentEntry = curContent ?? throw new NullReferenceException($"{SearchText} not found in {ServerText}");
|
||||||
throw new NullReferenceException($"{SearchText} not found in {ServerText}");
|
|
||||||
|
|
||||||
CurrentEntry = curContent;
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -144,11 +147,11 @@ public interface IContentEntry
|
|||||||
public string IconPath { get; }
|
public string IconPath { get; }
|
||||||
public ContentPath FullPath => Parent?.FullPath.With(Name) ?? new ContentPath(Name);
|
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()
|
public void GoCurrent()
|
||||||
{
|
{
|
||||||
var entry = Go(ContentPath.Empty);
|
var entry = Go(ContentPath.Empty, CancellationToken.None);
|
||||||
if(entry is not null) Holder.CurrentEntry = entry;
|
if(entry is not null) Holder.CurrentEntry = entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +181,7 @@ public sealed class LazyContentEntry : IContentEntry
|
|||||||
_lazyEntry = entry;
|
_lazyEntry = entry;
|
||||||
_lazyEntryInit = lazyEntryInit;
|
_lazyEntryInit = lazyEntryInit;
|
||||||
}
|
}
|
||||||
public IContentEntry? Go(ContentPath path)
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_lazyEntryInit?.Invoke();
|
_lazyEntryInit?.Invoke();
|
||||||
return _lazyEntry;
|
return _lazyEntry;
|
||||||
@@ -196,13 +199,13 @@ public sealed class ExtContentExecutor
|
|||||||
_decompilerService = decompilerService;
|
_decompilerService = decompilerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryExecute(RobustManifestItem manifestItem)
|
public bool TryExecute(IFileApi api, ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var ext = Path.GetExtension(manifestItem.Path);
|
var ext = Path.GetExtension(path.GetName());
|
||||||
|
|
||||||
if (ext == ".dll")
|
if (ext == ".dll")
|
||||||
{
|
{
|
||||||
_decompilerService.OpenServerDecompiler(_root.ServerUrl);
|
_decompilerService.OpenServerDecompiler(_root.ServerUrl, cancellationToken);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -211,42 +214,39 @@ public sealed class ExtContentExecutor
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public sealed partial class ManifestContentEntry : IContentEntry
|
public sealed partial class FileContentEntry : IContentEntry
|
||||||
{
|
{
|
||||||
public IContentHolder Holder { get; set; } = default!;
|
public IContentHolder Holder { get; set; } = default!;
|
||||||
public IContentEntry? Parent { get; set; }
|
public IContentEntry? Parent { get; set; }
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
public string IconPath => "/Assets/svg/file.svg";
|
public string IconPath => "/Assets/svg/file.svg";
|
||||||
|
|
||||||
private RobustManifestItem _manifestItem;
|
private IFileApi _fileApi = default!;
|
||||||
private HashApi _hashApi = default!;
|
|
||||||
private ExtContentExecutor _extContentExecutor = default!;
|
private ExtContentExecutor _extContentExecutor = default!;
|
||||||
|
|
||||||
public void Init(IContentHolder holder, RobustManifestItem manifestItem, HashApi api, ExtContentExecutor executor)
|
public void Init(IContentHolder holder, IFileApi api, string fileName, ExtContentExecutor executor)
|
||||||
{
|
{
|
||||||
Holder = holder;
|
Holder = holder;
|
||||||
Name = new ContentPath(manifestItem.Path).GetName();
|
Name = fileName;
|
||||||
_manifestItem = manifestItem;
|
_fileApi = api;
|
||||||
_hashApi = api;
|
|
||||||
_extContentExecutor = executor;
|
_extContentExecutor = executor;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IContentEntry? Go(ContentPath path)
|
public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (_extContentExecutor.TryExecute(_manifestItem))
|
var fullPath = ((IContentEntry)this).FullPath;
|
||||||
|
if (_extContentExecutor.TryExecute(_fileApi, fullPath, cancellationToken))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var ext = Path.GetExtension(_manifestItem.Path);
|
var ext = Path.GetExtension(fullPath.GetName());
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!_hashApi.TryOpen(_manifestItem, out var stream))
|
if (!_fileApi.TryOpen(fullPath.Path, out var stream))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
|
|
||||||
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
|
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
|
||||||
|
|
||||||
|
|
||||||
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
stream.CopyTo(sw);
|
stream.CopyTo(sw);
|
||||||
|
|
||||||
@@ -295,7 +295,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
|
|
||||||
public RobustUrl ServerUrl { get; private set; }
|
public RobustUrl ServerUrl { get; private set; }
|
||||||
|
|
||||||
public HashApi FileApi { get; private set; } = default!;
|
public IFileApi FileApi { get; private set; } = default!;
|
||||||
|
|
||||||
private ExtContentExecutor _contentExecutor = default!;
|
private ExtContentExecutor _contentExecutor = default!;
|
||||||
|
|
||||||
@@ -312,20 +312,20 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
|
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
|
||||||
FileApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loading,
|
FileApi = await ContentService.EnsureItems(buildInfo, loading,
|
||||||
CancellationService.Token);
|
CancellationService.Token);
|
||||||
|
|
||||||
foreach (var (path, item) in FileApi.Manifest)
|
foreach (var path in FileApi.AllFiles)
|
||||||
{
|
{
|
||||||
CreateContent(new ContentPath(path), item);
|
CreateContent(new ContentPath(path));
|
||||||
}
|
}
|
||||||
|
|
||||||
IsLoading = false;
|
IsLoading = false;
|
||||||
loading.Dispose();
|
loading.Dispose();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public ManifestContentEntry CreateContent(ContentPath path, RobustManifestItem manifestItem)
|
public FileContentEntry CreateContent(ContentPath path)
|
||||||
{
|
{
|
||||||
var pathDir = path.GetDirectory();
|
var pathDir = path.GetDirectory();
|
||||||
BaseFolderContentEntry parent = this;
|
BaseFolderContentEntry parent = this;
|
||||||
@@ -342,8 +342,8 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
|
|||||||
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
|
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
var manifestContent = new ManifestContentEntry();
|
var manifestContent = new FileContentEntry();
|
||||||
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor);
|
manifestContent.Init(Holder, FileApi, path.GetName(), _contentExecutor);
|
||||||
|
|
||||||
parent.AddChild(manifestContent);
|
parent.AddChild(manifestContent);
|
||||||
|
|
||||||
@@ -433,11 +433,11 @@ public abstract class BaseFolderContentEntry : ViewModelBase, IContentEntry
|
|||||||
public IContentEntry? Parent { get; set; }
|
public IContentEntry? Parent { get; set; }
|
||||||
public string? Name { get; private 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 (path.IsEmpty()) return this;
|
||||||
if (_childs.TryGetValue(path.GetNext(), out var child))
|
if (_childs.TryGetValue(path.GetNext(), out var child))
|
||||||
return child.Go(path);
|
return child.Go(path, cancellationToken);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Avalonia.Controls;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using JetBrains.Annotations;
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -14,6 +16,7 @@ using Nebula.Launcher.Views.Pages;
|
|||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
using Nebula.Shared.ViewHelper;
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels.Pages;
|
namespace Nebula.Launcher.ViewModels.Pages;
|
||||||
@@ -23,22 +26,17 @@ namespace Nebula.Launcher.ViewModels.Pages;
|
|||||||
public partial class ServerOverviewModel : ViewModelBase
|
public partial class ServerOverviewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
[ObservableProperty] private string _searchText = string.Empty;
|
[ObservableProperty] private string _searchText = string.Empty;
|
||||||
|
|
||||||
[ObservableProperty] private bool _isFilterVisible;
|
[ObservableProperty] private bool _isFilterVisible;
|
||||||
|
|
||||||
[ObservableProperty] private ServerListView _currentServerList = new();
|
|
||||||
|
|
||||||
public readonly ServerFilter CurrentFilter = new();
|
|
||||||
|
|
||||||
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
|
||||||
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
|
||||||
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
|
||||||
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
|
|
||||||
[ObservableProperty] private ServerListTabTemplate _selectedItem;
|
[ObservableProperty] private ServerListTabTemplate _selectedItem;
|
||||||
|
|
||||||
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; }
|
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; }
|
||||||
|
[GenerateProperty, DesignConstruct] public ServerListViewModel CurrentServerList { get; }
|
||||||
|
|
||||||
private Dictionary<string, ServerListView> _viewCache = [];
|
public ServerFilter CurrentFilter { get; } = new();
|
||||||
|
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
//Design think
|
//Design think
|
||||||
@@ -54,9 +52,18 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
//real think
|
//real think
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
|
FavoriteServerListProvider.OnRefreshRequired += OnFavoriteRefreshRequired;
|
||||||
ConfigurationService.SubscribeVarChanged(LauncherConVar.Hub, OnHubListChanged, true);
|
ConfigurationService.SubscribeVarChanged(LauncherConVar.Hub, OnHubListChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnFavoriteRefreshRequired()
|
||||||
|
{
|
||||||
|
if(CurrentServerList.Provider is FavoriteServerListProvider favoriteServerListProvider)
|
||||||
|
{
|
||||||
|
RefreshProvider();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnHubListChanged(ServerHubRecord[]? value)
|
private void OnHubListChanged(ServerHubRecord[]? value)
|
||||||
{
|
{
|
||||||
var tempItems = new List<ServerListTabTemplate>();
|
var tempItems = new List<ServerListTabTemplate>();
|
||||||
@@ -66,7 +73,7 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService<HubServerListProvider>()!.With(record.MainUrl), record.Name));
|
tempItems.Add(new ServerListTabTemplate(ServiceProvider.GetService<HubServerListProvider>()!.With(record.MainUrl), record.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, "Favorite"));
|
tempItems.Add(new ServerListTabTemplate(FavoriteServerListProvider, LocalizationService.GetString("tab-favorite")));
|
||||||
|
|
||||||
Items = new ObservableCollection<ServerListTabTemplate>(tempItems);
|
Items = new ObservableCollection<ServerListTabTemplate>(tempItems);
|
||||||
|
|
||||||
@@ -79,13 +86,9 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
ApplyFilter();
|
ApplyFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ApplyFilter()
|
private void ApplyFilter()
|
||||||
{
|
{
|
||||||
foreach (var entry in ServerViewContainer.Items)
|
ServerViewContainer.ApplyFilter(CurrentFilter);
|
||||||
{
|
|
||||||
if(entry is IFilterConsumer filterConsumer)
|
|
||||||
filterConsumer.ProcessFilter(CurrentFilter);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnFilterChanged(FilterBoxChangedEventArgs args)
|
public void OnFilterChanged(FilterBoxChangedEventArgs args)
|
||||||
@@ -105,32 +108,39 @@ public partial class ServerOverviewModel : ViewModelBase
|
|||||||
public void UpdateRequired()
|
public void UpdateRequired()
|
||||||
{
|
{
|
||||||
ServerViewContainer.Clear();
|
ServerViewContainer.Clear();
|
||||||
|
RefreshProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshProvider()
|
||||||
|
{
|
||||||
|
CurrentServerList.ClearProvider();
|
||||||
CurrentServerList.RefreshFromProvider();
|
CurrentServerList.RefreshFromProvider();
|
||||||
CurrentServerList.RequireStatusUpdate();
|
|
||||||
CurrentServerList.ApplyFilter(CurrentFilter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedItemChanged(ServerListTabTemplate value)
|
partial void OnSelectedItemChanged(ServerListTabTemplate value)
|
||||||
{
|
{
|
||||||
if (!_viewCache.TryGetValue(value.TabName, out var view))
|
CurrentServerList.ClearProvider();
|
||||||
{
|
CurrentServerList.SetProvider(value.ServerListProvider);
|
||||||
view = ServerListView.TakeFrom(value.ServerListProvider);
|
|
||||||
_viewCache[value.TabName] = view;
|
|
||||||
}
|
|
||||||
|
|
||||||
CurrentServerList = view;
|
|
||||||
ApplyFilter();
|
ApplyFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public class ServerViewContainer
|
public sealed class ServerViewContainer
|
||||||
{
|
{
|
||||||
private readonly ViewHelperService _viewHelperService;
|
private readonly ViewHelperService _viewHelperService;
|
||||||
private readonly List<string> _favorites = [];
|
private readonly List<string> _favorites = [];
|
||||||
private readonly Dictionary<string, string> _customNames = [];
|
private readonly Dictionary<string, string> _customNames = [];
|
||||||
|
|
||||||
|
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
|
||||||
|
private ServerFilter? _currentFilter;
|
||||||
|
|
||||||
|
public ICollection<IListEntryModelView> Items =>
|
||||||
|
_entries.Values
|
||||||
|
.Select(wr => wr.TryGetTarget(out var target) ? target : null)
|
||||||
|
.Where(t => t != null)
|
||||||
|
.ToList()!;
|
||||||
|
|
||||||
public ServerViewContainer()
|
public ServerViewContainer()
|
||||||
{
|
{
|
||||||
_viewHelperService = new ViewHelperService();
|
_viewHelperService = new ViewHelperService();
|
||||||
@@ -144,111 +154,171 @@ public class ServerViewContainer
|
|||||||
configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true);
|
configurationService.SubscribeVarChanged(LauncherConVar.ServerCustomNames, OnCustomNamesChanged, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnCustomNamesChanged(Dictionary<string,string>? value)
|
public void Clear()
|
||||||
{
|
{
|
||||||
var oldNames =
|
foreach (var (_, weakRef) in _entries)
|
||||||
_customNames.ToDictionary(k => k.Key, v => v.Value); //Clone think
|
|
||||||
|
|
||||||
_customNames.Clear();
|
|
||||||
|
|
||||||
if(value == null)
|
|
||||||
{
|
{
|
||||||
foreach (var (ip,_) in oldNames)
|
if (weakRef.TryGetTarget(out var value) && value is IDisposable disposable)
|
||||||
{
|
disposable.Dispose();
|
||||||
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
entryNameHolder.Name = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (oldIp, oldName) in oldNames)
|
_entries.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IListEntryModelView Get(string url, ServerStatus? serverStatus = null) => Get(url.ToRobustUrl(), serverStatus);
|
||||||
|
|
||||||
|
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
|
||||||
|
{
|
||||||
|
var key = url.ToString();
|
||||||
|
IListEntryModelView? entry;
|
||||||
|
|
||||||
|
lock (_entries)
|
||||||
{
|
{
|
||||||
if(value.TryGetValue(oldIp, out var newName))
|
if (_entries.TryGetValue(key, out var weakEntry)
|
||||||
|
&& weakEntry.TryGetTarget(out entry))
|
||||||
{
|
{
|
||||||
if (oldName == newName)
|
return entry;
|
||||||
value.Remove(newName);
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!_entries.TryGetValue(oldIp, out var listEntry) ||
|
entry = Create(url, serverStatus);
|
||||||
listEntry is not IEntryNameHolder entryNameHolder)
|
|
||||||
continue;
|
_entries[key] = new WeakReference<IListEntryModelView>(entry);
|
||||||
|
}
|
||||||
entryNameHolder.Name = null;
|
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IListEntryModelView Create(RobustUrl url, ServerStatus? serverStatus = null)
|
||||||
|
{
|
||||||
|
IListEntryModelView? entry;
|
||||||
|
var key = url.ToString();
|
||||||
|
|
||||||
|
_customNames.TryGetValue(key, out var customName);
|
||||||
|
|
||||||
|
if (serverStatus is not null)
|
||||||
|
{
|
||||||
|
entry = _viewHelperService
|
||||||
|
.GetViewModel<ServerEntryViewModel>()
|
||||||
|
.WithData(url, customName, serverStatus);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
entry = _viewHelperService
|
||||||
|
.GetViewModel<ServerCompoundEntryViewModel>()
|
||||||
|
.LoadServerEntry(url, customName, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is IFavoriteEntryModelView fav)
|
||||||
|
{
|
||||||
|
fav.IsFavorite = _favorites.Contains(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry is IFilterConsumer filterConsumer)
|
||||||
|
{
|
||||||
|
filterConsumer.ProcessFilter(_currentFilter);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (ip, name) in value)
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyFilter(ServerFilter? filter)
|
||||||
|
{
|
||||||
|
_currentFilter = filter;
|
||||||
|
|
||||||
|
foreach (var serverView in Items)
|
||||||
{
|
{
|
||||||
_customNames.Add(ip, name);
|
if(serverView is IFilterConsumer filterConsumer)
|
||||||
if(!_entries.TryGetValue(ip, out var listEntry) || listEntry is not IEntryNameHolder entryNameHolder)
|
filterConsumer.ProcessFilter(filter);
|
||||||
continue;
|
|
||||||
|
|
||||||
entryNameHolder.Name = name;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFavoritesChange(string[]? value)
|
private void OnFavoritesChange(string[]? value)
|
||||||
{
|
{
|
||||||
_favorites.Clear();
|
_favorites.Clear();
|
||||||
if(value == null) return;
|
if (value == null) return;
|
||||||
|
|
||||||
foreach (var favorite in value)
|
foreach (var favorite in value)
|
||||||
{
|
{
|
||||||
_favorites.Add(favorite);
|
_favorites.Add(favorite);
|
||||||
if (_entries.TryGetValue(favorite, out var entry) && entry is IFavoriteEntryModelView favoriteView)
|
if (_entries.TryGetValue(favorite, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IFavoriteEntryModelView fav)
|
||||||
{
|
{
|
||||||
favoriteView.IsFavorite = true;
|
fav.IsFavorite = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Dictionary<string, IListEntryModelView> _entries = new();
|
private void OnCustomNamesChanged(Dictionary<string, string>? value)
|
||||||
|
|
||||||
public ICollection<IListEntryModelView> Items => _entries.Values;
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
{
|
||||||
_entries.Clear();
|
var oldNames = _customNames.ToDictionary(x => x.Key, x => x.Value);
|
||||||
}
|
_customNames.Clear();
|
||||||
|
|
||||||
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
|
if (value == null)
|
||||||
{
|
|
||||||
IListEntryModelView? entry;
|
|
||||||
|
|
||||||
lock (_entries)
|
|
||||||
{
|
{
|
||||||
_customNames.TryGetValue(url.ToString(), out var customName);
|
foreach (var (ip, _) in oldNames)
|
||||||
|
|
||||||
if (_entries.TryGetValue(url.ToString(), out entry))
|
|
||||||
{
|
{
|
||||||
return entry;
|
ResetName(ip);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (serverStatus is not null)
|
return;
|
||||||
entry = _viewHelperService.GetViewModel<ServerEntryModelView>().WithData(url, customName, serverStatus);
|
}
|
||||||
else
|
|
||||||
entry = _viewHelperService.GetViewModel<ServerCompoundEntryViewModel>().LoadServerEntry(url, customName, CancellationToken.None);
|
foreach (var (oldIp, oldName) in oldNames)
|
||||||
|
{
|
||||||
if(_favorites.Contains(url.ToString()) &&
|
if (value.TryGetValue(oldIp, out var newName))
|
||||||
entry is IFavoriteEntryModelView favoriteEntryModelView)
|
{
|
||||||
favoriteEntryModelView.IsFavorite = true;
|
if (oldName == newName)
|
||||||
|
value.Remove(newName);
|
||||||
_entries.Add(url.ToString(), entry);
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
ResetName(oldIp);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (ip, name) in value)
|
||||||
|
{
|
||||||
|
_customNames.Add(ip, name);
|
||||||
|
|
||||||
|
if (_entries.TryGetValue(ip, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IEntryNameHolder holder)
|
||||||
|
{
|
||||||
|
holder.Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetName(string ip)
|
||||||
|
{
|
||||||
|
if (_entries.TryGetValue(ip, out var weak)
|
||||||
|
&& weak.TryGetTarget(out var entry)
|
||||||
|
&& entry is IEntryNameHolder holder)
|
||||||
|
{
|
||||||
|
holder.Name = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return entry;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IListEntryModelView
|
public interface IListEntryModelView
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed class ExampleEntry : StackPanel, IListEntryModelView
|
||||||
|
{
|
||||||
|
public ExampleEntry(string name)
|
||||||
|
{
|
||||||
|
Children.Add(new Label { Content = name });
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public interface IFavoriteEntryModelView
|
public interface IFavoriteEntryModelView
|
||||||
{
|
{
|
||||||
public bool IsFavorite { get; set; }
|
public bool IsFavorite { get; set; }
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
[GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; }
|
[GenerateProperty] private ServerOverviewModel ServerOverviewModel { get; }
|
||||||
[GenerateProperty] private DebugService DebugService { get; }
|
[GenerateProperty] private DebugService DebugService { get; }
|
||||||
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { 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;
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
[ObservableProperty] private string _ipInput;
|
[ObservableProperty] private string _ipInput;
|
||||||
@@ -43,7 +43,7 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
if(string.IsNullOrWhiteSpace(IpInput))
|
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();
|
var uri = IpInput.ToRobustUrl();
|
||||||
FavoriteServerListProvider.AddFavorite(uri);
|
FavoriteServerListProvider.AddFavorite(uri);
|
||||||
@@ -55,4 +55,10 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
|||||||
_logger.Error(e);
|
_logger.Error(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnDispose()
|
||||||
|
{
|
||||||
|
base.OnDispose();
|
||||||
|
_logger.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ public sealed partial class EditServerNameViewModel : PopupViewModelBase
|
|||||||
{
|
{
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] public ConfigurationService ConfigurationService { 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;
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
[ObservableProperty] private string _ipInput;
|
[ObservableProperty] private string _ipInput;
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ namespace Nebula.Launcher.ViewModels.Popup;
|
|||||||
public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
||||||
{
|
{
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[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 override bool IsClosable => true;
|
||||||
|
|
||||||
public ObservableCollection<Exception> Errors { get; } = new();
|
public ObservableCollection<ExceptionCompound> Errors { get; } = new();
|
||||||
|
|
||||||
protected override void Initialise()
|
protected override void Initialise()
|
||||||
{
|
{
|
||||||
@@ -23,13 +23,18 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase
|
|||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
var e = new Exception("TEST");
|
var e = new ExceptionCompound("TEST", "thrown in design mode");
|
||||||
AppendError(e);
|
AppendError(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AppendError(ExceptionCompound exception)
|
||||||
|
{
|
||||||
|
Errors.Add(exception);
|
||||||
|
}
|
||||||
|
|
||||||
public void AppendError(Exception exception)
|
public void AppendError(Exception exception)
|
||||||
{
|
{
|
||||||
Errors.Add(exception);
|
AppendError(new ExceptionCompound(exception));
|
||||||
if (exception.InnerException != null)
|
if (exception.InnerException != null)
|
||||||
AppendError(exception.InnerException);
|
AppendError(exception.InnerException);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public partial class InfoPopupViewModel : PopupViewModelBase
|
|||||||
|
|
||||||
[ObservableProperty] private string _infoText = "Test";
|
[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 bool IsInfoClosable { get; set; } = true;
|
||||||
public override bool IsClosable => IsInfoClosable;
|
public override bool IsClosable => IsInfoClosable;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace Nebula.Launcher.ViewModels.Popup;
|
|||||||
[ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))]
|
[ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))]
|
||||||
public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
|
public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
|
||||||
{
|
{
|
||||||
private ServerEntryModelView _entry;
|
private ServerEntryViewModel _entryView;
|
||||||
|
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
|
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
|
||||||
@@ -22,15 +22,15 @@ public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public IsLoginCredentialsNullPopupViewModel WithServerEntry(ServerEntryModelView entryModelView)
|
public IsLoginCredentialsNullPopupViewModel WithServerEntry(ServerEntryViewModel entryViewModel)
|
||||||
{
|
{
|
||||||
_entry = entryModelView;
|
_entryView = entryViewModel;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Proceed()
|
public void Proceed()
|
||||||
{
|
{
|
||||||
_entry.RunInstanceIgnoreAuth();
|
_entryView.RunInstanceIgnoreAuth();
|
||||||
Dispose();
|
Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,6 +45,6 @@ public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
|
|||||||
Dispose();
|
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;
|
public override bool IsClosable => true;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Nebula.Launcher.Services;
|
using Nebula.Launcher.Services;
|
||||||
using Nebula.Launcher.Views.Popup;
|
using Nebula.Launcher.Views.Popup;
|
||||||
@@ -9,82 +11,121 @@ namespace Nebula.Launcher.ViewModels.Popup;
|
|||||||
|
|
||||||
[ViewModelRegister(typeof(LoadingContextView), false)]
|
[ViewModelRegister(typeof(LoadingContextView), false)]
|
||||||
[ConstructGenerator]
|
[ConstructGenerator]
|
||||||
public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandler
|
public sealed partial class LoadingContextViewModel : PopupViewModelBase, ILoadingHandlerFactory, IConnectionSpeedHandler
|
||||||
{
|
{
|
||||||
|
public ObservableCollection<LoadingContext> LoadingContexts { get; } = [];
|
||||||
|
public ObservableCollection<double> Values { get; } = [];
|
||||||
|
[ObservableProperty] private string _speedText = "";
|
||||||
|
[ObservableProperty] private bool _showSpeed;
|
||||||
|
[ObservableProperty] private int _loadingColumnSize = 2;
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] public CancellationService CancellationService { 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 bool IsCancellable { get; set; } = true;
|
||||||
public override bool IsClosable => false;
|
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;
|
if (!IsCancellable) return;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
CancellationService.Cancel();
|
CancellationService.Cancel();
|
||||||
Dispose();
|
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 Initialise()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
{
|
{
|
||||||
SetJobsCount(5);
|
var context = CreateLoadingContext();
|
||||||
SetResolvedJobsCount(2);
|
context.SetJobsCount(5);
|
||||||
string[] debugMessages = {
|
context.SetResolvedJobsCount(2);
|
||||||
"Debug: Starting phase 1...",
|
context.SetLoadingMessage("message");
|
||||||
"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."
|
|
||||||
};
|
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ public abstract class PopupViewModelBase : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
public abstract string Title { get; }
|
public abstract string Title { get; }
|
||||||
public abstract bool IsClosable { get; }
|
public abstract bool IsClosable { get; }
|
||||||
|
public Action<PopupViewModelBase>? OnDisposing;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
OnDispose();
|
OnDispose();
|
||||||
|
OnDisposing?.Invoke(this);
|
||||||
PopupMessageService.ClosePopup(this);
|
PopupMessageService.ClosePopup(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ public partial class TfaViewModel : PopupViewModelBase
|
|||||||
{
|
{
|
||||||
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
[GenerateProperty] public override PopupMessageService PopupMessageService { get; }
|
||||||
[GenerateProperty] public AccountInfoViewModel AccountInfo { 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;
|
public override bool IsClosable => true;
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
protected override void InitialiseInDesignMode()
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Nebula.Launcher.Models;
|
using Nebula.Launcher.Models;
|
||||||
@@ -13,7 +10,6 @@ using Nebula.Launcher.Views;
|
|||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.ViewHelper;
|
using Nebula.Shared.ViewHelper;
|
||||||
using BindingFlags = System.Reflection.BindingFlags;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
@@ -22,8 +18,6 @@ namespace Nebula.Launcher.ViewModels;
|
|||||||
public sealed partial class ServerCompoundEntryViewModel :
|
public sealed partial class ServerCompoundEntryViewModel :
|
||||||
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
|
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
|
||||||
{
|
{
|
||||||
[ObservableProperty] private ServerEntryModelView? _currentEntry;
|
|
||||||
[ObservableProperty] private Control? _entryControl;
|
|
||||||
[ObservableProperty] private string _message = "Loading server entry...";
|
[ObservableProperty] private string _message = "Loading server entry...";
|
||||||
[ObservableProperty] private bool _isFavorite;
|
[ObservableProperty] private bool _isFavorite;
|
||||||
[ObservableProperty] private bool _loading = true;
|
[ObservableProperty] private bool _loading = true;
|
||||||
@@ -32,6 +26,28 @@ public sealed partial class ServerCompoundEntryViewModel :
|
|||||||
private RobustUrl? _url;
|
private RobustUrl? _url;
|
||||||
private ServerFilter? _currentFilter;
|
private ServerFilter? _currentFilter;
|
||||||
|
|
||||||
|
public ServerEntryViewModel? CurrentEntry
|
||||||
|
{
|
||||||
|
get;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value == field) return;
|
||||||
|
|
||||||
|
field = value;
|
||||||
|
|
||||||
|
if (field != null)
|
||||||
|
{
|
||||||
|
field.IsFavorite = IsFavorite;
|
||||||
|
field.Name = Name;
|
||||||
|
field.ProcessFilter(_currentFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading = field == null;
|
||||||
|
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public string? Name
|
public string? Name
|
||||||
{
|
{
|
||||||
get => _name;
|
get => _name;
|
||||||
@@ -58,31 +74,43 @@ public sealed partial class ServerCompoundEntryViewModel :
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url,string? name, CancellationToken cancellationToken)
|
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryViewModel? entry)
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
CurrentEntry = entry;
|
||||||
{
|
|
||||||
_url = url;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Message = "Loading server entry...";
|
|
||||||
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, cancellationToken);
|
|
||||||
|
|
||||||
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url,name, status);
|
|
||||||
CurrentEntry.IsFavorite = IsFavorite;
|
|
||||||
CurrentEntry.Loading = false;
|
|
||||||
CurrentEntry.ProcessFilter(_currentFilter);
|
|
||||||
Loading = false;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Message = e.Message;
|
|
||||||
}
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServerCompoundEntryViewModel LoadServerEntry(RobustUrl url, string? name, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_url = url;
|
||||||
|
_name = name;
|
||||||
|
Task.Run(LoadServer, cancellationToken);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadServer()
|
||||||
|
{
|
||||||
|
if (_url is null)
|
||||||
|
{
|
||||||
|
Message = "Url is not set";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Message = "Loading server entry...";
|
||||||
|
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None);
|
||||||
|
|
||||||
|
CurrentEntry = ServiceProvider.GetService<ServerEntryViewModel>()!.WithData(_url, null, status);
|
||||||
|
|
||||||
|
Loading = false;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Message = "Error while fetching data from " + _url + " : " + e.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ToggleFavorites()
|
public void ToggleFavorites()
|
||||||
{
|
{
|
||||||
if (CurrentEntry is null && _url is not null)
|
if (CurrentEntry is null && _url is not null)
|
||||||
|
|||||||
@@ -1,268 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows.Input;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
|
||||||
using Nebula.Launcher.Models;
|
|
||||||
using Nebula.Launcher.ProcessHelper;
|
|
||||||
using Nebula.Launcher.ServerListProviders;
|
|
||||||
using Nebula.Launcher.Services;
|
|
||||||
using Nebula.Launcher.ViewModels.Pages;
|
|
||||||
using Nebula.Launcher.ViewModels.Popup;
|
|
||||||
using Nebula.Launcher.Views;
|
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
|
||||||
using Nebula.Shared.Services.Logging;
|
|
||||||
using Nebula.Shared.Utils;
|
|
||||||
using Nebula.Shared.ViewHelper;
|
|
||||||
|
|
||||||
namespace Nebula.Launcher.ViewModels;
|
|
||||||
|
|
||||||
[ViewModelRegister(typeof(ServerEntryView), false)]
|
|
||||||
[ConstructGenerator]
|
|
||||||
public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, IListEntryModelView, IFavoriteEntryModelView, IEntryNameHolder
|
|
||||||
{
|
|
||||||
[ObservableProperty] private string _description = "Fetching info...";
|
|
||||||
[ObservableProperty] private bool _expandInfo;
|
|
||||||
[ObservableProperty] private bool _isFavorite;
|
|
||||||
[ObservableProperty] private bool _isVisible;
|
|
||||||
[ObservableProperty] private bool _runVisible = true;
|
|
||||||
[ObservableProperty] private bool _tagDataVisible;
|
|
||||||
[ObservableProperty] private bool _loading;
|
|
||||||
[ObservableProperty] private string _realName;
|
|
||||||
|
|
||||||
public string? Name
|
|
||||||
{
|
|
||||||
get => RealName;
|
|
||||||
set => RealName = value ?? Status.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ILogger _logger;
|
|
||||||
private ServerInfo? _serverInfo;
|
|
||||||
private ContentLogConsumer _currentContentLogConsumer;
|
|
||||||
private ProcessRunHandler<GameProcessStartInfoProvider>? _currentInstance;
|
|
||||||
|
|
||||||
public LogPopupModelView CurrLog;
|
|
||||||
public RobustUrl Address { get; private set; }
|
|
||||||
[GenerateProperty] private AccountInfoViewModel AccountInfoViewModel { get; }
|
|
||||||
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
|
|
||||||
[GenerateProperty] private DebugService DebugService { get; } = default!;
|
|
||||||
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
|
|
||||||
[GenerateProperty] private ViewHelperService ViewHelperService { get; } = default!;
|
|
||||||
[GenerateProperty] private RestService RestService { get; } = default!;
|
|
||||||
[GenerateProperty] private MainViewModel MainViewModel { get; } = default!;
|
|
||||||
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; } = default!;
|
|
||||||
[GenerateProperty] private GameRunnerPreparer GameRunnerPreparer { get; } = default!;
|
|
||||||
|
|
||||||
public ServerStatus Status { get; private set; } =
|
|
||||||
new(
|
|
||||||
"Fetching data...",
|
|
||||||
"Loading...", [],
|
|
||||||
"",
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
-1,
|
|
||||||
false,
|
|
||||||
DateTime.Now,
|
|
||||||
-1
|
|
||||||
);
|
|
||||||
|
|
||||||
public ObservableCollection<ServerLink> Links { get; } = new();
|
|
||||||
public ObservableCollection<string> Tags { get; } = [];
|
|
||||||
public ICommand OnLinkGo { get; } = new LinkGoCommand();
|
|
||||||
|
|
||||||
public async Task<ServerInfo?> GetServerInfo()
|
|
||||||
{
|
|
||||||
if (_serverInfo == null)
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Description = e.Message;
|
|
||||||
_logger.Error(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return _serverInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void InitialiseInDesignMode()
|
|
||||||
{
|
|
||||||
IsVisible = true;
|
|
||||||
RealName = "TEST.TEST";
|
|
||||||
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
|
|
||||||
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
|
|
||||||
Status = new ServerStatus("Ameba",
|
|
||||||
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
|
|
||||||
["rp:hrp", "18+"],
|
|
||||||
"Antag", 15, 5, 1, false
|
|
||||||
, DateTime.Now, 100);
|
|
||||||
Address = "ss14://localhost".ToRobustUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void Initialise()
|
|
||||||
{
|
|
||||||
_logger = DebugService.GetLogger(this);
|
|
||||||
CurrLog = ViewHelperService.GetViewModel<LogPopupModelView>();
|
|
||||||
_currentContentLogConsumer = new(CurrLog, PopupMessageService);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ProcessFilter(ServerFilter? serverFilter)
|
|
||||||
{
|
|
||||||
if (serverFilter == null)
|
|
||||||
{
|
|
||||||
IsVisible = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SetStatus(ServerStatus serverStatus)
|
|
||||||
{
|
|
||||||
Status = serverStatus;
|
|
||||||
Tags.Clear();
|
|
||||||
foreach (var tag in Status.Tags) Tags.Add(tag);
|
|
||||||
OnPropertyChanged(nameof(Status));
|
|
||||||
}
|
|
||||||
|
|
||||||
public ServerEntryModelView WithData(RobustUrl url, string? name,ServerStatus serverStatus)
|
|
||||||
{
|
|
||||||
Address = url;
|
|
||||||
SetStatus(serverStatus);
|
|
||||||
Name = name;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void EditName()
|
|
||||||
{
|
|
||||||
var popup = ViewHelperService.GetViewModel<EditServerNameViewModel>();
|
|
||||||
popup.IpInput = Address.ToString();
|
|
||||||
popup.NameInput = Name ?? string.Empty;
|
|
||||||
PopupMessageService.Popup(popup);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OpenContentViewer()
|
|
||||||
{
|
|
||||||
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address, ContentPath.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ToggleFavorites()
|
|
||||||
{
|
|
||||||
IsFavorite = !IsFavorite;
|
|
||||||
if(IsFavorite)
|
|
||||||
FavoriteServerListProvider.AddFavorite(this);
|
|
||||||
else
|
|
||||||
FavoriteServerListProvider.RemoveFavorite(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunInstance()
|
|
||||||
{
|
|
||||||
CurrLog.Clear();
|
|
||||||
Task.Run(async ()=> await RunInstanceAsync());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void RunInstanceIgnoreAuth()
|
|
||||||
{
|
|
||||||
CurrLog.Clear();
|
|
||||||
Task.Run(async ()=> await RunInstanceAsync(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RunInstanceAsync(bool ignoreLoginCredentials = false)
|
|
||||||
{
|
|
||||||
_logger.Log("Running instance..." + RealName);
|
|
||||||
if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials.Value is null)
|
|
||||||
{
|
|
||||||
var warningContext = ViewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
|
|
||||||
.WithServerEntry(this);
|
|
||||||
|
|
||||||
PopupMessageService.Popup(warningContext);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using var loadingContext = ViewHelperService.GetViewModel<LoadingContextViewModel>();
|
|
||||||
loadingContext.LoadingName = "Loading instance...";
|
|
||||||
((ILoadingHandler)loadingContext).AppendJob();
|
|
||||||
|
|
||||||
PopupMessageService.Popup(loadingContext);
|
|
||||||
_currentInstance =
|
|
||||||
await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, loadingContext, CancellationService.Token);
|
|
||||||
_logger.Log("Preparing instance...");
|
|
||||||
_currentInstance.RegisterLogger(_currentContentLogConsumer);
|
|
||||||
_currentInstance.RegisterLogger(new DebugLoggerBridge(DebugService.GetLogger($"PROCESS_{Random.Shared.Next(65535)}")));
|
|
||||||
_currentInstance.OnProcessExited += OnProcessExited;
|
|
||||||
RunVisible = false;
|
|
||||||
_currentInstance.Start();
|
|
||||||
_logger.Log("Starting instance..." + RealName);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
var error = new Exception("Error while attempt run instance", e);
|
|
||||||
_logger.Error(error);
|
|
||||||
PopupMessageService.Popup(error);
|
|
||||||
RunVisible = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnProcessExited(ProcessRunHandler<GameProcessStartInfoProvider> obj)
|
|
||||||
{
|
|
||||||
RunVisible = true;
|
|
||||||
if (_currentInstance == null) return;
|
|
||||||
|
|
||||||
_currentInstance.OnProcessExited -= OnProcessExited;
|
|
||||||
_currentInstance.Dispose();
|
|
||||||
_currentInstance = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void StopInstance()
|
|
||||||
{
|
|
||||||
_currentInstance?.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReadLog()
|
|
||||||
{
|
|
||||||
PopupMessageService.Popup(CurrLog);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async void ExpandInfoRequired()
|
|
||||||
{
|
|
||||||
ExpandInfo = !ExpandInfo;
|
|
||||||
if (Design.IsDesignMode) return;
|
|
||||||
|
|
||||||
var info = await GetServerInfo();
|
|
||||||
if (info == null) return;
|
|
||||||
|
|
||||||
Description = info.Desc;
|
|
||||||
|
|
||||||
Links.Clear();
|
|
||||||
if (info.Links is null) return;
|
|
||||||
foreach (var link in info.Links) Links.Add(link);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public class LinkGoCommand : ICommand
|
|
||||||
{
|
|
||||||
public LinkGoCommand()
|
|
||||||
{
|
|
||||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool CanExecute(object? parameter)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Execute(object? parameter)
|
|
||||||
{
|
|
||||||
if (parameter is not string str) return;
|
|
||||||
Helper.SafeOpenBrowser(str);
|
|
||||||
}
|
|
||||||
|
|
||||||
public event EventHandler? CanExecuteChanged;
|
|
||||||
}
|
|
||||||
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.Services;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.ViewModels.Popup;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Services;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ServerEntryView), false)]
|
||||||
|
public sealed partial class ServerEntryViewModel(
|
||||||
|
RestService restService,
|
||||||
|
CancellationService cancellationService,
|
||||||
|
GameRunnerService gameRunnerService
|
||||||
|
) :
|
||||||
|
ViewModelBase,
|
||||||
|
IFilterConsumer,
|
||||||
|
IListEntryModelView,
|
||||||
|
IFavoriteEntryModelView,
|
||||||
|
IEntryNameHolder,
|
||||||
|
IRunningSignalConsumer
|
||||||
|
{
|
||||||
|
[ObservableProperty] private string _description = "Fetching info...";
|
||||||
|
[ObservableProperty] private bool _expandInfo;
|
||||||
|
[ObservableProperty] private bool _isFavorite;
|
||||||
|
[ObservableProperty] private bool _isVisible;
|
||||||
|
[ObservableProperty] private bool _runVisible = true;
|
||||||
|
[ObservableProperty] private string _realName = string.Empty;
|
||||||
|
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
get => RealName;
|
||||||
|
set => RealName = value ?? Status.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ServerInfo? _serverInfo;
|
||||||
|
|
||||||
|
public RobustUrl Address { get; private set; }
|
||||||
|
|
||||||
|
public ServerStatus Status { get; private set; } =
|
||||||
|
new(
|
||||||
|
"Fetching data...",
|
||||||
|
"Loading...", [],
|
||||||
|
"",
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
-1,
|
||||||
|
false,
|
||||||
|
DateTime.Now,
|
||||||
|
-1
|
||||||
|
);
|
||||||
|
|
||||||
|
public ObservableCollection<ServerLink> Links { get; } = new();
|
||||||
|
public ObservableCollection<string> Tags { get; } = [];
|
||||||
|
public ICommand OnLinkGo { get; } = new LinkGoCommand();
|
||||||
|
|
||||||
|
public async Task<ServerInfo?> GetServerInfo()
|
||||||
|
{
|
||||||
|
if (_serverInfo != null)
|
||||||
|
return _serverInfo;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_serverInfo = await restService.GetAsync<ServerInfo>(Address.InfoUri, cancellationService.Token);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Description = e.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _serverInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
RealName = "TEST.TEST";
|
||||||
|
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
|
||||||
|
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
|
||||||
|
Status = new ServerStatus("Ameba",
|
||||||
|
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
|
||||||
|
["rp:hrp", "18+"],
|
||||||
|
"Antag", 15, 5, 1, false
|
||||||
|
, DateTime.Now, 100);
|
||||||
|
Address = "ss14://localhost";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ProcessFilter(ServerFilter? serverFilter)
|
||||||
|
{
|
||||||
|
if (serverFilter == null)
|
||||||
|
{
|
||||||
|
IsVisible = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetStatus(ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
Status = serverStatus;
|
||||||
|
Tags.Clear();
|
||||||
|
foreach (var tag in Status.Tags) Tags.Add(tag);
|
||||||
|
OnPropertyChanged(nameof(Status));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServerEntryViewModel WithData(RobustUrl url, string? name, ServerStatus serverStatus)
|
||||||
|
{
|
||||||
|
Address = url;
|
||||||
|
SetStatus(serverStatus);
|
||||||
|
Name = name;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OpenContentViewer()
|
||||||
|
{
|
||||||
|
gameRunnerService.OpenContentViewer(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ToggleFavorites()
|
||||||
|
{
|
||||||
|
IsFavorite = !IsFavorite;
|
||||||
|
if(IsFavorite)
|
||||||
|
gameRunnerService.AddFavorite(Address);
|
||||||
|
else
|
||||||
|
gameRunnerService.RemoveFavorite(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RunInstance()
|
||||||
|
{
|
||||||
|
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RunInstanceIgnoreAuth()
|
||||||
|
{
|
||||||
|
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void StopInstance()
|
||||||
|
{
|
||||||
|
gameRunnerService.StopInstance(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReadLog()
|
||||||
|
{
|
||||||
|
gameRunnerService.ReadInstanceLog(Address);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EditName()
|
||||||
|
{
|
||||||
|
gameRunnerService.EditName(Address, Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async void ExpandInfoRequired()
|
||||||
|
{
|
||||||
|
ExpandInfo = !ExpandInfo;
|
||||||
|
if (Design.IsDesignMode) return;
|
||||||
|
|
||||||
|
var info = await GetServerInfo();
|
||||||
|
if (info == null) return;
|
||||||
|
|
||||||
|
Description = info.Desc;
|
||||||
|
|
||||||
|
Links.Clear();
|
||||||
|
if (info.Links is null) return;
|
||||||
|
foreach (var link in info.Links) Links.Add(link);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void ProcessRunningSignal(bool isRunning)
|
||||||
|
{
|
||||||
|
RunVisible = !isRunning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class LinkGoCommand : ICommand
|
||||||
|
{
|
||||||
|
public LinkGoCommand()
|
||||||
|
{
|
||||||
|
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanExecute(object? parameter)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(object? parameter)
|
||||||
|
{
|
||||||
|
if (parameter is not string str) return;
|
||||||
|
Helper.SafeOpenBrowser(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
public event EventHandler? CanExecuteChanged;
|
||||||
|
}
|
||||||
57
Nebula.Launcher/ViewModels/ServerListViewModel.cs
Normal file
57
Nebula.Launcher/ViewModels/ServerListViewModel.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia.Collections;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
using Nebula.Launcher.Views;
|
||||||
|
using Nebula.Shared.ViewHelper;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
|
[ViewModelRegister(typeof(ServerListView), false)]
|
||||||
|
public class ServerListViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
public AvaloniaList<IListEntryModelView> ServerList { get; private set; } = new();
|
||||||
|
public AvaloniaList<Exception> ErrorList { get; private set; } = new();
|
||||||
|
public IServerListProvider? Provider { get; private set; }
|
||||||
|
|
||||||
|
public void ClearProvider()
|
||||||
|
{
|
||||||
|
foreach (var serverEntry in ServerList)
|
||||||
|
{
|
||||||
|
if (serverEntry is IDisposable disposable)
|
||||||
|
{
|
||||||
|
disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerList.Clear();
|
||||||
|
ErrorList.Clear();
|
||||||
|
GC.Collect();
|
||||||
|
GC.WaitForPendingFinalizers();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetProvider(IServerListProvider provider)
|
||||||
|
{
|
||||||
|
Provider = provider;
|
||||||
|
|
||||||
|
OnPropertyChanged(nameof(ServerList));
|
||||||
|
OnPropertyChanged(nameof(ErrorList));
|
||||||
|
|
||||||
|
RefreshFromProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshFromProvider()
|
||||||
|
{
|
||||||
|
Provider?.LoadServerList(ServerList, ErrorList);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void InitialiseInDesignMode()
|
||||||
|
{
|
||||||
|
SetProvider(new TestServerList());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Initialise()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,14 @@
|
|||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="Nebula.Launcher.Views.ExceptionView"
|
x:Class="Nebula.Launcher.Views.ExceptionView"
|
||||||
x:DataType="system:Exception"
|
x:DataType="viewModels:ExceptionCompound"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:system="clr-namespace:System;assembly=System.Runtime"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<system:Exception />
|
<viewModels:ExceptionCompound />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<Border
|
<Border
|
||||||
BoxShadow="{StaticResource DefaultShadow}"
|
BoxShadow="{StaticResource DefaultShadow}"
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System;
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
|
||||||
namespace Nebula.Launcher.Views;
|
namespace Nebula.Launcher.Views;
|
||||||
|
|
||||||
@@ -14,6 +16,6 @@ public partial class ExceptionView : UserControl
|
|||||||
|
|
||||||
public ExceptionView(Exception exception): this()
|
public ExceptionView(Exception exception): this()
|
||||||
{
|
{
|
||||||
DataContext = exception;
|
DataContext = new ExceptionCompound(exception);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</ListBox>
|
</ListBox>
|
||||||
<Button
|
<Button
|
||||||
Classes="ViewSelectButton"
|
Classes="ViewSelectButton"
|
||||||
Command="{Binding TriggerPaneCommand}"
|
Command="{Binding TriggerPane}"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Padding="5,0,5,0"
|
Padding="5,0,5,0"
|
||||||
@@ -178,7 +178,7 @@
|
|||||||
<Label Content="{Binding CurrentTitle}" VerticalAlignment="Center" />
|
<Label Content="{Binding CurrentTitle}" VerticalAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Button
|
<Button
|
||||||
Command="{Binding ClosePopupCommand}"
|
Command="{Binding CloseCurrentPopup}"
|
||||||
Content="X"
|
Content="X"
|
||||||
CornerRadius="0,10,0,0"
|
CornerRadius="0,10,0,0"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
ExtendClientAreaChromeHints="NoChrome"
|
ExtendClientAreaChromeHints="NoChrome"
|
||||||
ExtendClientAreaTitleBarHeightHint="-1"
|
ExtendClientAreaTitleBarHeightHint="-1"
|
||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
Height="500"
|
Height="550"
|
||||||
Icon="/Assets/nebula.ico"
|
Icon="/Assets/nebula.ico"
|
||||||
MinHeight="500"
|
MinHeight="550"
|
||||||
MinWidth="800"
|
MinWidth="800"
|
||||||
SystemDecorations="BorderOnly"
|
SystemDecorations="BorderOnly"
|
||||||
Title="Nebula.Launcher"
|
Title="Nebula.Launcher"
|
||||||
|
|||||||
@@ -188,14 +188,14 @@
|
|||||||
Margin="0,0,0,20"
|
Margin="0,0,0,20"
|
||||||
Path="/Assets/svg/user.svg" />
|
Path="/Assets/svg/user.svg" />
|
||||||
<Label>
|
<Label>
|
||||||
<StackPanel>
|
<StackPanel Spacing="15">
|
||||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
||||||
<customControls:LocalizedLabel LocalId="account-auth-hello"/>
|
<customControls:LocalizedLabel LocalId="account-auth-hello"/>
|
||||||
<TextBlock Text="{Binding Credentials.Value.Login}" />
|
<TextBlock Text="{Binding Credentials.Value.Login}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
<StackPanel HorizontalAlignment="Center" Orientation="Horizontal" Spacing="5">
|
||||||
<customControls:LocalizedLabel LocalId="account-auth-current-server"/>
|
<customControls:LocalizedLabel LocalId="account-auth-current-server"/>
|
||||||
<TextBlock Text="{Binding Credentials.Value.AuthServer}" />
|
<TextBlock Text="{Binding AuthServerName}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Label>
|
</Label>
|
||||||
|
|||||||
@@ -42,9 +42,10 @@
|
|||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
Child="{Binding CurrentServerList}"
|
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.RowSpan="2" />
|
Grid.RowSpan="2" >
|
||||||
|
<ContentControl Content="{Binding CurrentServerList}"></ContentControl>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="1"
|
<Border Grid.Row="1"
|
||||||
Background="{StaticResource DefaultGrad}"
|
Background="{StaticResource DefaultGrad}"
|
||||||
|
|||||||
@@ -10,33 +10,71 @@
|
|||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<popup:LoadingContextViewModel />
|
<popup:LoadingContextViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
<StackPanel Margin="25" Spacing="15">
|
<StackPanel Margin="25" Spacing="15" >
|
||||||
<ProgressBar Height="40" Maximum="{Binding CurrJobs}" Value="{Binding ResolvedJobs}" />
|
<Panel Margin="5">
|
||||||
<Panel>
|
<Border Padding="15" Background="{StaticResource DefaultGrad}" BoxShadow="0 1 1 0 #121212">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5" HorizontalAlignment="Left" VerticalAlignment="Center">
|
<Label VerticalAlignment="Center">
|
||||||
<Label>
|
<TextBlock Text="{Binding LoadingName}"/>
|
||||||
<TextBlock Text="{Binding ResolvedJobs}" />
|
|
||||||
</Label>
|
</Label>
|
||||||
<Label>
|
</Border>
|
||||||
/
|
|
||||||
</Label>
|
|
||||||
<Label>
|
|
||||||
<TextBlock Text="{Binding CurrJobs}" />
|
|
||||||
</Label>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Command="{Binding Cancel}"
|
Command="{Binding Cancel}"
|
||||||
IsVisible="{Binding IsCancellable}">
|
IsVisible="{Binding IsCancellable}">
|
||||||
<customControls:LocalizedLabel LocalId="task-cancel"/>
|
<Border Padding="15" Background="{StaticResource DefaultGrad}" BoxShadow="0 1 1 0 #121212">
|
||||||
|
<customControls:LocalizedLabel LocalId="task-cancel"/>
|
||||||
|
</Border>
|
||||||
</Button>
|
</Button>
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Grid ColumnDefinitions="*,*">
|
||||||
<Border Background="{StaticResource DefaultForeground}" MinHeight="210">
|
<ScrollViewer Grid.Column="0" Grid.ColumnSpan="{Binding LoadingColumnSize}">
|
||||||
<TextBlock TextWrapping="Wrap" Text="{Binding Message}" MaxLines="10" Margin="15"/>
|
<ItemsControl
|
||||||
</Border>
|
Background="#00000000"
|
||||||
</Panel>
|
ItemsSource="{Binding LoadingContexts}"
|
||||||
|
Padding="0" >
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<StackPanel Orientation="Vertical" Spacing="5" Margin="5" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate DataType="{x:Type popup:LoadingContext}">
|
||||||
|
<Border Background="{StaticResource DefaultGrad}" BoxShadow="0 1 1 0 #121212">
|
||||||
|
<StackPanel Margin="15">
|
||||||
|
<ProgressBar Height="40" Maximum="{Binding CurrJobs}" Value="{Binding ResolvedJobs}" />
|
||||||
|
<Panel Margin="5 15 5 5">
|
||||||
|
<Label HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding LoadingText}" />
|
||||||
|
</Label>
|
||||||
|
<Label HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding Message}" />
|
||||||
|
</Label>
|
||||||
|
</Panel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<StackPanel
|
||||||
|
Grid.Column="1"
|
||||||
|
Margin="10" Spacing="15"
|
||||||
|
IsVisible="{Binding ShowSpeed}">
|
||||||
|
<customControls:SimpleGraph Values="{Binding Values}"
|
||||||
|
Height="167"
|
||||||
|
GridBrush="{StaticResource DefaultForeground}"/>
|
||||||
|
<Border Background="{StaticResource DefaultGrad}" BoxShadow="0 1 1 0 #121212">
|
||||||
|
<Panel Margin="10">
|
||||||
|
<Label>Speed</Label>
|
||||||
|
<Label HorizontalAlignment="Right">
|
||||||
|
<TextBlock Text="{Binding SpeedText}" />
|
||||||
|
</Label>
|
||||||
|
</Panel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -70,9 +70,9 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Panel IsVisible="{Binding !Loading}">
|
<ContentControl
|
||||||
<views:ServerEntryView IsVisible="{Binding !Loading}" DataContext="{Binding CurrentEntry}"/>
|
IsVisible="{Binding !Loading}"
|
||||||
</Panel>
|
Content="{Binding CurrentEntry}"/>
|
||||||
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
x:Class="Nebula.Launcher.Views.ServerEntryView"
|
x:Class="Nebula.Launcher.Views.ServerEntryView"
|
||||||
x:DataType="viewModels:ServerEntryModelView"
|
x:DataType="viewModels:ServerEntryViewModel"
|
||||||
xmlns="https://github.com/avaloniaui"
|
xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
|
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
xmlns:services="clr-namespace:Nebula.Launcher.Services"
|
xmlns:services="clr-namespace:Nebula.Launcher.Services"
|
||||||
IsVisible="{Binding IsVisible}">
|
IsVisible="{Binding IsVisible}">
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<viewModels:ServerEntryModelView />
|
<viewModels:ServerEntryViewModel />
|
||||||
</Design.DataContext>
|
</Design.DataContext>
|
||||||
|
|
||||||
<Border
|
<Border
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
BoxShadow="0 0 13 -1 #121212"
|
BoxShadow="0 0 13 -1 #121212"
|
||||||
CornerRadius="10">
|
CornerRadius="10">
|
||||||
<Button
|
<Button
|
||||||
Command="{Binding $parent[views:ServerEntryView].((viewModels:ServerEntryModelView)DataContext).OnLinkGo}"
|
Command="{Binding $parent[views:ServerEntryView].((viewModels:ServerEntryViewModel)DataContext).OnLinkGo}"
|
||||||
CommandParameter="{Binding Url}"
|
CommandParameter="{Binding Url}"
|
||||||
Margin="3">
|
Margin="3">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
|||||||
@@ -2,19 +2,22 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Nebula.Launcher.Controls.ServerListView">
|
x:Class="Nebula.Launcher.Views.ServerListView"
|
||||||
|
x:DataType="viewModels1:ServerListViewModel">
|
||||||
|
<Design.DataContext>
|
||||||
|
<viewModels1:ServerListViewModel />
|
||||||
|
</Design.DataContext>
|
||||||
|
|
||||||
<ScrollViewer
|
<ScrollViewer
|
||||||
Margin="5,0,0,10"
|
Margin="5,0,0,10"
|
||||||
Padding="0,0,10,0">
|
Padding="0,0,10,0">
|
||||||
<StackPanel Margin="0,0,0,30">
|
<StackPanel Margin="0,0,0,30">
|
||||||
<Label x:Name="LoadingLabel" Margin="10" HorizontalAlignment="Center">Loading... Please wait</Label>
|
<ItemsControl ItemsSource="{Binding ErrorList}"
|
||||||
<ItemsControl
|
Margin="10,0,10,0" />
|
||||||
x:Name="ErrorList"
|
<ItemsControl ItemsSource="{Binding ServerList}"
|
||||||
Margin="10,0,10,0" />
|
Padding="0" />
|
||||||
<ItemsControl
|
|
||||||
x:Name="ServerList"
|
|
||||||
Padding="0" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
16
Nebula.Launcher/Views/ServerListView.axaml.cs
Normal file
16
Nebula.Launcher/Views/ServerListView.axaml.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Nebula.Launcher.Models;
|
||||||
|
using Nebula.Launcher.ServerListProviders;
|
||||||
|
using Nebula.Launcher.ViewModels;
|
||||||
|
using Nebula.Launcher.ViewModels.Pages;
|
||||||
|
|
||||||
|
namespace Nebula.Launcher.Views;
|
||||||
|
|
||||||
|
public partial class ServerListView : UserControl
|
||||||
|
{
|
||||||
|
public ServerListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Nebula.Packager/CommandLineParser.cs
Normal file
37
Nebula.Packager/CommandLineParser.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
namespace Nebula.Packager;
|
||||||
|
|
||||||
|
public class CommandLineParser
|
||||||
|
{
|
||||||
|
public string Configuration { get; set; } = "Release";
|
||||||
|
public string RootPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public static CommandLineParser Parse(IReadOnlyList<string> args)
|
||||||
|
{
|
||||||
|
using var enumerator = args.GetEnumerator();
|
||||||
|
|
||||||
|
var parsed = new CommandLineParser();
|
||||||
|
|
||||||
|
while (enumerator.MoveNext())
|
||||||
|
{
|
||||||
|
var arg = enumerator.Current;
|
||||||
|
|
||||||
|
if (arg == "--configuration")
|
||||||
|
{
|
||||||
|
if (!enumerator.MoveNext())
|
||||||
|
throw new InvalidOperationException("Missing args for --configuration");
|
||||||
|
|
||||||
|
parsed.Configuration = enumerator.Current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (arg == "--root-path")
|
||||||
|
{
|
||||||
|
if(!enumerator.MoveNext())
|
||||||
|
throw new InvalidOperationException("Missing args for --root-path");
|
||||||
|
|
||||||
|
parsed.RootPath = enumerator.Current;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
|
||||||
|
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -2,24 +2,36 @@
|
|||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
using Nebula.Shared;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Packager;
|
namespace Nebula.Packager;
|
||||||
public static class Program
|
public static class Program
|
||||||
{
|
{
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
{
|
{
|
||||||
Pack("","Release");
|
var parsedArgs = CommandLineParser.Parse(args);
|
||||||
|
|
||||||
|
Pack(parsedArgs.RootPath, parsedArgs.Configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Pack(string rootPath,string configuration)
|
private static string ShowEmptyOrValue(string? value)
|
||||||
{
|
{
|
||||||
|
if(string.IsNullOrWhiteSpace(value)) return "<empty>";
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Pack(string rootPath, string configuration)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Packaging with arguments: RootPath {ShowEmptyOrValue(rootPath)} and Configuration {configuration}");
|
||||||
|
|
||||||
var processInfo = new ProcessStartInfo
|
var processInfo = new ProcessStartInfo
|
||||||
{
|
{
|
||||||
FileName = "dotnet",
|
FileName = "dotnet",
|
||||||
ArgumentList =
|
ArgumentList =
|
||||||
{
|
{
|
||||||
"publish",
|
"publish",
|
||||||
Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"),
|
Path.Combine(rootPath, "Nebula.Launcher", "Nebula.Launcher.csproj"),
|
||||||
"-c", configuration,
|
"-c", configuration,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -57,18 +69,14 @@ public static class Program
|
|||||||
entries.Add(new LauncherManifestEntry(hashStr, fileNameCut));
|
entries.Add(new LauncherManifestEntry(hashStr, fileNameCut));
|
||||||
Console.WriteLine($"Added {hashStr} file name {fileNameCut}");
|
Console.WriteLine($"Added {hashStr} file name {fileNameCut}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var manifestRuntimeInfo = new LauncherRuntimeInfo(
|
||||||
|
CurrentConVar.DotnetVersion.DefaultValue!,
|
||||||
|
CurrentConVar.DotnetUrl.DefaultValue!
|
||||||
|
);
|
||||||
|
|
||||||
using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json"));
|
using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json"));
|
||||||
manifest.AutoFlush = true;
|
manifest.AutoFlush = true;
|
||||||
manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries)));
|
manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries, manifestRuntimeInfo)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public record struct LauncherManifest(
|
|
||||||
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries
|
|
||||||
);
|
|
||||||
|
|
||||||
public record struct LauncherManifestEntry(
|
|
||||||
[property: JsonPropertyName("hash")] string Hash,
|
|
||||||
[property: JsonPropertyName("path")] string Path
|
|
||||||
);
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using Nebula.Runner.Services;
|
using Nebula.Runner.Services;
|
||||||
using Nebula.Shared;
|
using Nebula.Shared;
|
||||||
using Nebula.Shared.Models;
|
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
@@ -16,6 +15,7 @@ public sealed class App(RunnerService runnerService, ContentService contentServi
|
|||||||
|
|
||||||
public void Redial(Uri uri, string text = "")
|
public void Redial(Uri uri, string text = "")
|
||||||
{
|
{
|
||||||
|
throw new Exception($"Redial requested. Reason: {text}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Run(string[] args1)
|
public async Task Run(string[] args1)
|
||||||
@@ -49,7 +49,7 @@ public sealed class App(RunnerService runnerService, ContentService contentServi
|
|||||||
args.Add("--ss14-address");
|
args.Add("--ss14-address");
|
||||||
args.Add(url.ToString());
|
args.Add(url.ToString());
|
||||||
|
|
||||||
await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandler(), cancelTokenSource.Token);
|
await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandlerFactory(), login, cancelTokenSource.Token);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
@@ -12,8 +10,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Lib.Harmony" Version="2.3.6" />
|
<PackageReference Include="Lib.Harmony"/>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0"/>
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
|
||||||
<PackageReference Include="SharpZstd.Interop" Version="1.5.6"/>
|
<PackageReference Include="SharpZstd.Interop"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -29,20 +29,37 @@ public class HarmonyService(ReflectionService reflectionService)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Я помню пенис большой,Я помню пенис большой, Я помню пенис большой, я помню....
|
/// Я не понимаю суть античитов в сосаке.
|
||||||
|
/// Эту хуйню может обойти любой школьник!
|
||||||
|
/// Нет.. я не хочу вводить читы, просто мне нужно поменять некоторые штучки :)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void UnShittyWizard()
|
private void UnShittyWizard()
|
||||||
{
|
{
|
||||||
var method = reflectionService.GetType("Robust.Client.GameController").TypeInitializer;
|
var method = reflectionService.GetType("Robust.Client.GameController").TypeInitializer;
|
||||||
_instance!.Harmony.Patch(method, new HarmonyMethod(Prefix));
|
_instance!.Harmony.Patch(method, new HarmonyMethod(IgnorePrefix));
|
||||||
|
|
||||||
|
var method2 = typeof(Type).Method(nameof(Type.GetType), new[] { typeof(string) });
|
||||||
|
_instance!.Harmony.Patch(method2, new HarmonyMethod(HidifyPrefix));
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool Prefix()
|
static bool IgnorePrefix()
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool HidifyPrefix(ref Type? __result, string typeName)
|
||||||
|
{
|
||||||
|
if (typeName.Contains("Harmony"))
|
||||||
|
{
|
||||||
|
__result = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class HarmonyInstance
|
public class HarmonyInstance
|
||||||
{
|
{
|
||||||
public readonly Harmony Harmony;
|
public readonly Harmony Harmony;
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ using Nebula.Shared;
|
|||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services;
|
using Nebula.Shared.Services;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.SharedModels;
|
||||||
using Robust.LoaderApi;
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
namespace Nebula.Runner.Services;
|
namespace Nebula.Runner.Services;
|
||||||
@@ -24,31 +26,31 @@ public sealed class RunnerService(
|
|||||||
private bool MetricEnabled = false; //TODO: ADD METRIC THINKS LATER
|
private bool MetricEnabled = false; //TODO: ADD METRIC THINKS LATER
|
||||||
|
|
||||||
public async Task Run(string[] runArgs, RobustBuildInfo buildInfo, IRedialApi redialApi,
|
public async Task Run(string[] runArgs, RobustBuildInfo buildInfo, IRedialApi redialApi,
|
||||||
ILoadingHandler loadingHandler,
|
ILoadingHandlerFactory loadingHandler, string? userDataPath = null,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_logger.Log("Start Content!");
|
_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)
|
if (engine is null)
|
||||||
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
throw new Exception("Engine version not found: " + buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
|
|
||||||
var hashApi = await contentService.EnsureItems(buildInfo.RobustManifestInfo, loadingHandler, cancellationToken);
|
var fileApi = await contentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
|
||||||
|
|
||||||
var extraMounts = new List<ApiMount>
|
var extraMounts = new List<ApiMount>
|
||||||
{
|
{
|
||||||
new(hashApi, "/")
|
new(fileApi, "/")
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hashApi.TryOpen("manifest.yml", out var stream))
|
if (fileApi.TryOpen("manifest.yml", out var stream))
|
||||||
{
|
{
|
||||||
var modules = ContentManifestParser.ExtractModules(stream);
|
var modules = ContentManifestParser.ExtractModules(stream);
|
||||||
|
|
||||||
foreach (var moduleStr in modules)
|
foreach (var moduleStr in modules)
|
||||||
{
|
{
|
||||||
var module =
|
var module =
|
||||||
await engineService.EnsureEngineModules(moduleStr, buildInfo.BuildInfo.Build.EngineVersion);
|
await engineService.EnsureEngineModules(moduleStr, loadingHandler, buildInfo.BuildInfo.Build.EngineVersion);
|
||||||
if (module is not null)
|
if (module is not null)
|
||||||
extraMounts.Add(new ApiMount(module, "/"));
|
extraMounts.Add(new ApiMount(module, "/"));
|
||||||
}
|
}
|
||||||
@@ -78,8 +80,14 @@ public sealed class RunnerService(
|
|||||||
MetricsEnabledPatcher.ApplyPatch(reflectionService, harmonyService);
|
MetricsEnabledPatcher.ApplyPatch(reflectionService, harmonyService);
|
||||||
metricServer = RunHelper.RunMetric(prometheusAssembly);
|
metricServer = RunHelper.RunMetric(prometheusAssembly);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userDataPath is not null)
|
||||||
|
{
|
||||||
|
UserDataDirPatcher.UserPath = userDataPath;
|
||||||
|
UserDataDirPatcher.ApplyPatch(reflectionService, harmonyService);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadingHandler.Dispose();
|
||||||
await Task.Run(() => loader.Main(args), cancellationToken);
|
await Task.Run(() => loader.Main(args), cancellationToken);
|
||||||
|
|
||||||
metricServer?.Dispose();
|
metricServer?.Dispose();
|
||||||
@@ -111,6 +119,38 @@ public static class MetricsEnabledPatcher
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public static class UserDataDirPatcher
|
||||||
|
{
|
||||||
|
public static string UserPath = "default";
|
||||||
|
|
||||||
|
public static void ApplyPatch(ReflectionService reflectionService, HarmonyService harmonyService)
|
||||||
|
{
|
||||||
|
var harmony = harmonyService.Instance.Harmony;
|
||||||
|
|
||||||
|
var targetType = reflectionService.GetType("Robust.Client.Utility.UserDataDir");
|
||||||
|
var targetMethod = targetType.GetMethod(
|
||||||
|
"GetRootUserDataDir",
|
||||||
|
BindingFlags.Static | BindingFlags.Public
|
||||||
|
) ?? throw new Exception("target method is null");
|
||||||
|
|
||||||
|
var prefix = typeof(UserDataDirPatcher).GetMethod(
|
||||||
|
nameof(GetRootUserDataDirPrefix),
|
||||||
|
BindingFlags.Static | BindingFlags.NonPublic
|
||||||
|
);
|
||||||
|
|
||||||
|
var prefixMethod = new HarmonyMethod(prefix);
|
||||||
|
|
||||||
|
harmony.Patch(targetMethod, prefix: prefixMethod);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool GetRootUserDataDirPrefix(ref string __result)
|
||||||
|
{
|
||||||
|
__result = Path.Join(AppDataPath.RootPath, "userData", UserPath);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class RunHelper
|
public static class RunHelper
|
||||||
{
|
{
|
||||||
public static IDisposable RunMetric(Assembly prometheusAssembly)
|
public static IDisposable RunMetric(Assembly prometheusAssembly)
|
||||||
@@ -140,44 +180,3 @@ public static class RunHelper
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ContentManifestParser
|
|
||||||
{
|
|
||||||
public static List<string> ExtractModules(Stream manifestStream)
|
|
||||||
{
|
|
||||||
using var reader = new StreamReader(manifestStream);
|
|
||||||
return ExtractModules(reader.ReadToEnd());
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<string> ExtractModules(string manifestContent)
|
|
||||||
{
|
|
||||||
var modules = new List<string>();
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,14 +8,14 @@ public static class CurrentConVar
|
|||||||
{
|
{
|
||||||
public static readonly ConVar<string[]> EngineManifestUrl =
|
public static readonly ConVar<string[]> EngineManifestUrl =
|
||||||
ConVarBuilder.Build<string[]>("engine.manifestUrl", [
|
ConVarBuilder.Build<string[]>("engine.manifestUrl", [
|
||||||
"https://harpy.durenko.tatar/manifests/manifest",
|
"https://feline.durenko.tatar/engine-cdn/manifest.json",
|
||||||
"https://robust-builds.fallback.cdn.spacestation14.com/manifest.json"
|
"https://robust-builds.fallback.cdn.spacestation14.com/manifest.json"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
public static readonly ConVar<string[]> EngineModuleManifestUrl =
|
public static readonly ConVar<string[]> EngineModuleManifestUrl =
|
||||||
ConVarBuilder.Build<string[]>("engine.moduleManifestUrl",
|
ConVarBuilder.Build<string[]>("engine.moduleManifestUrl",
|
||||||
[
|
[
|
||||||
"https://harpy.durenko.tatar/manifests/modules",
|
"https://feline.durenko.tatar/engine-cdn/modules.json",
|
||||||
"https://robust-builds.fallback.cdn.spacestation14.com/modules.json"
|
"https://robust-builds.fallback.cdn.spacestation14.com/modules.json"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@@ -34,8 +34,10 @@ public static class CurrentConVar
|
|||||||
|
|
||||||
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
|
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
|
||||||
new(){
|
new(){
|
||||||
{"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x64.zip"},
|
{"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x64.zip"},
|
||||||
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-win-x86.zip"},
|
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x86.zip"},
|
||||||
{"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/9.0.6/dotnet-runtime-9.0.6-linux-x64.tar.gz"}
|
{"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-linux-x64.tar.gz"}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
public static readonly ConVar<string> DotnetVersion = ConVarBuilder.Build<string>("dotnet.version", "10.0.2");
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using Nebula.Shared.FileApis.Interfaces;
|
using Nebula.Shared.FileApis.Interfaces;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
|
||||||
namespace Nebula.Shared.FileApis;
|
namespace Nebula.Shared.FileApis;
|
||||||
|
|
||||||
@@ -31,7 +33,7 @@ public sealed class FileApi : IReadWriteFileApi
|
|||||||
return false;
|
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);
|
var currPath = Path.Join(RootPath, path);
|
||||||
|
|
||||||
@@ -41,6 +43,13 @@ public sealed class FileApi : IReadWriteFileApi
|
|||||||
if (!dirInfo.Exists) dirInfo.Create();
|
if (!dirInfo.Exists) dirInfo.Create();
|
||||||
|
|
||||||
using var stream = new FileStream(currPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
using var stream = new FileStream(currPath, FileMode.Create, FileAccess.Write, FileShare.None);
|
||||||
|
|
||||||
|
if (loadingHandler != null)
|
||||||
|
{
|
||||||
|
input.CopyTo(stream, loadingHandler);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
input.CopyTo(stream);
|
input.CopyTo(stream);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ public class HashApi : IFileApi
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Save(RobustManifestItem item, Stream stream){
|
public bool Save(RobustManifestItem item, Stream stream, ILoadingHandler? loadingHandler){
|
||||||
return _fileApi.Save(GetManifestPath(item), stream);
|
return _fileApi.Save(GetManifestPath(item), stream, loadingHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Has(RobustManifestItem item){
|
public bool Has(RobustManifestItem item){
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
namespace Nebula.Shared.FileApis.Interfaces;
|
using Nebula.Shared.Models;
|
||||||
|
|
||||||
|
namespace Nebula.Shared.FileApis.Interfaces;
|
||||||
|
|
||||||
public interface IWriteFileApi
|
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 Remove(string path);
|
||||||
public bool Has(string path);
|
public bool Has(string path);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
namespace Nebula.Shared.Models;
|
namespace Nebula.Shared.Models;
|
||||||
|
|
||||||
public interface ILoadingHandler
|
public interface ILoadingHandler : IDisposable
|
||||||
{
|
{
|
||||||
public void SetJobsCount(int count);
|
public void SetJobsCount(long count);
|
||||||
public int GetJobsCount();
|
public long GetJobsCount();
|
||||||
|
|
||||||
public void SetResolvedJobsCount(int count);
|
public void SetResolvedJobsCount(long count);
|
||||||
public int GetResolvedJobsCount();
|
public long GetResolvedJobsCount();
|
||||||
public void SetLoadingMessage(string message);
|
public void SetLoadingMessage(string message);
|
||||||
|
|
||||||
public void AppendJob(int count = 1)
|
public void AppendJob(long count = 1)
|
||||||
{
|
{
|
||||||
SetJobsCount(GetJobsCount() + count);
|
SetJobsCount(GetJobsCount() + count);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AppendResolvedJob(int count = 1)
|
public void AppendResolvedJob(long count = 1)
|
||||||
{
|
{
|
||||||
SetResolvedJobsCount(GetResolvedJobsCount() + count);
|
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
|
public sealed class QueryJob : IDisposable
|
||||||
{
|
{
|
||||||
private readonly ILoadingHandler _handler;
|
private readonly ILoadingHandler _handler;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ namespace Nebula.Shared.Models;
|
|||||||
public class RobustBuildInfo
|
public class RobustBuildInfo
|
||||||
{
|
{
|
||||||
public ServerInfo BuildInfo = default!;
|
public ServerInfo BuildInfo = default!;
|
||||||
public RobustManifestInfo RobustManifestInfo;
|
public RobustManifestInfo? RobustManifestInfo;
|
||||||
|
public RobustZipContentInfo? DownloadUri;
|
||||||
public RobustUrl Url = default!;
|
public RobustUrl Url = default!;
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
namespace Nebula.Shared.Models;
|
namespace Nebula.Shared.Models;
|
||||||
|
|
||||||
public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash);
|
public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash);
|
||||||
|
public record struct RobustZipContentInfo(Uri DownloadUri, string Hash);
|
||||||
@@ -20,7 +20,7 @@ public sealed record BuildInfo(
|
|||||||
string ManifestDownloadUrl,
|
string ManifestDownloadUrl,
|
||||||
[property: JsonPropertyName("manifest_url")]
|
[property: JsonPropertyName("manifest_url")]
|
||||||
string ManifestUrl,
|
string ManifestUrl,
|
||||||
[property: JsonPropertyName("acz")] bool Acz,
|
[property: JsonPropertyName("acz")] bool? Acz,
|
||||||
[property: JsonPropertyName("hash")] string Hash,
|
[property: JsonPropertyName("hash")] string Hash,
|
||||||
[property: JsonPropertyName("manifest_hash")]
|
[property: JsonPropertyName("manifest_hash")]
|
||||||
string ManifestHash);
|
string ManifestHash);
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ public class RobustUrl
|
|||||||
return url.HttpUri;
|
return url.HttpUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static explicit operator RobustUrl(string url)
|
public static implicit operator RobustUrl(string url)
|
||||||
{
|
{
|
||||||
return new RobustUrl(url);
|
return new RobustUrl(url);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
@@ -11,12 +9,14 @@
|
|||||||
<EmbeddedResource Include="Utils\runtime.json">
|
<EmbeddedResource Include="Utils\runtime.json">
|
||||||
<LogicalName>Utility.runtime.json</LogicalName>
|
<LogicalName>Utility.runtime.json</LogicalName>
|
||||||
</EmbeddedResource>
|
</EmbeddedResource>
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
|
<PackageReference Include="JetBrains.Annotations" />
|
||||||
<PackageReference Include="Robust.Natives" Version="0.2.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"/>
|
||||||
<PackageReference Include="SharpZstd.Interop" Version="1.5.6" />
|
<PackageReference Include="Robust.Natives"/>
|
||||||
|
<PackageReference Include="SharpZstd.Interop"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
|
||||||
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||||
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
|
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using JetBrains.Annotations;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Nebula.Shared;
|
namespace Nebula.Shared;
|
||||||
@@ -42,6 +43,7 @@ public static class ServiceExt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MeansImplicitUse]
|
||||||
public sealed class ServiceRegisterAttribute : Attribute
|
public sealed class ServiceRegisterAttribute : Attribute
|
||||||
{
|
{
|
||||||
public ServiceRegisterAttribute(Type? inference = null, bool isSingleton = true)
|
public ServiceRegisterAttribute(Type? inference = null, bool isSingleton = true)
|
||||||
|
|||||||
@@ -32,19 +32,18 @@ public class ConfigurationService
|
|||||||
ConfigurationApi = fileService.CreateFileApi("config");
|
ConfigurationApi = fileService.CreateFileApi("config");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void MigrateConfigs(ILoadingHandler loadingHandler)
|
public void MigrateConfigs(ILoadingHandlerFactory loadingHandlerFactory)
|
||||||
{
|
{
|
||||||
Task.Run(async () =>
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
|
var loadingHandler = loadingHandlerFactory.CreateLoadingContext();
|
||||||
foreach (var migration in _migrations)
|
foreach (var migration in _migrations)
|
||||||
{
|
{
|
||||||
await migration.DoMigrate(this, _serviceProvider, loadingHandler);
|
await migration.DoMigrate(this, _serviceProvider, loadingHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loadingHandler is IDisposable disposable)
|
loadingHandler.Dispose();
|
||||||
{
|
loadingHandlerFactory.Dispose();
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.IO.Compression;
|
||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using Nebula.Shared.FileApis;
|
using Nebula.Shared.FileApis;
|
||||||
using Nebula.Shared.FileApis.Interfaces;
|
using Nebula.Shared.FileApis.Interfaces;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Utils;
|
using Nebula.Shared.Utils;
|
||||||
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
|
|
||||||
@@ -13,6 +17,7 @@ public partial class ContentService
|
|||||||
{
|
{
|
||||||
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
|
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
|
||||||
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
|
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
|
||||||
|
public readonly IReadWriteFileApi ZipContentApi = fileService.CreateFileApi("zipContent");
|
||||||
|
|
||||||
public void SetServerHash(string address, string hash)
|
public void SetServerHash(string address, string hash)
|
||||||
{
|
{
|
||||||
@@ -32,13 +37,24 @@ public partial class ContentService
|
|||||||
{
|
{
|
||||||
return new HashApi(manifestItems, ContentFileApi);
|
return new HashApi(manifestItems, ContentFileApi);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<IFileApi> EnsureItems(RobustBuildInfo info, ILoadingHandlerFactory loadingFactory,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (info.RobustManifestInfo.HasValue)
|
||||||
|
return await EnsureItems(info.RobustManifestInfo.Value, loadingFactory, cancellationToken);
|
||||||
|
|
||||||
|
if (info.DownloadUri.HasValue)
|
||||||
|
return await EnsureItems(info.DownloadUri.Value, loadingFactory, cancellationToken);
|
||||||
|
|
||||||
|
throw new InvalidOperationException("DownloadUri is null");
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
|
private async Task<HashApi> EnsureItems(ManifestReader manifestReader, Uri downloadUri,
|
||||||
ILoadingHandler loadingHandler,
|
ILoadingHandlerFactory loadingFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<RobustManifestItem> allItems = [];
|
List<RobustManifestItem> allItems = [];
|
||||||
List<RobustManifestItem> items = [];
|
|
||||||
|
|
||||||
while (manifestReader.TryReadItem(out var item))
|
while (manifestReader.TryReadItem(out var item))
|
||||||
{
|
{
|
||||||
@@ -50,46 +66,84 @@ public partial class ContentService
|
|||||||
|
|
||||||
var hashApi = CreateHashApi(allItems);
|
var hashApi = CreateHashApi(allItems);
|
||||||
|
|
||||||
items = allItems.Where(a=> !hashApi.Has(a)).ToList();
|
var items = allItems.Where(a=> !hashApi.Has(a)).ToList();
|
||||||
|
|
||||||
loadingHandler.SetLoadingMessage("Download Count:" + items.Count);
|
|
||||||
_logger.Log("Download Count:" + items.Count);
|
_logger.Log("Download Count:" + items.Count);
|
||||||
await Download(downloadUri, items, hashApi, loadingHandler, cancellationToken);
|
await Download(downloadUri, items, hashApi, loadingFactory, cancellationToken);
|
||||||
|
|
||||||
return hashApi;
|
return hashApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandler loadingHandler,
|
private async Task<ZipFileApi> EnsureItems(RobustZipContentInfo info, ILoadingHandlerFactory loadingFactory, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (TryFromFile(ZipContentApi, info.Hash, out var zipFile))
|
||||||
|
return zipFile;
|
||||||
|
|
||||||
|
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
|
||||||
|
|
||||||
|
var response = await _http.GetAsync(info.DownloadUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
loadingHandler.SetLoadingMessage("Downloading zip content");
|
||||||
|
loadingHandler.SetJobsCount(response.Content.Headers.ContentLength ?? 0);
|
||||||
|
await using var streamContent = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
|
ZipContentApi.Save(info.Hash, streamContent, loadingHandler);
|
||||||
|
loadingHandler.Dispose();
|
||||||
|
|
||||||
|
if (TryFromFile(ZipContentApi, info.Hash, out zipFile))
|
||||||
|
return zipFile;
|
||||||
|
|
||||||
|
ZipContentApi.Remove(info.Hash);
|
||||||
|
throw new Exception("Failed to load zip file");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFromFile(IFileApi fileApi, string path, [NotNullWhen(true)] out ZipFileApi? zipFileApi)
|
||||||
|
{
|
||||||
|
zipFileApi = null;
|
||||||
|
if(!fileApi.TryOpen(path, out var zipContent))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var zip = new ZipArchive(zipContent);
|
||||||
|
zipFileApi = new ZipFileApi(zip, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.Log("Getting manifest: " + info.Hash);
|
_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))
|
if (ManifestFileApi.TryOpen(info.Hash, out var stream))
|
||||||
{
|
{
|
||||||
_logger.Log("Loading manifest from: " + info.Hash);
|
_logger.Log("Loading manifest from disk");
|
||||||
return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingHandler, cancellationToken);
|
loadingHandler.Dispose();
|
||||||
|
return await EnsureItems(new ManifestReader(stream), info.DownloadUri, loadingFactory, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
SetServerHash(info.ManifestUri.ToString(), info.Hash);
|
SetServerHash(info.ManifestUri.ToString(), info.Hash);
|
||||||
|
|
||||||
_logger.Log("Fetching manifest from: " + info.ManifestUri);
|
_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);
|
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);
|
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);
|
streamContent.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
using var manifestReader = new ManifestReader(streamContent);
|
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)
|
public void Unpack(IFileApi hashApi, IWriteFileApi otherApi, ILoadingHandler loadingHandler)
|
||||||
{
|
{
|
||||||
_logger.Log("Unpack manifest files");
|
_logger.Log("Unpack manifest files");
|
||||||
var items = hashApi.Manifest.Values.ToList();
|
var items = hashApi.AllFiles.ToList();
|
||||||
loadingHandler.AppendJob(items.Count);
|
loadingHandler.AppendJob(items.Count);
|
||||||
|
|
||||||
var options = new ParallelOptions
|
var options = new ParallelOptions
|
||||||
@@ -101,36 +155,28 @@ public partial class ContentService
|
|||||||
{
|
{
|
||||||
if (hashApi.TryOpen(item, out var stream))
|
if (hashApi.TryOpen(item, out var stream))
|
||||||
{
|
{
|
||||||
_logger.Log($"Unpack {item.Hash} to: {item.Path}");
|
_logger.Log($"Unpack {item}");
|
||||||
otherApi.Save(item.Path, stream);
|
otherApi.Save(item, stream);
|
||||||
stream.Close();
|
stream.Close();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_logger.Error("OH FUCK!! " + item.Path);
|
_logger.Error($"Error while unpacking thinks {item}");
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingHandler.AppendResolvedJob();
|
loadingHandler.AppendResolvedJob();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (loadingHandler is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Download(Uri contentCdn, List<RobustManifestItem> toDownload, HashApi hashApi, ILoadingHandler loadingHandler,
|
private async Task Download(Uri contentCdn, List<RobustManifestItem> toDownload, HashApi hashApi, ILoadingHandlerFactory loadingHandlerFactory,
|
||||||
CancellationToken cancellationToken)
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (toDownload.Count == 0 || cancellationToken.IsCancellationRequested)
|
if (toDownload.Count == 0 || cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
_logger.Log("Nothing to download! Fuck this!");
|
_logger.Log("Nothing to download! Skip!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var downloadJobWatch = loadingHandler.GetQueryJob();
|
|
||||||
|
|
||||||
loadingHandler.SetLoadingMessage("Downloading from: " + contentCdn);
|
|
||||||
_logger.Log("Downloading from: " + contentCdn);
|
_logger.Log("Downloading from: " + contentCdn);
|
||||||
|
|
||||||
var requestBody = new byte[toDownload.Count * 4];
|
var requestBody = new byte[toDownload.Count * 4];
|
||||||
@@ -152,70 +198,56 @@ public partial class ContentService
|
|||||||
|
|
||||||
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("zstd"));
|
request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("zstd"));
|
||||||
var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
|
|
||||||
if (cancellationToken.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
_logger.Log("Downloading cancelled!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadJobWatch.Dispose();
|
|
||||||
|
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
var stream = await response.Content.ReadAsStreamAsync();
|
var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
var bandwidthStream = new BandwidthStream(stream);
|
|
||||||
stream = bandwidthStream;
|
|
||||||
if (response.Content.Headers.ContentEncoding.Contains("zstd"))
|
if (response.Content.Headers.ContentEncoding.Contains("zstd"))
|
||||||
stream = new ZStdDecompressStream(stream);
|
stream = new ZStdDecompressStream(stream);
|
||||||
|
|
||||||
await using var streamDispose = stream;
|
await using var streamDispose = stream;
|
||||||
|
|
||||||
// Read flags header
|
var streamHeader = await stream.ReadExactAsync(4, cancellationToken);
|
||||||
var streamHeader = await stream.ReadExactAsync(4, null);
|
|
||||||
var streamFlags = (DownloadStreamHeaderFlags)BinaryPrimitives.ReadInt32LittleEndian(streamHeader);
|
var streamFlags = (DownloadStreamHeaderFlags)BinaryPrimitives.ReadInt32LittleEndian(streamHeader);
|
||||||
var preCompressed = (streamFlags & DownloadStreamHeaderFlags.PreCompressed) != 0;
|
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 compressContext = preCompressed ? null : new ZStdCCtx();
|
||||||
var decompressContext = preCompressed ? new ZStdDCtx() : null;
|
var decompressContext = preCompressed ? new ZStdDCtx() : null;
|
||||||
|
|
||||||
// Normal file header:
|
|
||||||
// <int32> uncompressed length
|
|
||||||
// When preCompressed is set, we add:
|
|
||||||
// <int32> compressed length
|
|
||||||
var fileHeader = new byte[preCompressed ? 8 : 4];
|
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
|
try
|
||||||
{
|
{
|
||||||
// Buffer for storing compressed ZStd data.
|
|
||||||
var compressBuffer = new byte[1024];
|
var compressBuffer = new byte[1024];
|
||||||
|
|
||||||
// Buffer for storing uncompressed data.
|
|
||||||
var readBuffer = new byte[1024];
|
var readBuffer = new byte[1024];
|
||||||
|
|
||||||
var i = 0;
|
var i = 0;
|
||||||
|
var downloadWatchdog = new Stopwatch();
|
||||||
loadingHandler.AppendJob(toDownload.Count);
|
var lengthAcc = 0;
|
||||||
|
var timeAcc = 0L;
|
||||||
|
|
||||||
foreach (var item in toDownload)
|
foreach (var item in toDownload)
|
||||||
{
|
{
|
||||||
if (cancellationToken.IsCancellationRequested)
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
_logger.Log("Downloading cancelled!");
|
downloadWatchdog.Restart();
|
||||||
decompressContext?.Dispose();
|
|
||||||
compressContext?.Dispose();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read file header.
|
// Read file header.
|
||||||
await stream.ReadExactAsync(fileHeader, null);
|
await stream.ReadExactAsync(fileHeader, cancellationToken);
|
||||||
|
|
||||||
var length = BinaryPrimitives.ReadInt32LittleEndian(fileHeader.AsSpan(0, 4));
|
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);
|
EnsureBuffer(ref readBuffer, length);
|
||||||
var data = readBuffer.AsMemory(0, length);
|
var data = readBuffer.AsMemory(0, length);
|
||||||
|
|
||||||
@@ -226,9 +258,10 @@ public partial class ContentService
|
|||||||
|
|
||||||
if (compressedLength > 0)
|
if (compressedLength > 0)
|
||||||
{
|
{
|
||||||
|
fileLoadingHandler.AppendJob(compressedLength);
|
||||||
EnsureBuffer(ref compressBuffer, compressedLength);
|
EnsureBuffer(ref compressBuffer, compressedLength);
|
||||||
var compressedData = compressBuffer.AsMemory(0, 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.
|
// Decompress so that we can verify hash down below.
|
||||||
|
|
||||||
@@ -239,24 +272,53 @@ public partial class ContentService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await stream.ReadExactAsync(data, null);
|
fileLoadingHandler.AppendJob(length);
|
||||||
|
await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
await stream.ReadExactAsync(data, null);
|
fileLoadingHandler.AppendJob(length);
|
||||||
|
await stream.ReadExactAsync(data, cancellationToken, blockFileLoadHandle ? null : fileLoadingHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
using var fileStream = new MemoryStream(data.ToArray());
|
using var fileStream = new MemoryStream(data.ToArray());
|
||||||
hashApi.Save(item, fileStream);
|
hashApi.Save(item, fileStream, null);
|
||||||
|
|
||||||
_logger.Log("file saved:" + item.Path);
|
_logger.Log("file saved:" + item.Path);
|
||||||
loadingHandler.AppendResolvedJob();
|
fileLoadingHandler.Dispose();
|
||||||
|
downloadLoadHandler.AppendResolvedJob();
|
||||||
i += 1;
|
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
|
finally
|
||||||
{
|
{
|
||||||
|
downloadLoadHandler.Dispose();
|
||||||
decompressContext?.Dispose();
|
decompressContext?.Dispose();
|
||||||
compressContext?.Dispose();
|
compressContext?.Dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace Nebula.Shared.Services;
|
|||||||
|
|
||||||
public partial class ContentService
|
public partial class ContentService
|
||||||
{
|
{
|
||||||
public bool CheckMigration(ILoadingHandler loadingHandler)
|
public bool CheckMigration(ILoadingHandlerFactory loadingHandler)
|
||||||
{
|
{
|
||||||
_logger.Log("Checking migration...");
|
_logger.Log("Checking migration...");
|
||||||
|
|
||||||
@@ -17,16 +17,13 @@ public partial class ContentService
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoMigration(ILoadingHandler loadingHandler, List<string> migrationList)
|
private void DoMigration(ILoadingHandlerFactory loadingHandler, List<string> migrationList)
|
||||||
{
|
{
|
||||||
loadingHandler.SetJobsCount(migrationList.Count);
|
var mainLoadingHandler = loadingHandler.CreateLoadingContext();
|
||||||
|
mainLoadingHandler.SetJobsCount(migrationList.Count);
|
||||||
|
|
||||||
Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f,loadingHandler));
|
Parallel.ForEach(migrationList, (f,_)=>MigrateFile(f, mainLoadingHandler) );
|
||||||
|
loadingHandler.Dispose();
|
||||||
if (loadingHandler is IDisposable disposable)
|
|
||||||
{
|
|
||||||
disposable.Dispose();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void MigrateFile(string file, ILoadingHandler loadingHandler)
|
private void MigrateFile(string file, ILoadingHandler loadingHandler)
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ public partial class ContentService(
|
|||||||
info.Url = url;
|
info.Url = url;
|
||||||
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
|
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
|
||||||
info.BuildInfo = bi;
|
info.BuildInfo = bi;
|
||||||
info.RobustManifestInfo = info.BuildInfo.Build.Acz
|
|
||||||
|
if (info.BuildInfo.Build.Acz is null)
|
||||||
|
{
|
||||||
|
info.DownloadUri = new RobustZipContentInfo(new Uri(info.BuildInfo.Build.DownloadUrl), info.BuildInfo.Build.Hash);
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
info.RobustManifestInfo = info.BuildInfo.Build.Acz.Value
|
||||||
? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"),
|
? new RobustManifestInfo(new RobustPath(info.Url, "manifest.txt"), new RobustPath(info.Url, "download"),
|
||||||
bi.Build.ManifestHash)
|
bi.Build.ManifestHash)
|
||||||
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),
|
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ public class DebugService : IDisposable
|
|||||||
public static bool DoFileLog;
|
public static bool DoFileLog;
|
||||||
|
|
||||||
private readonly string _path =
|
private readonly string _path =
|
||||||
Path.Combine(FileService.RootPath, "log", Assembly.GetEntryAssembly()?.GetName().Name ?? "App");
|
Path.Combine(AppDataPath.RootPath, "log", Assembly.GetEntryAssembly()?.GetName().Name ?? "App");
|
||||||
|
|
||||||
public DebugService()
|
public DebugService()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,70 +1,61 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using Nebula.Shared.Utils;
|
||||||
|
using Nebula.SharedModels;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
|
|
||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public class DotnetResolverService(DebugService debugService, ConfigurationService configurationService)
|
public class DotnetResolverService(DebugService debugService, ConfigurationService configurationService)
|
||||||
{
|
{
|
||||||
private HttpClient _httpClient = new HttpClient();
|
private readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
private static readonly string FullPath = Path.Join(FileService.RootPath, "dotnet", DotnetUrlHelper.GetRuntimeIdentifier());
|
public async Task<string> EnsureDotnet(CancellationToken cancellationToken = default)
|
||||||
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
{
|
||||||
|
var dotnetEntry = new LauncherRuntimeInfo(
|
||||||
public async Task<string> EnsureDotnet(){
|
configurationService.GetConfigValue(CurrentConVar.DotnetVersion)!,
|
||||||
if(!Directory.Exists(FullPath))
|
configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
|
||||||
await Download();
|
);
|
||||||
|
|
||||||
return ExecutePath;
|
if (!File.Exists(dotnetEntry.GetExecutePath()))
|
||||||
|
await Download(dotnetEntry, cancellationToken);
|
||||||
|
|
||||||
|
return dotnetEntry.GetExecutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Download(){
|
private async Task Download(LauncherRuntimeInfo runtimeInfo, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var debugLogger = debugService.GetLogger(this);
|
||||||
|
debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||||
|
|
||||||
|
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
|
||||||
|
|
||||||
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
var fullPath = runtimeInfo.GetFullPath();
|
||||||
var ridExt =
|
|
||||||
DotnetUrlHelper.GetCurrentPlatformDotnetUrl(configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!);
|
UrlValidator.EnsureDomainValid(url, "microsoft.com");
|
||||||
using var response = await _httpClient.GetAsync(ridExt);
|
|
||||||
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync());
|
using var response = await _httpClient.GetAsync(url, cancellationToken);
|
||||||
Directory.CreateDirectory(FullPath);
|
response.EnsureSuccessStatusCode();
|
||||||
zipArchive.ExtractToDirectory(FullPath);
|
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||||
debugService.GetLogger("DotnetResolver").Log($"Downloading dotnet complete.");
|
|
||||||
|
Directory.CreateDirectory(fullPath);
|
||||||
|
|
||||||
|
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
await using var zipArchive = new ZipArchive(stream);
|
||||||
|
await zipArchive.ExtractToDirectoryAsync(fullPath, true, cancellationToken);
|
||||||
|
}
|
||||||
|
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
TarUtils.ExtractTarGz(stream, fullPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException("Unsupported archive format.");
|
||||||
|
}
|
||||||
|
|
||||||
|
debugLogger.Log("Downloading dotnet complete.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DotnetUrlHelper
|
|
||||||
{
|
|
||||||
public static string GetExtension()
|
|
||||||
{
|
|
||||||
if (OperatingSystem.IsWindows()) return ".exe";
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
|
||||||
{
|
|
||||||
string? rid = GetRuntimeIdentifier();
|
|
||||||
|
|
||||||
if (dotnetUrl.TryGetValue(rid, out var url))
|
|
||||||
{
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetRuntimeIdentifier()
|
|
||||||
{
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
|
||||||
{
|
|
||||||
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
|
||||||
{
|
|
||||||
return "linux-x64";
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new PlatformNotSupportedException("Unsupported operating system");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -108,11 +108,13 @@ public sealed class EngineService
|
|||||||
return info != null;
|
return info != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AssemblyApi?> EnsureEngine(string version)
|
public async Task<AssemblyApi?> EnsureEngine(string version, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_logger.Log("Ensure engine " + version);
|
_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
|
try
|
||||||
{
|
{
|
||||||
@@ -128,15 +130,24 @@ public sealed class EngineService
|
|||||||
return null;
|
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))
|
if (!TryGetVersionInfo(version, out var info))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Log("Downloading engine version " + version);
|
_logger.Log("Downloading engine version " + version);
|
||||||
|
loadingHandler.SetLoadingMessage("Downloading engine version " + version);
|
||||||
|
loadingHandler.Clear();
|
||||||
|
|
||||||
using var client = new HttpClient();
|
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();
|
await s.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +187,7 @@ public sealed class EngineService
|
|||||||
{
|
{
|
||||||
GetEngineInfo(out var modulesInfo, out var engineVersionInfo);
|
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 module = modulesInfo.Modules[moduleName];
|
||||||
var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv })
|
var selectedVersion = module.Versions.Select(kv => new { Version = Version.Parse(kv.Key), kv.Key, kv })
|
||||||
.Where(kv => engineVersionObj >= kv.Version)
|
.Where(kv => engineVersionObj >= kv.Version)
|
||||||
@@ -187,15 +198,18 @@ public sealed class EngineService
|
|||||||
return selectedVersion.Key;
|
return selectedVersion.Key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AssemblyApi?> EnsureEngineModules(string moduleName, string engineVersion)
|
public async Task<AssemblyApi?> EnsureEngineModules(string moduleName, ILoadingHandlerFactory loadingHandlerFactory, string engineVersion)
|
||||||
{
|
{
|
||||||
var moduleVersion = ResolveModuleVersion(moduleName, engineVersion);
|
var moduleVersion = ResolveModuleVersion(moduleName, engineVersion);
|
||||||
if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var buildInfo))
|
if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var buildInfo))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var fileName = ConcatName(moduleName, moduleVersion);
|
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
|
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))
|
if (!TryGetModuleBuildInfo(moduleName, moduleVersion, out var info))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Log("Downloading engine module version " + moduleVersion);
|
_logger.Log("Downloading engine module version " + moduleVersion);
|
||||||
|
loadingHandler.SetLoadingMessage("Downloading engine module version " + moduleVersion);
|
||||||
using var client = new HttpClient();
|
using var client = new HttpClient();
|
||||||
var s = await client.GetStreamAsync(info.Url);
|
var s = await client.GetStreamAsync(info.Url);
|
||||||
_engineFileApi.Save(ConcatName(moduleName, moduleVersion), s);
|
_engineFileApi.Save(ConcatName(moduleName, moduleVersion), s, loadingHandler);
|
||||||
await s.DisposeAsync();
|
await s.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ConcatName(string moduleName, string moduleVersion)
|
private string ConcatName(string moduleName, string moduleVersion)
|
||||||
{
|
{
|
||||||
return moduleName + "" + moduleVersion;
|
return moduleName + "" + moduleVersion;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using Nebula.Shared.FileApis;
|
using Nebula.Shared.FileApis;
|
||||||
using Nebula.Shared.FileApis.Interfaces;
|
using Nebula.Shared.FileApis.Interfaces;
|
||||||
using Nebula.Shared.Models;
|
using Nebula.Shared.Models;
|
||||||
using Nebula.Shared.Services.Logging;
|
using Nebula.Shared.Services.Logging;
|
||||||
|
using Nebula.SharedModels;
|
||||||
using Robust.LoaderApi;
|
using Robust.LoaderApi;
|
||||||
|
|
||||||
namespace Nebula.Shared.Services;
|
namespace Nebula.Shared.Services;
|
||||||
@@ -10,23 +12,20 @@ namespace Nebula.Shared.Services;
|
|||||||
[ServiceRegister]
|
[ServiceRegister]
|
||||||
public class FileService
|
public class FileService
|
||||||
{
|
{
|
||||||
public static string RootPath = Path.Join(Environment.GetFolderPath(
|
|
||||||
Environment.SpecialFolder.ApplicationData), "Datum");
|
|
||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public FileService(DebugService debugService)
|
public FileService(DebugService debugService)
|
||||||
{
|
{
|
||||||
_logger = debugService.GetLogger(this);
|
_logger = debugService.GetLogger(this);
|
||||||
|
|
||||||
if(!Directory.Exists(RootPath))
|
if(!Directory.Exists(AppDataPath.RootPath))
|
||||||
Directory.CreateDirectory(RootPath);
|
Directory.CreateDirectory(AppDataPath.RootPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadWriteFileApi CreateFileApi(string path)
|
public IReadWriteFileApi CreateFileApi(string path)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Creating file api for {path}");
|
_logger.Debug($"Creating file api for {path}");
|
||||||
return new FileApi(Path.Join(RootPath, path));
|
return new FileApi(Path.Join(AppDataPath.RootPath, path));
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadWriteFileApi EnsureTempDir(out string path)
|
public IReadWriteFileApi EnsureTempDir(out string path)
|
||||||
@@ -59,7 +58,7 @@ public class FileService
|
|||||||
public void RemoveAllFiles(string fileApiName,ILoadingHandler loadingHandler, CancellationToken cancellationToken)
|
public void RemoveAllFiles(string fileApiName,ILoadingHandler loadingHandler, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.Debug($"Deleting files from {fileApiName}");
|
_logger.Debug($"Deleting files from {fileApiName}");
|
||||||
var path = Path.Combine(RootPath, fileApiName);
|
var path = Path.Combine(AppDataPath.RootPath, fileApiName);
|
||||||
|
|
||||||
var di = new DirectoryInfo(path);
|
var di = new DirectoryInfo(path);
|
||||||
|
|
||||||
@@ -89,14 +88,27 @@ 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
|
public sealed class ConsoleLoadingHandler : ILoadingHandler
|
||||||
{
|
{
|
||||||
private int _currJobs;
|
private long _currJobs;
|
||||||
|
|
||||||
private float _percent;
|
private float _percent;
|
||||||
private int _resolvedJobs;
|
private long _resolvedJobs;
|
||||||
|
|
||||||
public void SetJobsCount(int count)
|
public void SetJobsCount(long count)
|
||||||
{
|
{
|
||||||
_currJobs = count;
|
_currJobs = count;
|
||||||
|
|
||||||
@@ -104,12 +116,12 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler
|
|||||||
Draw();
|
Draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetJobsCount()
|
public long GetJobsCount()
|
||||||
{
|
{
|
||||||
return _currJobs;
|
return _currJobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetResolvedJobsCount(int count)
|
public void SetResolvedJobsCount(long count)
|
||||||
{
|
{
|
||||||
_resolvedJobs = count;
|
_resolvedJobs = count;
|
||||||
|
|
||||||
@@ -117,7 +129,7 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler
|
|||||||
Draw();
|
Draw();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int GetResolvedJobsCount()
|
public long GetResolvedJobsCount()
|
||||||
{
|
{
|
||||||
return _resolvedJobs;
|
return _resolvedJobs;
|
||||||
}
|
}
|
||||||
@@ -154,4 +166,9 @@ public sealed class ConsoleLoadingHandler : ILoadingHandler
|
|||||||
|
|
||||||
Console.Write($"\t {_resolvedJobs}/{_currJobs}");
|
Console.Write($"\t {_resolvedJobs}/{_currJobs}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -29,10 +29,7 @@ public class RestService
|
|||||||
[Pure]
|
[Pure]
|
||||||
public async Task<T> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
public async Task<T> GetAsync<T>(Uri uri, CancellationToken cancellationToken) where T : notnull
|
||||||
{
|
{
|
||||||
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri)
|
var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, uri);
|
||||||
{
|
|
||||||
Version = HttpVersion.Version10,
|
|
||||||
};
|
|
||||||
|
|
||||||
var response = await _client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
var response = await _client.SendAsync(httpRequestMessage, cancellationToken).ConfigureAwait(false);
|
||||||
return await ReadResult<T>(response, cancellationToken, uri);
|
return await ReadResult<T>(response, cancellationToken, uri);
|
||||||
|
|||||||
@@ -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<int> ReadAsync(Memory<byte> 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<byte> buffer,
|
|
||||||
CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
await _baseStream.WriteAsync(buffer, cancellationToken);
|
|
||||||
TrackBandwidth(buffer.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
43
Nebula.Shared/Utils/ContentManifestParser.cs
Normal file
43
Nebula.Shared/Utils/ContentManifestParser.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
namespace Nebula.Shared.Utils;
|
||||||
|
|
||||||
|
public static class ContentManifestParser
|
||||||
|
{
|
||||||
|
public static List<string> ExtractModules(Stream manifestStream)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(manifestStream);
|
||||||
|
return ExtractModules(reader.ReadToEnd());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<string> ExtractModules(string manifestContent)
|
||||||
|
{
|
||||||
|
var modules = new List<string>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,21 +1,37 @@
|
|||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
|
using Nebula.Shared.Models;
|
||||||
|
|
||||||
namespace Nebula.Shared.Utils;
|
namespace Nebula.Shared.Utils;
|
||||||
|
|
||||||
public static class StreamHelper
|
public static class StreamHelper
|
||||||
{
|
{
|
||||||
public static async ValueTask<byte[]> 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<byte[]> ReadExactAsync(this Stream stream, int amount, CancellationToken cancel = default)
|
||||||
{
|
{
|
||||||
var data = new byte[amount];
|
var data = new byte[amount];
|
||||||
await ReadExactAsync(stream, data, cancel);
|
await ReadExactAsync(stream, data, cancel);
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async ValueTask ReadExactAsync(this Stream stream, Memory<byte> into, CancellationToken? cancel)
|
public static async ValueTask ReadExactAsync(this Stream stream, Memory<byte> into, CancellationToken cancel = default, ILoadingHandler? loadingHandler = null)
|
||||||
{
|
{
|
||||||
while (into.Length > 0)
|
while (into.Length > 0)
|
||||||
{
|
{
|
||||||
var read = await stream.ReadAsync(into);
|
var read = await stream.ReadAsync(into, cancel);
|
||||||
|
|
||||||
|
loadingHandler?.AppendResolvedJob(read);
|
||||||
|
|
||||||
// Check EOF.
|
// Check EOF.
|
||||||
if (read == 0)
|
if (read == 0)
|
||||||
@@ -24,31 +40,4 @@ public static class StreamHelper
|
|||||||
into = into[read..];
|
into = into[read..];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task CopyAmountToAsync(
|
|
||||||
this Stream stream,
|
|
||||||
Stream to,
|
|
||||||
int amount,
|
|
||||||
int bufferSize,
|
|
||||||
CancellationToken cancel)
|
|
||||||
{
|
|
||||||
var buffer = ArrayPool<byte>.Shared.Rent(bufferSize);
|
|
||||||
|
|
||||||
while (amount > 0)
|
|
||||||
{
|
|
||||||
Memory<byte> 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
393
Nebula.Shared/Utils/TarUtils.cs
Normal file
393
Nebula.Shared/Utils/TarUtils.cs
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace Nebula.Shared.Utils;
|
||||||
|
|
||||||
|
public static class TarUtils
|
||||||
|
{
|
||||||
|
public static void ExtractTarGz(Stream stream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
using (var gzs = new GZipStream(stream, CompressionMode.Decompress, leaveOpen: false))
|
||||||
|
{
|
||||||
|
// GZipStream does not expose length, so just pass as streaming source
|
||||||
|
TarExtractor.ExtractTar(gzs, destinationDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class TarExtractor
|
||||||
|
{
|
||||||
|
private const int BlockSize = 512;
|
||||||
|
|
||||||
|
public static void ExtractTar(Stream tarStream, string destinationDirectory)
|
||||||
|
{
|
||||||
|
if (tarStream == null) throw new ArgumentNullException(nameof(tarStream));
|
||||||
|
if (destinationDirectory == null) throw new ArgumentNullException(nameof(destinationDirectory));
|
||||||
|
|
||||||
|
Directory.CreateDirectory(destinationDirectory);
|
||||||
|
|
||||||
|
string pendingLongName = null;
|
||||||
|
string pendingLongLink = null;
|
||||||
|
|
||||||
|
var block = new byte[BlockSize];
|
||||||
|
var zeroBlockCount = 0;
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var read = ReadExactly(tarStream, block, 0, BlockSize);
|
||||||
|
if (read == 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
if (IsAllZero(block))
|
||||||
|
{
|
||||||
|
zeroBlockCount++;
|
||||||
|
if (zeroBlockCount >= 2) break; // two consecutive zero blocks -> end of archive
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
zeroBlockCount = 0;
|
||||||
|
|
||||||
|
var header = TarHeader.FromBlock(block);
|
||||||
|
|
||||||
|
// validate header checksum (best-effort)
|
||||||
|
if (!header.IsValidChecksum(block))
|
||||||
|
{
|
||||||
|
// Not fatal, but warn (we're not writing warnings to console by default).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some tar implementations supply the long filename in a preceding entry whose typeflag is 'L'.
|
||||||
|
// If present, use that name for the following file.
|
||||||
|
if (header.TypeFlag == 'L') // GNU long name
|
||||||
|
{
|
||||||
|
// read content blocks with size header.Size
|
||||||
|
var size = header.Size;
|
||||||
|
var nameBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, nameBytes, 0, (int)size);
|
||||||
|
// skip padding to full 512 block
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongName = ReadNullTerminatedString(nameBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (header.TypeFlag == 'K') // GNU long linkname
|
||||||
|
{
|
||||||
|
var size = header.Size;
|
||||||
|
var linkBytes = new byte[size];
|
||||||
|
ReadExactly(tarStream, linkBytes, 0, (int)size);
|
||||||
|
SkipPadding(tarStream, size);
|
||||||
|
pendingLongLink = ReadNullTerminatedString(linkBytes);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine final name
|
||||||
|
var entryName = !string.IsNullOrEmpty(pendingLongName) ? pendingLongName : header.GetName();
|
||||||
|
var entryLinkName = !string.IsNullOrEmpty(pendingLongLink) ? pendingLongLink : header.LinkName;
|
||||||
|
|
||||||
|
// reset pending longs after use
|
||||||
|
pendingLongName = null;
|
||||||
|
pendingLongLink = null;
|
||||||
|
|
||||||
|
// sanitize path separators and avoid absolute paths
|
||||||
|
entryName = entryName.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar)
|
||||||
|
.TrimStart(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
var targetPath = Path.Combine(destinationDirectory, entryName);
|
||||||
|
|
||||||
|
switch (header.TypeFlag)
|
||||||
|
{
|
||||||
|
case '0':
|
||||||
|
case '\0': // normal file
|
||||||
|
case '7': // regular file (SUSv4)
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '5': // directory
|
||||||
|
Directory.CreateDirectory(targetPath);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '2': // symlink
|
||||||
|
// Creating symlinks require privileges on Windows and may fail.
|
||||||
|
// To keep things robust across platforms, write a small .symlink-info file for Windows fallback,
|
||||||
|
// and attempt real symlink creation on Unix-like platforms.
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
TryCreateSymlink(entryLinkName, targetPath);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '1': // hard link - we will try to create by copying if target exists; otherwise skip
|
||||||
|
var linkTargetPath = Path.Combine(destinationDirectory,
|
||||||
|
entryLinkName.Replace('/', Path.DirectorySeparatorChar));
|
||||||
|
if (File.Exists(linkTargetPath))
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
File.Copy(linkTargetPath, targetPath, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case '3': // character device - skip
|
||||||
|
case '4': // block device - skip
|
||||||
|
case '6': // contiguous file - treat as regular
|
||||||
|
// To be safe, treat as file if size > 0
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
EnsureParentDirectoryExists(targetPath);
|
||||||
|
using (var outFile = File.Open(targetPath, FileMode.Create, FileAccess.Write))
|
||||||
|
{
|
||||||
|
CopyExact(tarStream, outFile, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
TrySetTimes(targetPath, header.ModTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// Unknown type - skip the file data
|
||||||
|
if (header.Size > 0)
|
||||||
|
{
|
||||||
|
Skip(tarStream, header.Size);
|
||||||
|
SkipPadding(tarStream, header.Size);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryCreateSymlink(string linkTarget, string symlinkPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// On Unix-like systems we can try to create a symlink
|
||||||
|
if (IsWindows())
|
||||||
|
{
|
||||||
|
// don't try symlinks on Windows by default - write a .symlink-info file instead
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Unix: use symlink
|
||||||
|
var dir = Path.GetDirectoryName(symlinkPath);
|
||||||
|
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
|
||||||
|
// Use native syscall via Mono.Posix? Not allowed. Fall back to invoking 'ln -s' is not allowed.
|
||||||
|
// Instead use System.IO.File.CreateSymbolicLink if available (net core 2.1+)
|
||||||
|
#if NETCOREAPP2_1_OR_GREATER || NET5_0_OR_GREATER
|
||||||
|
var sym = new FileInfo(symlinkPath);
|
||||||
|
sym.CreateAsSymbolicLink(linkTarget);
|
||||||
|
#else
|
||||||
|
// If unavailable, write a .symlink-info file.
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore failures to create symlink; write fallback info
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.WriteAllText(symlinkPath + ".symlink-info", $"symlink -> {linkTarget}");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWindows()
|
||||||
|
{
|
||||||
|
return Path.DirectorySeparatorChar == '\\';
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TrySetTimes(string path, DateTimeOffset modTime)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dt = modTime.UtcDateTime;
|
||||||
|
// convert to local to set file time sensibly
|
||||||
|
File.SetLastWriteTimeUtc(path, dt);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
/* best-effort */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EnsureParentDirectoryExists(string path)
|
||||||
|
{
|
||||||
|
var dir = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||||
|
Directory.CreateDirectory(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read exactly count bytes or throw if cannot
|
||||||
|
private static int ReadExactly(Stream s, byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
var total = 0;
|
||||||
|
while (total < count)
|
||||||
|
{
|
||||||
|
var r = s.Read(buffer, offset + total, count - total);
|
||||||
|
if (r == 0) break;
|
||||||
|
total += r;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip count bytes by reading and discarding
|
||||||
|
private static void Skip(Stream s, long count)
|
||||||
|
{
|
||||||
|
var tmp = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(tmp.Length, remaining);
|
||||||
|
var r = s.Read(tmp, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CopyExact(Stream source, Stream dest, long count)
|
||||||
|
{
|
||||||
|
var buf = new byte[8192];
|
||||||
|
var remaining = count;
|
||||||
|
while (remaining > 0)
|
||||||
|
{
|
||||||
|
var toRead = (int)Math.Min(buf.Length, remaining);
|
||||||
|
var r = source.Read(buf, 0, toRead);
|
||||||
|
if (r == 0) break;
|
||||||
|
dest.Write(buf, 0, r);
|
||||||
|
remaining -= r;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SkipPadding(Stream s, long size)
|
||||||
|
{
|
||||||
|
var pad = (BlockSize - size % BlockSize) % BlockSize;
|
||||||
|
if (pad > 0) Skip(s, pad);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAllZero(byte[] block)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (block[i] != 0)
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadNullTerminatedString(byte[] bytes)
|
||||||
|
{
|
||||||
|
var len = 0;
|
||||||
|
while (len < bytes.Length && bytes[len] != 0) len++;
|
||||||
|
return Encoding.UTF8.GetString(bytes, 0, len);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TarHeader
|
||||||
|
{
|
||||||
|
public string Name { get; private set; }
|
||||||
|
public int Mode { get; private set; }
|
||||||
|
public int Uid { get; private set; }
|
||||||
|
public int Gid { get; private set; }
|
||||||
|
public long Size { get; private set; }
|
||||||
|
public DateTimeOffset ModTime { get; private set; }
|
||||||
|
public int Checksum { get; private set; }
|
||||||
|
public char TypeFlag { get; private set; }
|
||||||
|
public string LinkName { get; private set; }
|
||||||
|
public string Magic { get; private set; }
|
||||||
|
public string Version { get; private set; }
|
||||||
|
public string UName { get; private set; }
|
||||||
|
public string GName { get; private set; }
|
||||||
|
public string DevMajor { get; private set; }
|
||||||
|
public string DevMinor { get; private set; }
|
||||||
|
public string Prefix { get; private set; }
|
||||||
|
|
||||||
|
public static TarHeader FromBlock(byte[] block)
|
||||||
|
{
|
||||||
|
var h = new TarHeader();
|
||||||
|
h.Name = ReadString(block, 0, 100);
|
||||||
|
h.Mode = (int)ReadOctal(block, 100, 8);
|
||||||
|
h.Uid = (int)ReadOctal(block, 108, 8);
|
||||||
|
h.Gid = (int)ReadOctal(block, 116, 8);
|
||||||
|
h.Size = ReadOctal(block, 124, 12);
|
||||||
|
var mtime = ReadOctal(block, 136, 12);
|
||||||
|
h.ModTime = DateTimeOffset.FromUnixTimeSeconds(mtime);
|
||||||
|
h.Checksum = (int)ReadOctal(block, 148, 8);
|
||||||
|
h.TypeFlag = (char)block[156];
|
||||||
|
h.LinkName = ReadString(block, 157, 100);
|
||||||
|
h.Magic = ReadString(block, 257, 6);
|
||||||
|
h.Version = ReadString(block, 263, 2);
|
||||||
|
h.UName = ReadString(block, 265, 32);
|
||||||
|
h.GName = ReadString(block, 297, 32);
|
||||||
|
h.DevMajor = ReadString(block, 329, 8);
|
||||||
|
h.DevMinor = ReadString(block, 337, 8);
|
||||||
|
h.Prefix = ReadString(block, 345, 155);
|
||||||
|
|
||||||
|
return h;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetName()
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(Prefix))
|
||||||
|
return $"{Prefix}/{Name}".Trim('/');
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsValidChecksum(byte[] block)
|
||||||
|
{
|
||||||
|
// compute checksum where checksum field (148..155) is spaces (0x20)
|
||||||
|
long sum = 0;
|
||||||
|
for (var i = 0; i < block.Length; i++)
|
||||||
|
if (i >= 148 && i < 156) sum += 32; // space
|
||||||
|
else sum += block[i];
|
||||||
|
|
||||||
|
// stored checksum could be octal until null
|
||||||
|
var stored = Checksum;
|
||||||
|
return Math.Abs(sum - stored) <= 1; // allow +/-1 tolerance
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadString(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
var end = offset;
|
||||||
|
var max = offset + length;
|
||||||
|
while (end < max && buf[end] != 0) end++;
|
||||||
|
if (end == offset) return string.Empty;
|
||||||
|
return Encoding.ASCII.GetString(buf, offset, end - offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long ReadOctal(byte[] buf, int offset, int length)
|
||||||
|
{
|
||||||
|
// Many tars store as ASCII octal, possibly padded with nulls or spaces.
|
||||||
|
var end = offset + length;
|
||||||
|
var i = offset;
|
||||||
|
// skip leading spaces and nulls
|
||||||
|
while (i < end && (buf[i] == 0 || buf[i] == (byte)' ')) i++;
|
||||||
|
long val = 0;
|
||||||
|
var found = false;
|
||||||
|
for (; i < end; i++)
|
||||||
|
{
|
||||||
|
var b = buf[i];
|
||||||
|
if (b == 0 || b == (byte)' ') break;
|
||||||
|
if (b >= (byte)'0' && b <= (byte)'7')
|
||||||
|
{
|
||||||
|
found = true;
|
||||||
|
val = (val << 3) + (b - (byte)'0');
|
||||||
|
}
|
||||||
|
// some implementations use base-10 ascii or binary; ignore invalid chars
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) return 0;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
124
Nebula.SharedModels/AppDataHelper.cs
Normal file
124
Nebula.SharedModels/AppDataHelper.cs
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Nebula.SharedModels;
|
||||||
|
|
||||||
|
public static class AppDataPath
|
||||||
|
{
|
||||||
|
public static string RootPath { get; private set; } = GetAppDataPath("Datum");
|
||||||
|
|
||||||
|
public static void SetTestRootPath(string rootPath)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"REWRITE ROOT PATH TO {rootPath}");
|
||||||
|
RootPath = rootPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetAppDataPath(string appName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(appName))
|
||||||
|
throw new ArgumentException("appName cannot be null or empty.", nameof(appName));
|
||||||
|
|
||||||
|
string basePath;
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
basePath = Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||||
|
"Library",
|
||||||
|
"Application Support"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")
|
||||||
|
?? Path.Combine(
|
||||||
|
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||||
|
".config"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(basePath, appName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class UrlValidator
|
||||||
|
{
|
||||||
|
public static bool IsInDomainUrl(string url, string allowedDomain)
|
||||||
|
{
|
||||||
|
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (uri.Scheme != Uri.UriSchemeHttps)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var host = uri.Host.ToLowerInvariant();
|
||||||
|
return host == allowedDomain || host.EndsWith("." + allowedDomain);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void EnsureDomainValid(string url, string allowedDomain)
|
||||||
|
{
|
||||||
|
if(!IsInDomainUrl(url, allowedDomain))
|
||||||
|
throw new InvalidOperationException($"URL {url} is not in domain {allowedDomain}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DotnetUrlHelper
|
||||||
|
{
|
||||||
|
public static string GetExtension()
|
||||||
|
{
|
||||||
|
return OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
||||||
|
}
|
||||||
|
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
||||||
|
{
|
||||||
|
var rid = GetRuntimeIdentifier();
|
||||||
|
|
||||||
|
if (dotnetUrl.TryGetValue(rid, out var url)) return url;
|
||||||
|
|
||||||
|
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetRuntimeIdentifier()
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "win-x64",
|
||||||
|
Architecture.X86 => "win-x86",
|
||||||
|
Architecture.Arm64 => "win-arm64",
|
||||||
|
_ => throw new PlatformNotSupportedException($"Unsupported Windows architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||||
|
{
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "linux-x64",
|
||||||
|
Architecture.X86 => "linux-x86",
|
||||||
|
Architecture.Arm => "linux-arm",
|
||||||
|
Architecture.Arm64 => "linux-arm64",
|
||||||
|
_ => throw new PlatformNotSupportedException($"Unsupported Linux architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||||
|
{
|
||||||
|
return RuntimeInformation.ProcessArchitecture switch
|
||||||
|
{
|
||||||
|
Architecture.X64 => "osx-x64",
|
||||||
|
Architecture.Arm64 => "osx-arm64",
|
||||||
|
_ => throw new PlatformNotSupportedException($"Unsupported macOS architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}");
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Nebula.SharedModels/LauncherManifest.cs
Normal file
8
Nebula.SharedModels/LauncherManifest.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nebula.SharedModels;
|
||||||
|
|
||||||
|
public record struct LauncherManifest(
|
||||||
|
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries,
|
||||||
|
[property: JsonPropertyName("runtime_info")] LauncherRuntimeInfo RuntimeInfo
|
||||||
|
);
|
||||||
8
Nebula.SharedModels/LauncherManifestEntry.cs
Normal file
8
Nebula.SharedModels/LauncherManifestEntry.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nebula.SharedModels;
|
||||||
|
|
||||||
|
public record struct LauncherManifestEntry(
|
||||||
|
[property: JsonPropertyName("hash")] string Hash,
|
||||||
|
[property: JsonPropertyName("path")] string Path
|
||||||
|
);
|
||||||
23
Nebula.SharedModels/LauncherRuntimeInfo.cs
Normal file
23
Nebula.SharedModels/LauncherRuntimeInfo.cs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Nebula.SharedModels;
|
||||||
|
|
||||||
|
public record struct LauncherRuntimeInfo(
|
||||||
|
[property: JsonPropertyName("version")] string RuntimeVersion,
|
||||||
|
[property: JsonPropertyName("runtimes")] Dictionary<string, string> DotnetRuntimes);
|
||||||
|
|
||||||
|
public static class LauncherManifestEntryHelper
|
||||||
|
{
|
||||||
|
public static string GetFullPath(this LauncherRuntimeInfo runtimeInfo)
|
||||||
|
{
|
||||||
|
return Path.Join(AppDataPath.RootPath,
|
||||||
|
$"dotnet.{runtimeInfo.RuntimeVersion}",
|
||||||
|
DotnetUrlHelper.GetRuntimeIdentifier());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetExecutePath(this LauncherRuntimeInfo runtimeInfo )
|
||||||
|
{
|
||||||
|
return Path.Join(GetFullPath(runtimeInfo),
|
||||||
|
$"dotnet{DotnetUrlHelper.GetExtension()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Nebula.SharedModels/Nebula.SharedModels.csproj
Normal file
9
Nebula.SharedModels/Nebula.SharedModels.csproj
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -14,13 +14,12 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4">
|
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/>
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp"/>
|
||||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
|
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user