11 Commits

Author SHA1 Message Date
63a4b39aa9 - tweak: change mirror 2026-04-08 21:58:51 +03:00
8bf665d1f1 - fix: code logic 2026-03-16 12:39:25 +03:00
7a77af2d80 - add: game service 2026-03-15 15:47:20 +03:00
830cb38d9f - tweak: less memory tweak part 2 2026-03-13 20:29:53 +03:00
8f66bf9f09 - tweak: less memory tweak 2026-03-12 21:25:38 +03:00
755fa51adc - add: separation user data 2026-01-25 12:01:08 +03:00
a15d187550 - fix: update resolver dotnet resolving 2026-01-24 00:10:58 +03:00
6e6ebffb62 - tweak: improve update resolver for further updates 2026-01-23 23:52:21 +03:00
c2ab550329 - tweak: exception representation 2026-01-22 21:13:39 +03:00
ff31412719 - add: zip content support 2026-01-16 21:02:34 +03:00
15e4e3fbd7 - fix: harmony thinks 2026-01-16 18:53:26 +03:00
70 changed files with 1451 additions and 970 deletions

View File

@@ -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>

View File

@@ -9,23 +9,25 @@
<PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" /> <PackageVersion Include="AsyncImageLoader.Avalonia" Version="3.5.0" />
<PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" /> <PackageVersion Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageVersion Include="Fluent.Net" Version="1.0.63" /> <PackageVersion Include="Fluent.Net" Version="1.0.63" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" /> <PackageVersion Include="JetBrains.Annotations" Version="2025.2.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" /> <PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.2" />
<PackageVersion Include="libsodium" Version="1.0.20" /> <PackageVersion Include="libsodium" Version="1.0.20" />
<PackageVersion Include="Robust.Natives" Version="0.2.3" /> <PackageVersion Include="Robust.Natives" Version="0.2.3" />
<PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" /> <PackageVersion Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageVersion Include="Lib.Harmony" Version="2.3.6" /> <PackageVersion Include="Lib.Harmony" Version="2.4.2" />
<PackageVersion Include="SharpZstd.Interop" Version="1.5.6" /> <PackageVersion Include="SharpZstd.Interop" Version="1.5.6" />
<PackageVersion Include="coverlet.collector" Version="6.0.0" /> <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="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageVersion Include="Moq" Version="4.20.72" /> <PackageVersion Include="Moq" Version="4.20.72" />
<PackageVersion Include="NUnit" Version="3.14.0" /> <PackageVersion Include="NUnit" Version="3.14.0" />
<PackageVersion Include="NUnit.Analyzers" Version="3.9.0" /> <PackageVersion Include="NUnit.Analyzers" Version="3.9.0" />
<PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" /> <PackageVersion Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.0"/> <PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.3.0"/>
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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,13 +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/v10.0-preview2/ILSpy_selfcontained_10.0.0.8282-preview2-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"); public static readonly ConVar<string> ILSpyVersion = ConVarBuilder.Build<string>("dotnet.version", "10");
} }

View File

@@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; 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;
@@ -8,18 +9,24 @@ namespace Nebula.Launcher.Models;
public sealed class ContentLogConsumer : IProcessLogConsumer public sealed class ContentLogConsumer : IProcessLogConsumer
{ {
private readonly PopupMessageService _popupMessageService;
private readonly List<string> _outMessages = []; private readonly List<string> _outMessages = [];
private LogPopupModelView? _currentLogPopup; private LogPopupModelView? _currentLogPopup;
public int MaxMessages { get; set; } = 100; public int MaxMessages { get; set; } = 100;
public void Popup(PopupMessageService popupMessageService) public ContentLogConsumer(PopupMessageService popupMessageService)
{
_popupMessageService = popupMessageService;
}
public void Popup()
{ {
if(_currentLogPopup is not null) if(_currentLogPopup is not null)
return; return;
_currentLogPopup = new LogPopupModelView(popupMessageService); _currentLogPopup = new LogPopupModelView(_popupMessageService);
_currentLogPopup.OnDisposing += OnLogPopupDisposing; _currentLogPopup.OnDisposing += OnLogPopupDisposing;
foreach (var message in _outMessages.ToArray()) foreach (var message in _outMessages.ToArray())
@@ -27,7 +34,7 @@ public sealed class ContentLogConsumer : IProcessLogConsumer
_currentLogPopup.Append(message); _currentLogPopup.Append(message);
} }
popupMessageService.Popup(_currentLogPopup); _popupMessageService.Popup(_currentLogPopup);
} }
private void OnLogPopupDisposing(PopupViewModelBase obj) private void OnLogPopupDisposing(PopupViewModelBase obj)
@@ -55,6 +62,6 @@ public sealed class ContentLogConsumer : IProcessLogConsumer
public void Fatal(string text) public void Fatal(string text)
{ {
throw new Exception("Error while running programm: " + text); _popupMessageService.Popup(new ExceptionCompound("Error while running program", text));
} }
} }

View File

@@ -0,0 +1,6 @@
namespace Nebula.Launcher.Models;
public interface IRunningSignalConsumer
{
public void ProcessRunningSignal(bool isRunning);
}

View File

@@ -62,5 +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"/>
<ProjectReference Include="..\Nebula.Runner\Nebula.Runner.csproj"
ReferenceOutputAssembly="false" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -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,

View File

@@ -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)

View File

@@ -6,7 +6,6 @@ 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.Utils;
using Robust.LoaderApi;
namespace Nebula.Launcher.ProcessHelper; namespace Nebula.Launcher.ProcessHelper;
@@ -22,8 +21,7 @@ public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService
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, loadingHandlerFactory, cancellationToken); var hashApi = await contentService.EnsureItems(buildInfo, loadingHandlerFactory, cancellationToken);
if (hashApi.TryOpen("manifest.yml", out var stream)) if (hashApi.TryOpen("manifest.yml", out var stream))
{ {

View File

@@ -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);
} }

View File

@@ -1,5 +1,7 @@
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;
@@ -8,36 +10,23 @@ namespace Nebula.Launcher.ProcessHelper;
public class ProcessRunHandler : IDisposable public class ProcessRunHandler : IDisposable
{ {
private ProcessStartInfo? _processInfo;
private Task<ProcessStartInfo>? _processInfoTask;
private Process? _process; private Process? _process;
private readonly IProcessLogConsumer _logConsumer; private readonly IProcessLogConsumer _logConsumer;
private string _lastError = string.Empty; private StringBuilder _lastErrorBuilder = new StringBuilder();
private readonly IProcessStartInfoProvider _currentProcessStartInfoProvider;
public IProcessStartInfoProvider GetCurrentProcessStartInfo() => _currentProcessStartInfoProvider; public bool IsRunning => _process is not null;
public bool IsRunning => _processInfo is not null;
public Action<ProcessRunHandler>? OnProcessExited; public Action<ProcessRunHandler>? OnProcessExited;
public AsyncValueCache<ProcessStartInfo> ProcessStartInfoProvider { get; }
public bool Disposed { get; private set; } public bool Disposed { get; private set; }
public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer) public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer)
{ {
_currentProcessStartInfoProvider = processStartInfoProvider;
_logConsumer = logConsumer; _logConsumer = logConsumer;
_processInfoTask = _currentProcessStartInfoProvider.GetProcessStartInfo();
_processInfoTask.GetAwaiter().OnCompleted(OnInfoProvided);
}
private void OnInfoProvided() ProcessStartInfoProvider = new AsyncValueCache<ProcessStartInfo>(processStartInfoProvider.GetProcessStartInfo);
{
if (_processInfoTask == null)
return;
_processInfo = _processInfoTask.GetAwaiter().GetResult();
_processInfoTask = null;
} }
private void CheckIfDisposed() private void CheckIfDisposed()
@@ -51,13 +40,8 @@ public class ProcessRunHandler : IDisposable
CheckIfDisposed(); CheckIfDisposed();
if(_process is not null) if(_process is not null)
throw new InvalidOperationException("Already running"); throw new InvalidOperationException("Already running");
if (_processInfoTask != null)
{
_processInfoTask.Wait();
}
_process = Process.Start(_processInfo!); _process = Process.Start(ProcessStartInfoProvider.GetValue());
if (_process is null) return; if (_process is null) return;
@@ -86,9 +70,8 @@ public class ProcessRunHandler : IDisposable
_process.ErrorDataReceived -= OnErrorDataReceived; _process.ErrorDataReceived -= OnErrorDataReceived;
_process.Exited -= OnExited; _process.Exited -= OnExited;
if (_process.ExitCode != 0) if (_process.ExitCode != 0)
_logConsumer.Fatal(_lastError); _logConsumer.Fatal(_lastErrorBuilder.ToString());
_process.Dispose(); _process.Dispose();
_process = null; _process = null;
@@ -99,11 +82,13 @@ public class ProcessRunHandler : IDisposable
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(" "))
_logConsumer.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)
@@ -122,9 +107,10 @@ public class ProcessRunHandler : IDisposable
return; return;
} }
ProcessStartInfoProvider.Invalidate();
CheckIfDisposed(); CheckIfDisposed();
_processInfoTask?.Dispose();
Disposed = true; Disposed = true;
} }
} }
@@ -152,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));
}
} }

View File

@@ -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,50 +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? OnDisposed { 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)
@@ -71,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());
} }
@@ -87,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()
@@ -99,26 +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 void Dispose()
{
OnDisposed?.Invoke();
}
} }
public sealed 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);
@@ -133,10 +106,5 @@ public sealed class AddFavoriteButton: Border, IListEntryModelView{
_addFavoriteButton.Content = "Add Favorite"; _addFavoriteButton.Content = "Add Favorite";
Child = _addFavoriteButton; Child = _addFavoriteButton;
} }
public bool IsFavorite { get; set; } public void Dispose(){}
public void Dispose()
{
}
} }

View File

@@ -1,83 +1,107 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
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; }
public Action? OnDisposed { 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(){}
@@ -85,7 +109,17 @@ public sealed partial class HubServerListProvider : IServerListProvider
public void Dispose() public void Dispose()
{ {
OnDisposed?.Invoke();
_cts?.Dispose(); _cts?.Dispose();
} }
}
public sealed class LoadingServerEntry : Label, IListEntryModelView
{
public LoadingServerEntry()
{
HorizontalAlignment = HorizontalAlignment.Center;
Content = LocalizationService.GetString("server-list-loading");
}
public void Dispose()
{}
} }

View File

@@ -1,23 +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 : IDisposable public interface IServerListProvider
{ {
public bool IsLoaded { get; } public void LoadServerList(
public Action? OnLoaded { get; set; } AvaloniaList<IListEntryModelView> servers,
public Action? OnDisposed { get; set; } AvaloniaList<Exception> exceptions);
public IEnumerable<IListEntryModelView> GetServers();
public IEnumerable<Exception> GetErrors();
public void LoadServerList();
}
public interface IServerListDirtyInvoker
{
public Action? Dirty { get; set; }
} }

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.ObjectModel;
using Avalonia.Collections;
using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Pages;
@@ -7,27 +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 Action? OnDisposed { get; set; } AvaloniaList<Exception> exceptions)
public IEnumerable<IListEntryModelView> GetServers()
{
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());
public void Dispose()
{ exceptions.Add(new Exception("Oh no!"));
OnDisposed?.Invoke();
} }
} }

View File

@@ -4,6 +4,7 @@ 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;
@@ -15,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;
@@ -32,7 +35,7 @@ public sealed partial class DecompilerService
private readonly HttpClient _httpClient = new(); private readonly HttpClient _httpClient = new();
private ILogger _logger; private ILogger _logger;
private string FullPath => Path.Join(FileService.RootPath,$"ILSpy.{ConfigurationService.GetConfigValue(LauncherConVar.ILSpyVersion)}"); private string FullPath => Path.Join(AppDataPath.RootPath, $"ILSpy.{ConfigurationService.GetConfigValue(LauncherConVar.ILSpyVersion)}");
private 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){
@@ -61,13 +64,12 @@ 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, cancellationToken);
foreach (var (file, hash) in hashApi.Manifest) var hashApi = await ContentService.EnsureItems(buildInfo, loadingHandler, cancellationToken);
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();
} }
@@ -91,10 +93,17 @@ public sealed partial class DecompilerService
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.CreateLoadingContext().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);
context.SetJobsCount(response.Content.Headers.ContentLength ?? 1000);
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); Directory.CreateDirectory(FullPath);
zipArchive.ExtractToDirectory(FullPath); zipArchive.ExtractToDirectory(FullPath);
} }

View 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);
}
}

View File

@@ -3,13 +3,17 @@ using System.Collections.Generic;
using Nebula.Launcher.Models; using Nebula.Launcher.Models;
using Nebula.Launcher.ProcessHelper; using Nebula.Launcher.ProcessHelper;
using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared; using Nebula.Shared;
using Nebula.Shared.Services; using Nebula.Shared.Services;
namespace Nebula.Launcher.Services; namespace Nebula.Launcher.Services;
[ServiceRegister] [ServiceRegister]
public sealed class InstanceRunningContainer(PopupMessageService popupMessageService, DebugService debugService) public sealed class InstanceRunningContainer(
PopupMessageService popupMessageService,
DebugService debugService
)
{ {
private readonly InstanceKeyPool _keyPool = new(); private readonly InstanceKeyPool _keyPool = new();
private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new(); private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new();
@@ -22,7 +26,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer
{ {
var id = _keyPool.Take(); var id = _keyPool.Take();
var currentContentLogConsumer = new ContentLogConsumer(); var currentContentLogConsumer = new ContentLogConsumer(popupMessageService);
var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id)); var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id));
var logContainer = new ProcessLogConsumerCollection(); var logContainer = new ProcessLogConsumerCollection();
logContainer.RegisterLogger(currentContentLogConsumer); logContainer.RegisterLogger(currentContentLogConsumer);
@@ -43,7 +47,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer
if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler)) if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler))
return; return;
handler.Popup(popupMessageService); handler.Popup();
} }
public void Run(InstanceKey instanceKey) public void Run(InstanceKey instanceKey)

View 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()
{
}
}

View 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);
};

View 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
}
}

View File

@@ -3,9 +3,7 @@ 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.Utils;
@@ -16,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;
@@ -209,7 +208,7 @@ public partial class MainViewModel : ViewModelBase
public void OpenRootPath() public void OpenRootPath()
{ {
ExplorerUtils.OpenFolder(FileService.RootPath); ExplorerUtils.OpenFolder(AppDataPath.RootPath);
} }
public void OpenLink() public void OpenLink()
@@ -231,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);

View File

@@ -12,6 +12,7 @@ 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;
@@ -70,12 +71,12 @@ public partial class ConfigurationViewModel : ViewModelBase
public void OpenDataFolder() public void OpenDataFolder()
{ {
ExplorerUtils.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);

View File

@@ -15,11 +15,11 @@ 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;
@@ -199,9 +199,9 @@ public sealed class ExtContentExecutor
_decompilerService = decompilerService; _decompilerService = decompilerService;
} }
public bool TryExecute(RobustManifestItem manifestItem, CancellationToken cancellationToken) 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")
{ {
@@ -214,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, CancellationToken cancellationToken) public IContentEntry? Go(ContentPath path, CancellationToken cancellationToken)
{ {
if (_extContentExecutor.TryExecute(_manifestItem, cancellationToken)) 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);
@@ -298,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!;
@@ -315,12 +312,12 @@ 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;
@@ -328,7 +325,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
}); });
} }
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;
@@ -345,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);

View File

@@ -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;
@@ -24,16 +27,17 @@ 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;
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; } [GenerateProperty, DesignConstruct] public ServerListViewModel CurrentServerList { get; }
public ServerFilter CurrentFilter { get; } = new();
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
//Design think //Design think
protected override void InitialiseInDesignMode() protected override void InitialiseInDesignMode()
@@ -48,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>();
@@ -73,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)
@@ -99,16 +108,21 @@ 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.ApplyFilter(CurrentFilter);
} }
partial void OnSelectedItemChanged(ServerListTabTemplate value) partial void OnSelectedItemChanged(ServerListTabTemplate value)
{ {
CurrentServerList.Provider = value.ServerListProvider; CurrentServerList.ClearProvider();
CurrentServerList.SetProvider(value.ServerListProvider);
ApplyFilter(); ApplyFilter();
} }
} }
[ServiceRegister] [ServiceRegister]
@@ -119,6 +133,7 @@ public sealed class ServerViewContainer
private readonly Dictionary<string, string> _customNames = []; private readonly Dictionary<string, string> _customNames = [];
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new(); private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
private ServerFilter? _currentFilter;
public ICollection<IListEntryModelView> Items => public ICollection<IListEntryModelView> Items =>
_entries.Values _entries.Values
@@ -143,13 +158,15 @@ public sealed class ServerViewContainer
{ {
foreach (var (_, weakRef) in _entries) foreach (var (_, weakRef) in _entries)
{ {
if (weakRef.TryGetTarget(out var value)) if (weakRef.TryGetTarget(out var value) && value is IDisposable disposable)
value.Dispose(); disposable.Dispose();
} }
_entries.Clear(); _entries.Clear();
} }
public IListEntryModelView Get(string url, ServerStatus? serverStatus = null) => Get(url.ToRobustUrl(), serverStatus);
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null) public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
{ {
var key = url.ToString(); var key = url.ToString();
@@ -157,38 +174,63 @@ public sealed class ServerViewContainer
lock (_entries) lock (_entries)
{ {
_customNames.TryGetValue(key, out var customName);
if (_entries.TryGetValue(key, out var weakEntry) if (_entries.TryGetValue(key, out var weakEntry)
&& weakEntry.TryGetTarget(out entry)) && weakEntry.TryGetTarget(out entry))
{ {
return entry; return entry;
} }
if (serverStatus is not null) entry = Create(url, serverStatus);
{
entry = _viewHelperService
.GetViewModel<ServerEntryModelView>()
.WithData(url, customName, serverStatus);
}
else
{
entry = _viewHelperService
.GetViewModel<ServerCompoundEntryViewModel>()
.LoadServerEntry(url, customName, CancellationToken.None);
}
if (_favorites.Contains(key)
&& entry is IFavoriteEntryModelView fav)
{
fav.IsFavorite = true;
}
_entries[key] = new WeakReference<IListEntryModelView>(entry); _entries[key] = new WeakReference<IListEntryModelView>(entry);
} }
return entry; 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);
}
return entry;
}
public void ApplyFilter(ServerFilter? filter)
{
_currentFilter = filter;
foreach (var serverView in Items)
{
if(serverView is IFilterConsumer filterConsumer)
filterConsumer.ProcessFilter(filter);
}
}
private void OnFavoritesChange(string[]? value) private void OnFavoritesChange(string[]? value)
{ {
@@ -259,11 +301,24 @@ public sealed class ServerViewContainer
} }
} }
public interface IListEntryModelView : IDisposable 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; }

View File

@@ -55,4 +55,10 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
_logger.Error(e); _logger.Error(e);
} }
} }
protected override void OnDispose()
{
base.OnDispose();
_logger.Dispose();
}
} }

View File

@@ -15,7 +15,7 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase
public override string Title => LocalizationService.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);
} }

View File

@@ -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();
} }

View File

@@ -18,7 +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
{ {
private ServerEntryModelView? _currentEntry;
[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;
@@ -27,24 +26,24 @@ public sealed partial class ServerCompoundEntryViewModel :
private RobustUrl? _url; private RobustUrl? _url;
private ServerFilter? _currentFilter; private ServerFilter? _currentFilter;
public ServerEntryModelView? CurrentEntry public ServerEntryViewModel? CurrentEntry
{ {
get => _currentEntry; get;
set set
{ {
if (value == _currentEntry) return; if (value == field) return;
_currentEntry = value;
if (_currentEntry != null) field = value;
if (field != null)
{ {
_currentEntry.IsFavorite = IsFavorite; field.IsFavorite = IsFavorite;
_currentEntry.Name = Name; field.Name = Name;
_currentEntry.ProcessFilter(_currentFilter); field.ProcessFilter(_currentFilter);
} }
Loading = _currentEntry == null; Loading = field == null;
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@@ -75,7 +74,7 @@ public sealed partial class ServerCompoundEntryViewModel :
{ {
} }
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryModelView? entry) public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryViewModel? entry)
{ {
CurrentEntry = entry; CurrentEntry = entry;
return this; return this;
@@ -102,7 +101,7 @@ public sealed partial class ServerCompoundEntryViewModel :
Message = "Loading server entry..."; Message = "Loading server entry...";
var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None); var status = await RestService.GetAsync<ServerStatus>(_url.StatusUri, CancellationToken.None);
CurrentEntry = ServiceProvider.GetService<ServerEntryModelView>()!.WithData(_url, null, status); CurrentEntry = ServiceProvider.GetService<ServerEntryViewModel>()!.WithData(_url, null, status);
Loading = false; Loading = false;
} }
@@ -131,9 +130,4 @@ public sealed partial class ServerCompoundEntryViewModel :
if(CurrentEntry is IFilterConsumer filterConsumer) if(CurrentEntry is IFilterConsumer filterConsumer)
filterConsumer.ProcessFilter(serverFilter); filterConsumer.ProcessFilter(serverFilter);
} }
public void Dispose()
{
CurrentEntry?.Dispose();
}
} }

View File

@@ -1,280 +0,0 @@
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.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 sealed 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 string _realName;
public string? Name
{
get => RealName;
set => RealName = value ?? Status.Name;
}
private ILogger _logger;
private ServerInfo? _serverInfo;
private InstanceKey _instanceKey;
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!;
[GenerateProperty] private InstanceRunningContainer InstanceRunningContainer { 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";
}
protected override void Initialise()
{
_logger = DebugService.GetLogger(this);
InstanceRunningContainer.IsRunningChanged += IsRunningChanged;
}
private void IsRunningChanged(InstanceKey arg1, bool isRunning)
{
if(arg1.Equals(_instanceKey))
RunVisible = !isRunning;
}
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()
{
Task.Run(async ()=> await RunInstanceAsync());
}
public void RunInstanceIgnoreAuth()
{
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 viewModelLoading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
viewModelLoading.LoadingName = "Loading instance...";
PopupMessageService.Popup(viewModelLoading);
var currProcessStartProvider =
await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, viewModelLoading, CancellationService.Token);
_logger.Log("Preparing instance...");
_instanceKey = InstanceRunningContainer.RegisterInstance(currProcessStartProvider);
InstanceRunningContainer.Run(_instanceKey);
_logger.Log("Starting instance..." + RealName);
}
catch (Exception e)
{
var error = new Exception("Error while attempt run instance", e);
_logger.Error(error);
PopupMessageService.Popup(error);
}
}
public void StopInstance()
{
InstanceRunningContainer.Stop(_instanceKey);
}
public void ReadLog()
{
InstanceRunningContainer.Popup(_instanceKey);
}
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 Dispose()
{
_logger.Dispose();
}
}
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
}
}
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);
};
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;
}

View 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;
}

View File

@@ -1,8 +1,6 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.Controls; using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Models;
using Nebula.Launcher.ServerListProviders; using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.Views; using Nebula.Launcher.Views;
@@ -11,135 +9,46 @@ using Nebula.Shared.ViewHelper;
namespace Nebula.Launcher.ViewModels; namespace Nebula.Launcher.ViewModels;
[ViewModelRegister(typeof(ServerListView), false)] [ViewModelRegister(typeof(ServerListView), false)]
public partial class ServerListViewModel : ViewModelBase public class ServerListViewModel : ViewModelBase
{ {
[ObservableProperty] private bool _isLoading; public AvaloniaList<IListEntryModelView> ServerList { get; private set; } = new();
public AvaloniaList<Exception> ErrorList { get; private set; } = new();
public IServerListProvider? Provider { get; private set; }
public ServerListViewModel() public void ClearProvider()
{ {
if (Design.IsDesignMode) foreach (var serverEntry in ServerList)
{ {
Provider = new TestServerList(); if (serverEntry is IDisposable disposable)
}
}
private IServerListProvider? _provider;
public ObservableCollection<IListEntryModelView> ServerList { get; } = new();
public ObservableCollection<Exception> ErrorList { get; } = new();
public IServerListProvider Provider
{
get => _provider ?? throw new Exception();
set
{
_provider = value;
_provider.OnDisposed += OnProviderDisposed;
if (_provider is IServerListDirtyInvoker invoker)
{ {
invoker.Dirty += OnDirty; disposable.Dispose();
}
if(!_provider.IsLoaded)
RefreshFromProvider();
else
{
Clear();
PasteServersFromList();
} }
} }
ServerList.Clear();
ErrorList.Clear();
GC.Collect();
GC.WaitForPendingFinalizers();
} }
private void OnProviderDisposed() public void SetProvider(IServerListProvider provider)
{ {
Provider.OnLoaded -= RefreshRequired; Provider = provider;
Provider.OnDisposed -= OnProviderDisposed;
if (Provider is IServerListDirtyInvoker invoker)
{
invoker.Dirty -= OnDirty;
}
_provider = null; OnPropertyChanged(nameof(ServerList));
} OnPropertyChanged(nameof(ErrorList));
private ServerFilter? _currentFilter;
public void RefreshFromProvider()
{
if (IsLoading)
return;
Clear();
StartLoading();
Provider.LoadServerList();
if (Provider.IsLoaded) PasteServersFromList();
else Provider.OnLoaded += RefreshRequired;
}
public void ApplyFilter(ServerFilter? filter)
{
_currentFilter = filter;
if(IsLoading)
return;
foreach (var serverView in ServerList)
{
if(serverView is IFilterConsumer filterConsumer)
filterConsumer.ProcessFilter(filter);
}
}
private void OnDirty()
{
RefreshFromProvider(); RefreshFromProvider();
} }
private void Clear() public void RefreshFromProvider()
{ {
ErrorList.Clear(); Provider?.LoadServerList(ServerList, ErrorList);
ServerList.Clear();
} }
private void PasteServersFromList()
{
foreach (var serverEntry in Provider.GetServers())
{
ServerList.Add(serverEntry);
if(serverEntry is IFilterConsumer serverFilter)
serverFilter.ProcessFilter(_currentFilter);
}
foreach (var error in Provider.GetErrors())
{
ErrorList.Add(error);
}
EndLoading();
}
private void RefreshRequired()
{
PasteServersFromList();
Provider.OnLoaded -= RefreshRequired;
}
private void StartLoading()
{
Clear();
IsLoading = true;
}
private void EndLoading()
{
IsLoading = false;
}
protected override void InitialiseInDesignMode() protected override void InitialiseInDesignMode()
{ {
SetProvider(new TestServerList());
} }
protected override void Initialise() protected override void Initialise()

View File

@@ -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}"

View File

@@ -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);
} }
} }

View File

@@ -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">

View File

@@ -2,7 +2,6 @@
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:services="clr-namespace:Nebula.Launcher.Services"
xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels" 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.Views.ServerListView" x:Class="Nebula.Launcher.Views.ServerListView"
@@ -15,16 +14,10 @@
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 IsVisible="{Binding IsLoading}" <ItemsControl ItemsSource="{Binding ErrorList}"
x:Name="LoadingLabel" Margin="10,0,10,0" />
Margin="10" HorizontalAlignment="Center" <ItemsControl ItemsSource="{Binding ServerList}"
Content="{services:LocaledText 'server-list-loading'}"/> Padding="0" />
<ItemsControl
ItemsSource="{Binding ErrorList}"
Margin="10,0,10,0" />
<ItemsControl
ItemsSource="{Binding ServerList}"
Padding="0" />
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View 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;
}
}

View File

@@ -4,4 +4,11 @@
<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>

View File

@@ -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
);

View File

@@ -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 ConsoleLoadingHandlerFactory(), cancelTokenSource.Token); await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandlerFactory(), login, cancelTokenSource.Token);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@@ -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;

View File

@@ -7,6 +7,7 @@ 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;
using Nebula.SharedModels;
using Robust.LoaderApi; using Robust.LoaderApi;
namespace Nebula.Runner.Services; namespace Nebula.Runner.Services;
@@ -25,8 +26,8 @@ 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,
ILoadingHandlerFactory loadingHandler, ILoadingHandlerFactory loadingHandler, string? userDataPath = null,
CancellationToken cancellationToken) CancellationToken cancellationToken = default)
{ {
_logger.Log("Start Content!"); _logger.Log("Start Content!");
@@ -35,14 +36,14 @@ public sealed class RunnerService(
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);
@@ -80,6 +81,12 @@ public sealed class RunnerService(
metricServer = RunHelper.RunMetric(prometheusAssembly); metricServer = RunHelper.RunMetric(prometheusAssembly);
} }
if (userDataPath is not null)
{
UserDataDirPatcher.UserPath = userDataPath;
UserDataDirPatcher.ApplyPatch(reflectionService, harmonyService);
}
loadingHandler.Dispose(); loadingHandler.Dispose();
await Task.Run(() => loader.Main(args), cancellationToken); await Task.Run(() => loader.Main(args), cancellationToken);
@@ -112,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)

View File

@@ -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"
]); ]);

View File

@@ -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!;
} }

View File

@@ -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);

View File

@@ -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);

View File

@@ -9,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="JetBrains.Annotations" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"/> <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"/>
<PackageReference Include="Robust.Natives"/> <PackageReference Include="Robust.Natives"/>
<PackageReference Include="SharpZstd.Interop"/> <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>

View File

@@ -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)

View File

@@ -1,12 +1,15 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Diagnostics; 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;
@@ -14,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)
{ {
@@ -33,8 +37,20 @@ 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,
ILoadingHandlerFactory loadingFactory, ILoadingHandlerFactory loadingFactory,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
@@ -58,7 +74,41 @@ public partial class ContentService
return hashApi; return hashApi;
} }
public async Task<HashApi> EnsureItems(RobustManifestInfo info, ILoadingHandlerFactory loadingFactory, 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);
@@ -90,10 +140,10 @@ public partial class ContentService
return await EnsureItems(manifestReader, info.DownloadUri, loadingFactory, 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
@@ -105,13 +155,13 @@ 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("Error while unpacking thinks " + item.Path); _logger.Error($"Error while unpacking thinks {item}");
} }
loadingHandler.AppendResolvedJob(); loadingHandler.AppendResolvedJob();

View File

@@ -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),

View File

@@ -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()
{ {

View File

@@ -2,50 +2,54 @@ using System.IO.Compression;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using Nebula.Shared.Utils; 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 string FullPath =>
Path.Join(FileService.RootPath, $"dotnet.{configurationService.GetConfigValue(CurrentConVar.DotnetVersion)}", DotnetUrlHelper.GetRuntimeIdentifier());
private string ExecutePath => Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
private readonly HttpClient _httpClient = new(); private readonly HttpClient _httpClient = new();
public async Task<string> EnsureDotnet() public async Task<string> EnsureDotnet(CancellationToken cancellationToken = default)
{ {
if (!Directory.Exists(FullPath)) var dotnetEntry = new LauncherRuntimeInfo(
await Download(); configurationService.GetConfigValue(CurrentConVar.DotnetVersion)!,
configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
);
if (!File.Exists(dotnetEntry.GetExecutePath()))
await Download(dotnetEntry, cancellationToken);
return ExecutePath; return dotnetEntry.GetExecutePath();
} }
private async Task Download() private async Task Download(LauncherRuntimeInfo runtimeInfo, CancellationToken cancellationToken = default)
{ {
var debugLogger = debugService.GetLogger(this); var debugLogger = debugService.GetLogger(this);
debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}..."); debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl( var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
); var fullPath = runtimeInfo.GetFullPath();
UrlValidator.EnsureDomainValid(url, "microsoft.com");
using var response = await _httpClient.GetAsync(url); using var response = await _httpClient.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(); await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
Directory.CreateDirectory(FullPath); Directory.CreateDirectory(fullPath);
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{ {
using var zipArchive = new ZipArchive(stream); await using var zipArchive = new ZipArchive(stream);
zipArchive.ExtractToDirectory(FullPath, true); await zipArchive.ExtractToDirectoryAsync(fullPath, true, cancellationToken);
} }
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) || url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{ {
TarUtils.ExtractTarGz(stream, FullPath); TarUtils.ExtractTarGz(stream, fullPath);
} }
else else
{ {
@@ -55,36 +59,3 @@ public class DotnetResolverService(DebugService debugService, ConfigurationServi
debugLogger.Log("Downloading dotnet complete."); debugLogger.Log("Downloading dotnet complete.");
} }
} }
public static class DotnetUrlHelper
{
[Obsolete("FOR TEST USING ONLY!")]
public static string? RidOverrideTest = null; // FOR TEST PURPOSES ONLY!!!
public static string GetExtension()
{
if (OperatingSystem.IsWindows()) return ".exe";
return "";
}
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(RidOverrideTest != null) return RidOverrideTest;
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");
}
}

View File

@@ -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,6 +88,7 @@ public class FileService
} }
} }
public sealed class ConsoleLoadingHandlerFactory : ILoadingHandlerFactory public sealed class ConsoleLoadingHandlerFactory : ILoadingHandlerFactory
{ {
public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null) public ILoadingHandler CreateLoadingContext(ILoadingFormater? loadingFormater = null)

View 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}");
}
}

View 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
);

View 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
);

View 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()}");
}
}

View File

@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection;
using Moq; using Moq;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.SharedModels;
using Robust.LoaderApi; using Robust.LoaderApi;
namespace Nebula.UnitTest.NebulaSharedTests; namespace Nebula.UnitTest.NebulaSharedTests;
@@ -36,7 +37,7 @@ public class FileServiceTests : BaseSharedTest
fileApi.Save("test.txt", stream); fileApi.Save("test.txt", stream);
} }
var expectedPath = Path.Combine(FileService.RootPath, subPath); var expectedPath = Path.Combine(AppDataPath.RootPath, subPath);
Assert.That(Directory.Exists(expectedPath), Is.True, $"Expected path to be created: {expectedPath}"); Assert.That(Directory.Exists(expectedPath), Is.True, $"Expected path to be created: {expectedPath}");
} }
@@ -53,7 +54,7 @@ public class FileServiceTests : BaseSharedTest
[Test] [Test]
public void OpenZip_ReturnsZipFileApi_WhenValid() public void OpenZip_ReturnsZipFileApi_WhenValid()
{ {
var testZipPath = Path.Combine(FileService.RootPath, "test.zip"); var testZipPath = Path.Combine(AppDataPath.RootPath, "test.zip");
using (var archive = ZipFile.Open(testZipPath, ZipArchiveMode.Create)) using (var archive = ZipFile.Open(testZipPath, ZipArchiveMode.Create))
{ {
var entry = archive.CreateEntry("test.txt"); var entry = archive.CreateEntry("test.txt");
@@ -93,7 +94,7 @@ public class FileServiceTests : BaseSharedTest
[Test] [Test]
public void RemoveAllFiles_DeletesAllFilesAndDirectories() public void RemoveAllFiles_DeletesAllFilesAndDirectories()
{ {
var testDir = Path.Combine(FileService.RootPath, "cleanup-test"); var testDir = Path.Combine(AppDataPath.RootPath, "cleanup-test");
Directory.CreateDirectory(testDir); Directory.CreateDirectory(testDir);
File.WriteAllText(Path.Combine(testDir, "test1.txt"), "data"); File.WriteAllText(Path.Combine(testDir, "test1.txt"), "data");

View File

@@ -4,6 +4,7 @@ using Nebula.Shared;
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;
using Nebula.SharedModels;
namespace Nebula.UnitTest.NebulaSharedTests; namespace Nebula.UnitTest.NebulaSharedTests;
@@ -31,28 +32,26 @@ public class TarTest : BaseSharedTest
[Test] [Test]
public async Task DownloadTarAndUnzipTest() public async Task DownloadTarAndUnzipTest()
{ {
DotnetUrlHelper.RidOverrideTest = "linux-x64"; Console.WriteLine($"Downloading dotnet linux-x64...");
Console.WriteLine($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
if(!_configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!.TryGetValue("linux-x64", out var url))
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl( throw new NullReferenceException();
_configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
);
using var response = await _httpClient.GetAsync(url); using var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(); await using var stream = await response.Content.ReadAsStreamAsync();
Directory.CreateDirectory(FileService.RootPath); Directory.CreateDirectory(AppDataPath.RootPath);
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{ {
using var zipArchive = new ZipArchive(stream); using var zipArchive = new ZipArchive(stream);
zipArchive.ExtractToDirectory(FileService.RootPath, true); await zipArchive.ExtractToDirectoryAsync(AppDataPath.RootPath, true);
} }
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) || url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{ {
TarUtils.ExtractTarGz(stream, FileService.RootPath); TarUtils.ExtractTarGz(stream, AppDataPath.RootPath);
} }
else else
{ {

View File

@@ -1,6 +1,6 @@
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Nebula.Shared; using Nebula.Shared;
using Nebula.Shared.Services; using Nebula.SharedModels;
namespace Nebula.UnitTest; namespace Nebula.UnitTest;
@@ -20,19 +20,10 @@ public static class TestServiceHelper
{ {
var path = Path.Combine(Path.GetTempPath(), "tempThink"+Path.GetRandomFileName()); var path = Path.Combine(Path.GetTempPath(), "tempThink"+Path.GetRandomFileName());
Directory.CreateDirectory(path); Directory.CreateDirectory(path);
FileService.RootPath = path; AppDataPath.SetTestRootPath(path);
Console.WriteLine("Change root path for file api: " + FileService.RootPath);
} }
} }
public sealed class LauncherUnit : SharedUnit
{
public LauncherUnit(IServiceProvider serviceProvider) : base(serviceProvider)
{
}
}
public class SharedUnit public class SharedUnit
{ {
public SharedUnit(IServiceProvider serviceProvider) public SharedUnit(IServiceProvider serviceProvider)

View File

@@ -7,7 +7,8 @@ using System.Net.Http;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Nebula.UpdateResolver.Configuration; using Nebula.SharedModels;
using Nebula.UpdateResolver.Rest;
namespace Nebula.UpdateResolver; namespace Nebula.UpdateResolver;
@@ -15,19 +16,13 @@ public static class DotnetStandalone
{ {
private static readonly HttpClient HttpClient = new(); private static readonly HttpClient HttpClient = new();
private static readonly string FullPath = public static async Task<Process?> Run(LauncherRuntimeInfo runtimeInfo, string dllPath)
Path.Join(MainWindow.RootPath, $"dotnet.{ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetVersion)}",
DotnetUrlHelper.GetRuntimeIdentifier());
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
public static async Task<Process?> Run(string dllPath)
{ {
await EnsureDotnet(); await EnsureDotnet(runtimeInfo);
return Process.Start(new ProcessStartInfo return Process.Start(new ProcessStartInfo
{ {
FileName = ExecutePath, FileName = runtimeInfo.GetExecutePath(),
Arguments = dllPath, Arguments = dllPath,
CreateNoWindow = true, CreateNoWindow = true,
UseShellExecute = false, UseShellExecute = false,
@@ -37,35 +32,40 @@ public static class DotnetStandalone
}); });
} }
private static async Task EnsureDotnet() private static async Task EnsureDotnet(LauncherRuntimeInfo runtimeInfo)
{ {
if (!Directory.Exists(FullPath)) if (!File.Exists(runtimeInfo.GetExecutePath()))
await Download(); await Download(runtimeInfo);
} }
private static async Task Download() private static async Task Download(LauncherRuntimeInfo runtimeInfo)
{ {
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}..."); LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl( var fullPath = runtimeInfo.GetFullPath();
ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!
);
using var response = await HttpClient.GetAsync(url); var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
UrlValidator.EnsureDomainValid(url, "microsoft.com");
using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(); var stream = await response.Content.ReadAsStreamAsync();
await using var tempStream = new MemoryStream();
stream.CopyTo(tempStream,"dotnet", response.Content.Headers.ContentLength ?? 0);
await stream.DisposeAsync();
Directory.CreateDirectory(FullPath); Directory.CreateDirectory(fullPath);
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{ {
using var zipArchive = new ZipArchive(stream); await using var zipArchive = new ZipArchive(tempStream);
zipArchive.ExtractToDirectory(FullPath, true); await zipArchive.ExtractToDirectoryAsync(fullPath, true);
} }
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase) else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase)) || url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{ {
TarUtils.ExtractTarGz(stream, FullPath); TarUtils.ExtractTarGz(tempStream, fullPath);
} }
else else
{ {
@@ -74,32 +74,4 @@ public static class DotnetStandalone
LogStandalone.Log("Downloading dotnet complete."); LogStandalone.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)
{
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 Environment.Is64BitProcess ? "win-x64" : "win-x86";
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64";
throw new PlatformNotSupportedException("Unsupported operating system");
}
} }

View File

@@ -1,13 +0,0 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Nebula.UpdateResolver;
public record struct LauncherManifest(
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries
);
public record struct LauncherManifestEntry(
[property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("path")] string Path
);

View File

@@ -3,10 +3,11 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using Nebula.SharedModels;
using Nebula.UpdateResolver.Configuration; using Nebula.UpdateResolver.Configuration;
using Nebula.UpdateResolver.Rest; using Nebula.UpdateResolver.Rest;
@@ -14,31 +15,36 @@ namespace Nebula.UpdateResolver;
public partial class MainWindow : Window public partial class MainWindow : Window
{ {
public static readonly string RootPath = Path.Join(Environment.GetFolderPath( public static readonly string RootPath = AppDataPath.GetAppDataPath("Datum");
Environment.SpecialFolder.ApplicationData), "Datum");
private readonly HttpClient _httpClient = new();
private readonly FileApi _fileApi = new(Path.Join(RootPath, "app"));
private string _logStr = "";
private readonly HttpClient _httpClient = new HttpClient();
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
private string LogStr = "";
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
LogStandalone.OnLog += (message, percentage) => LogStandalone.OnLog += (message, percentage) =>
{ {
ProgressLabel.Content = message; var percentText = "";
if (percentage == 0) if (percentage != 0)
PercentLabel.Content = ""; percentText = $"{percentage}%";
else
PercentLabel.Content = percentage + "%"; Dispatcher.UIThread.Invoke(() =>
{
ProgressLabel.Content = message;
PercentLabel.Content = percentText;
});
var messageOut = var messageOut =
$"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}"; $"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {percentText}";
Console.WriteLine(messageOut); Console.WriteLine(messageOut);
LogStr += messageOut + "\n"; _logStr += messageOut + "\n";
}; };
LogStandalone.Log("Starting up"); LogStandalone.Log("Starting up");
if (!Design.IsDesignMode) if (!Design.IsDesignMode)
_ = Start(); Task.Run(Start);
else else
LogStandalone.Log("Debug information", 51); LogStandalone.Log("Debug information", 51);
} }
@@ -47,17 +53,21 @@ public partial class MainWindow : Window
{ {
try try
{ {
var info = await EnsureFiles(); var manifest = await RestStandalone.GetAsync<LauncherManifest>(
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
var info = EnsureFiles(FilterEntries(manifest.Entries));
LogStandalone.Log("Downloading files..."); LogStandalone.Log("Downloading files...");
foreach (var file in info.ToDelete) foreach (var file in info.ToDelete)
{ {
LogStandalone.Log("Deleting " + file.Path); LogStandalone.Log("Deleting " + file.Path);
FileApi.Remove(file.Path); _fileApi.Remove(file.Path);
} }
var loadedManifest = info.FilesExist; var loadedManifest = info.FilesExist;
Save(loadedManifest); Save(loadedManifest, manifest.RuntimeInfo);
var count = info.ToDownload.Count; var count = info.ToDownload.Count;
var resolved = 0; var resolved = 0;
@@ -70,23 +80,23 @@ public partial class MainWindow : Window
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(); await using var stream = await response.Content.ReadAsStreamAsync();
FileApi.Save(file.Path, stream); _fileApi.Save(file.Path, stream);
resolved++; resolved++;
LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f)); LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
loadedManifest.Add(file); loadedManifest.Add(file);
Save(loadedManifest); Save(loadedManifest, manifest.RuntimeInfo);
} }
LogStandalone.Log("Download finished. Running launcher..."); LogStandalone.Log("Download finished. Running launcher...");
await DotnetStandalone.Run(Path.Join(FileApi.RootPath, "Nebula.Launcher.dll")); await DotnetStandalone.Run(manifest.RuntimeInfo, Path.Join(_fileApi.RootPath, "Nebula.Launcher.dll"));
} }
catch(HttpRequestException e){ catch(HttpRequestException e){
LogStandalone.LogError(e); LogStandalone.LogError(e);
LogStandalone.Log("Network connection error..."); LogStandalone.Log("Network connection error...");
var logPath = Path.Join(RootPath,"updateResloverError.txt"); var logPath = Path.Join(RootPath,"updateResloverError.txt");
await File.WriteAllTextAsync(logPath, LogStr); await File.WriteAllTextAsync(logPath, _logStr);
Process.Start(new ProcessStartInfo(){ Process.Start(new ProcessStartInfo(){
FileName = "notepad", FileName = "notepad",
Arguments = logPath Arguments = logPath
@@ -96,7 +106,7 @@ public partial class MainWindow : Window
{ {
LogStandalone.LogError(e); LogStandalone.LogError(e);
var logPath = Path.Join(RootPath,"updateResloverError.txt"); var logPath = Path.Join(RootPath,"updateResloverError.txt");
await File.WriteAllTextAsync(logPath, LogStr); await File.WriteAllTextAsync(logPath, _logStr);
Process.Start(new ProcessStartInfo(){ Process.Start(new ProcessStartInfo(){
FileName = "notepad", FileName = "notepad",
Arguments = logPath Arguments = logPath
@@ -108,11 +118,9 @@ public partial class MainWindow : Window
Environment.Exit(0); Environment.Exit(0);
} }
private async Task<ManifestEnsureInfo> EnsureFiles() private ManifestEnsureInfo EnsureFiles(HashSet<LauncherManifestEntry> entries)
{ {
LogStandalone.Log("Ensuring launcher manifest..."); LogStandalone.Log("Ensuring launcher manifest...");
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
var toDownload = new HashSet<LauncherManifestEntry>(); var toDownload = new HashSet<LauncherManifestEntry>();
var toDelete = new HashSet<LauncherManifestEntry>(); var toDelete = new HashSet<LauncherManifestEntry>();
@@ -124,13 +132,13 @@ public partial class MainWindow : Window
LogStandalone.Log("Delta manifest loaded!"); LogStandalone.Log("Delta manifest loaded!");
foreach (var file in currentManifest.Entries) foreach (var file in currentManifest.Entries)
{ {
if (!manifest.Entries.Contains(file)) if (!entries.Contains(file))
toDelete.Add(EnsurePath(file)); toDelete.Add(EnsurePath(file));
else else
filesExist.Add(EnsurePath(file)); filesExist.Add(EnsurePath(file));
} }
foreach (var file in manifest.Entries) foreach (var file in entries)
{ {
if(!currentManifest.Entries.Contains(file)) if(!currentManifest.Entries.Contains(file))
toDownload.Add(EnsurePath(file)); toDownload.Add(EnsurePath(file));
@@ -138,30 +146,48 @@ public partial class MainWindow : Window
} }
else else
{ {
toDownload = manifest.Entries; toDownload = entries;
} }
LogStandalone.Log("Saving launcher manifest..."); LogStandalone.Log("Saving launcher manifest...");
return new ManifestEnsureInfo(toDownload, toDelete, filesExist); return new ManifestEnsureInfo(toDownload, toDelete, filesExist);
} }
private void Save(HashSet<LauncherManifestEntry> entries) private HashSet<LauncherManifestEntry> FilterEntries(IEnumerable<LauncherManifestEntry> entries)
{ {
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries)); var filtered = new HashSet<LauncherManifestEntry>();
var runtimeIdentifier = DotnetUrlHelper.GetRuntimeIdentifier();
foreach (var entry in entries)
{
var splited = entry.Path.Split("/");
if(splited.Length < 2 ||
splited[0] != "runtimes" ||
splited[1] == runtimeIdentifier)
{
filtered.Add(entry);
}
}
return filtered;
}
private void Save(HashSet<LauncherManifestEntry> entries, LauncherRuntimeInfo info)
{
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries, info));
} }
private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry) private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry)
{ {
if(!PathValidator.IsSafePath(FileApi.RootPath, entry.Path)) if(!PathValidator.IsSafePath(_fileApi.RootPath, entry.Path))
throw new ArgumentException("Path contains invalid characters. Manifest hash: " + entry.Hash); throw new ArgumentException("Path contains invalid characters. Manifest hash: " + entry.Hash);
return entry; return entry;
} }
} }
public static class PathValidator public static class PathValidator
{ {
public static bool IsSafePath(string baseDirectory, string relativePath) public static bool IsSafePath(string baseDirectory, string relativePath)

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic; using System.Collections.Generic;
using Nebula.SharedModels;
namespace Nebula.UpdateResolver; namespace Nebula.UpdateResolver;

View File

@@ -26,4 +26,8 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets> <PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference> </PackageReference>
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
</ItemGroup>
</Project> </Project>

View File

@@ -1,5 +1,4 @@
using System; using System.IO;
using System.Diagnostics;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
@@ -9,31 +8,33 @@ namespace Nebula.UpdateResolver.Rest;
public static class Helper public static class Helper
{ {
public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web); public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web);
public static void SafeOpenBrowser(string uri)
{
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
{
Console.WriteLine("Unable to parse URI in server-provided link: {Link}", uri);
return;
}
if (parsedUri.Scheme is not ("http" or "https"))
{
Console.WriteLine("Refusing to open server-provided link {Link}, only http/https are allowed", parsedUri);
return;
}
OpenBrowser(parsedUri.ToString());
}
public static void OpenBrowser(string url)
{
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
}
public static async Task<T> AsJson<T>(this HttpContent content) where T : notnull public static async Task<T> AsJson<T>(this HttpContent content) where T : notnull
{ {
var str = await content.ReadAsStringAsync(); var str = await content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ?? return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
throw new JsonException("AsJson: did not expect null response"); throw new JsonException("AsJson: did not expect null response");
} }
public static void CopyTo(this Stream input, Stream output, string fileName, long totalLength)
{
const int bufferSize = 81920;
var buffer = new byte[bufferSize];
int skipStep = 0;
long totalRead = 0;
int bytesRead;
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
{
output.Write(buffer, 0, bytesRead);
totalRead += bytesRead;
skipStep++;
if(skipStep < 50) continue;
skipStep = 0;
LogStandalone.Log($"Saving {fileName}", (int)((totalRead / (float)totalLength) * 100));
}
}
} }

View File

@@ -1,4 +1,4 @@
using System.Collections.Generic; using Nebula.SharedModels;
using Nebula.UpdateResolver.Configuration; using Nebula.UpdateResolver.Configuration;
namespace Nebula.UpdateResolver; namespace Nebula.UpdateResolver;
@@ -9,13 +9,4 @@ public static class UpdateConVars
ConVarBuilder.Build<string>("update.url","https://durenko.tatar/nebula/manifest/"); ConVarBuilder.Build<string>("update.url","https://durenko.tatar/nebula/manifest/");
public static readonly ConVar<LauncherManifest> CurrentLauncherManifest = public static readonly ConVar<LauncherManifest> CurrentLauncherManifest =
ConVarBuilder.Build<LauncherManifest>("update.manifest"); ConVarBuilder.Build<LauncherManifest>("update.manifest");
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
new(){
{"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/10.0.2/dotnet-runtime-10.0.2-win-x86.zip"},
{"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");
} }

View File

@@ -1,5 +1,6 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArchiving_002EUtils_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27e9f12ad1e4318b9b02849ec3e6a502fa3ee761c4f0522ba756ab30cde1c_003FArchiving_002EUtils_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArchiving_002EUtils_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27e9f12ad1e4318b9b02849ec3e6a502fa3ee761c4f0522ba756ab30cde1c_003FArchiving_002EUtils_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArray_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003F45_003F049a0c03_003FArray_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssembly_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F501151723a8d43558c75acbd334f26322066fa4b1c82b1297291314bf92ff_003FAssembly_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssembly_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F501151723a8d43558c75acbd334f26322066fa4b1c82b1297291314bf92ff_003FAssembly_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHeaderValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88b338246f59cffdb6f3dc3d8dbcfc169599dc71d6f44a8f2732983db7f73a_003FAuthenticationHeaderValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHeaderValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88b338246f59cffdb6f3dc3d8dbcfc169599dc71d6f44a8f2732983db7f73a_003FAuthenticationHeaderValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3cc366334cc52275393f9def48cfcbccc8382175579fbd4f75b8c0e4bf33_003FAvaloniaList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3cc366334cc52275393f9def48cfcbccc8382175579fbd4f75b8c0e4bf33_003FAvaloniaList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -27,8 +28,10 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuncValueConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe91c13e7e24d7ba324e0e6eb12a24ea8c7761299d3c4703e55c86dd120835e61_003FFuncValueConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuncValueConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe91c13e7e24d7ba324e0e6eb12a24ea8c7761299d3c4703e55c86dd120835e61_003FFuncValueConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFunc_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fab_003F4dac48f4_003FFunc_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFunc_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fab_003F4dac48f4_003FFunc_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGC_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb35fce9cd25198662cd7a8324941fd794f2d31c98c32a35a18b355bc79386_003FGC_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpBaseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5c9ea82983a677ae263ed0c49dd93a5e32866ad7ae97beea733f6df197e995_003FHttpBaseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpBaseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5c9ea82983a677ae263ed0c49dd93a5e32866ad7ae97beea733f6df197e995_003FHttpBaseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1d7280b53be4f32b5e9b2587f54915348ec89107b99282d2748ac94b8c1_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpCompletionOption_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffcc079c54e9940c5ac59f0141dda9ad01b4928_003F28_003Fe60e6194_003FHttpCompletionOption_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpCompletionOption_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffcc079c54e9940c5ac59f0141dda9ad01b4928_003F28_003Fe60e6194_003FHttpCompletionOption_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9657cc383c70851dc2bdcf91eff27f21196844abfe552fc9c3243ff36974cd_003FHttpContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9657cc383c70851dc2bdcf91eff27f21196844abfe552fc9c3243ff36974cd_003FHttpContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpListener_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffb21cfde6c1ffa9b6be622d15d56f666ad94ada7dd7d81451418d807b98f2_003FHttpListener_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpListener_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffb21cfde6c1ffa9b6be622d15d56f666ad94ada7dd7d81451418d807b98f2_003FHttpListener_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -39,7 +42,9 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86529590f9604f327a3b1b19aec3ff2310f0654aa06bb8cef2e3d820ea3bfd_003FHttpRequestMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86529590f9604f327a3b1b19aec3ff2310f0654aa06bb8cef2e3d820ea3bfd_003FHttpRequestMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDispatcherImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F22d92db124764b1ab49745245c66f01b1e1a00_003F0f_003F01061787_003FIDispatcherImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDispatcherImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F22d92db124764b1ab49745245c66f01b1e1a00_003F0f_003F01061787_003FIDispatcherImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003F4a_003Ff21bf9b5_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEnumerable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003Fbc_003F64378026_003FIEnumerable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F22d92db124764b1ab49745245c66f01b1e1a00_003F_005F2c742_003FIGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F22d92db124764b1ab49745245c66f01b1e1a00_003F_005F2c742_003FIGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2b95745d8f2ddf7b8ad6130e01c5b2782e253ff11247a9aeefcef47277b1ab_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2b95745d8f2ddf7b8ad6130e01c5b2782e253ff11247a9aeefcef47277b1ab_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndex_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2a1a813823579c69832f1304f97761e7be433bd6aa928f351d138050b56a38_003FIndex_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndex_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2a1a813823579c69832f1304f97761e7be433bd6aa928f351d138050b56a38_003FIndex_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -60,6 +65,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanelContainerGenerator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F66d34460cd4eaf18a9301eeb1e7ae63cfc928ce72cc7e064e398d2bcfb9628_003FPanelContainerGenerator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -77,10 +83,13 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStreamGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5245275d55a2287c120f7503c3e453ddeee7c693d2f85f8cde43f7c8f01ee6_003FStreamGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStreamGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5245275d55a2287c120f7503c3e453ddeee7c693d2f85f8cde43f7c8f01ee6_003FStreamGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStreamReader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a802af2346a87e430fb66291f320aa22871259d47c2bc928a59f14b42aa34_003FStreamReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStreamReader_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a802af2346a87e430fb66291f320aa22871259d47c2bc928a59f14b42aa34_003FStreamReader_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1287462d4ec4078c61b8e92a0952fb7de3e7e877d279e390a4c136a6365126_003FStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1287462d4ec4078c61b8e92a0952fb7de3e7e877d279e390a4c136a6365126_003FStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStringBuilder_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd98be6eaa7445b7eb5caec7916b10e37af115adb1635b6336772135513ae6_003FStringBuilder_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AString_002EManipulation_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe75a5575ba872c8ea754c015cb363850e6c661f39569712d5b74aaca67263c_003FString_002EManipulation_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyledElement_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe49d9521ff091d353928d1c44539ba0a4c93a9ebb2e65190880b4fe5eb8_003FStyledElement_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcfbd5689fdab68d1c02f6a9b3c5921abcc409b8743dcc958da77cc1cfcb8e_003FStyle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcfbd5689fdab68d1c02f6a9b3c5921abcc409b8743dcc958da77cc1cfcb8e_003FStyle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F43273dba3ac6a4e11aefe78fbbccf5d36f07542ca37ecebffb25c95d1a1c16b_003FTextBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F43273dba3ac6a4e11aefe78fbbccf5d36f07542ca37ecebffb25c95d1a1c16b_003FTextBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cde391207de75962d7bacb899ca2bd3985c86911b152d185b58999a422bf0_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cde391207de75962d7bacb899ca2bd3985c86911b152d185b58999a422bf0_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F76bfc310d2c1ee89b5a3bb7f318b5c55015f66ebdf298211386b17a57fc25b_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F76_003F1f1e9043_003FUserControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F76_003F1f1e9043_003FUserControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValuePrinter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F80d1676fb411442983574149e0b6aebc72e00_003F2f_003F26a40f58_003FValuePrinter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValuePrinter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F80d1676fb411442983574149e0b6aebc72e00_003F2f_003F26a40f58_003FValuePrinter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -96,23 +105,12 @@
&lt;/AssemblyExplorer&gt;</s:String> &lt;/AssemblyExplorer&gt;</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=47519EA2_002D03C0_002D49D8_002D86CA_002D418F6B7267A4/@EntryIndexedValue">735691F8-949C-4476-B9E4-5DF6FF8D3D0B</s:String> <s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestProjectMapping/=47519EA2_002D03C0_002D49D8_002D86CA_002D418F6B7267A4/@EntryIndexedValue">735691F8-949C-4476-B9E4-5DF6FF8D3D0B</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=NUnit3x/@EntryIndexedValue">db4927dd-2e12-48a7-9a84-2b7e3e31b9c8</s:String> <s:String x:Key="/Default/Environment/UnitTesting/CreateUnitTestDialog/TestTemplateMapping/=NUnit3x/@EntryIndexedValue">db4927dd-2e12-48a7-9a84-2b7e3e31b9c8</s:String>
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=d7603912_002D51de_002D4bca_002D9082_002D886ee6d4c2f5/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="RestServiceTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD; <s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=44ee9fbc_002Dddaf_002D4efa_002Da7df_002D48c0ce9aec7e/@EntryIndexedValue">&lt;SessionState ContinuousTestingMode="0" IsActive="True" Name="CryptographicTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"&gt;&#xD;
&lt;TestAncestor&gt;&#xD; &lt;TestAncestor&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.RestServiceTests&lt;/TestId&gt;&#xD; &lt;TestId&gt;NUnit3x::7430875B-ABAA-D4E0-C34F-0797C4762C66::net10.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.PopupMessageServiceTests.PopupMessageServiceTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.PopupMessageServiceTests.DisposeTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.CreateFileApi_CreatesCorrectPath&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.EnsureTempDir_CreatesDirectoryAndReturnsApi&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.OpenZip_ReturnsZipFileApi_WhenValid&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.RemoveAllFiles_DeletesAllFilesAndDirectories&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.OpenZip_ThrowsException_WhenFileApiFails&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.EngineServiceTests.GetVersionInfo_ReturnsCorrectBuildInfo&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.EngineServiceTests.TryGetVersionInfo_ReturnsTrue&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests.WriteConVarTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests.WriteArrayConvarTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest.EncryptDecrypt&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest&lt;/TestId&gt;&#xD;
&lt;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.TarTest.Download&lt;/TestId&gt;&#xD;
&lt;/TestAncestor&gt;&#xD; &lt;/TestAncestor&gt;&#xD;
&lt;/SessionState&gt;</s:String></wpf:ResourceDictionary> &lt;/SessionState&gt;</s:String>
</wpf:ResourceDictionary>

View File

@@ -2,6 +2,7 @@
<Project Path="Nebula.Launcher\Nebula.Launcher.csproj" Type="Classic C#" /> <Project Path="Nebula.Launcher\Nebula.Launcher.csproj" Type="Classic C#" />
<Project Path="Nebula.Packager\Nebula.Packager.csproj" Type="Classic C#" /> <Project Path="Nebula.Packager\Nebula.Packager.csproj" Type="Classic C#" />
<Project Path="Nebula.Runner\Nebula.Runner.csproj" Type="Classic C#" /> <Project Path="Nebula.Runner\Nebula.Runner.csproj" Type="Classic C#" />
<Project Path="Nebula.SharedModels\Nebula.SharedModels.csproj" Type="Classic C#" />
<Project Path="Nebula.Shared\Nebula.Shared.csproj" Type="Classic C#" /> <Project Path="Nebula.Shared\Nebula.Shared.csproj" Type="Classic C#" />
<Project Path="Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" Type="Classic C#" /> <Project Path="Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" Type="Classic C#" />
<Project Path="Nebula.UnitTest\Nebula.UnitTest.csproj" Type="Classic C#" /> <Project Path="Nebula.UnitTest\Nebula.UnitTest.csproj" Type="Classic C#" />