7 Commits

Author SHA1 Message Date
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
5306a86d13 - fix: dotnet url 2026-01-16 18:44:09 +03:00
52 changed files with 755 additions and 371 deletions

View File

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

View File

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

View File

@@ -62,5 +62,7 @@
<ItemGroup>
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
<ProjectReference Include="..\Nebula.Runner\Nebula.Runner.csproj"
ReferenceOutputAssembly="false" />
</ItemGroup>
</Project>

View File

@@ -1,5 +1,6 @@
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Nebula.Shared.Services;
@@ -9,11 +10,11 @@ public abstract class DotnetProcessStartInfoProviderBase(DotnetResolverService r
{
protected abstract string GetDllPath();
public virtual async Task<ProcessStartInfo> GetProcessStartInfo()
public virtual async Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default)
{
return new ProcessStartInfo
{
FileName = await resolverService.EnsureDotnet(),
FileName = await resolverService.EnsureDotnet(cancellationToken),
Arguments = GetDllPath(),
CreateNoWindow = true,
UseShellExecute = false,

View File

@@ -2,6 +2,7 @@ using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared;
@@ -31,9 +32,9 @@ public sealed class GameProcessStartInfoProvider(DotnetResolverService resolverS
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;
if(authProv is null)

View File

@@ -6,7 +6,6 @@ using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
using Robust.LoaderApi;
namespace Nebula.Launcher.ProcessHelper;
@@ -22,8 +21,7 @@ public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService
if (engine is null)
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))
{

View File

@@ -1,9 +1,10 @@
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
namespace Nebula.Launcher.ProcessHelper;
public interface IProcessStartInfoProvider
{
public Task<ProcessStartInfo> GetProcessStartInfo();
public Task<ProcessStartInfo> GetProcessStartInfo(CancellationToken cancellationToken = default);
}

View File

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

@@ -15,6 +15,7 @@ using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.SharedModels;
namespace Nebula.Launcher.Services;
@@ -32,7 +33,7 @@ public sealed partial class DecompilerService
private readonly HttpClient _httpClient = new();
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");
public async void OpenDecompiler(string arguments){
@@ -61,13 +62,12 @@ public sealed partial class DecompilerService
myTempDir.Save(file, stream);
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);
await stream.DisposeAsync();
}

View File

@@ -22,7 +22,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer
{
var id = _keyPool.Take();
var currentContentLogConsumer = new ContentLogConsumer();
var currentContentLogConsumer = new ContentLogConsumer(popupMessageService);
var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id));
var logContainer = new ProcessLogConsumerCollection();
logContainer.RegisterLogger(currentContentLogConsumer);
@@ -43,7 +43,7 @@ public sealed class InstanceRunningContainer(PopupMessageService popupMessageSer
if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler))
return;
handler.Popup(popupMessageService);
handler.Popup();
}
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

@@ -3,9 +3,7 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Avalonia.Logging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Nebula.Launcher.Models;
using Nebula.Launcher.Services;
using Nebula.Launcher.Utils;
@@ -16,6 +14,7 @@ using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper;
using Nebula.SharedModels;
namespace Nebula.Launcher.ViewModels;
@@ -209,7 +208,7 @@ public partial class MainViewModel : ViewModelBase
public void OpenRootPath()
{
ExplorerUtils.OpenFolder(FileService.RootPath);
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
}
public void OpenLink()
@@ -231,6 +230,11 @@ public partial class MainViewModel : ViewModelBase
case PopupViewModelBase @base:
PopupMessage(@base);
break;
case ExceptionCompound error:
var errViewModel = ViewHelperService.GetViewModel<ExceptionListViewModel>();
errViewModel.AppendError(error);
PopupMessage(errViewModel);
break;
case Exception error:
var err = ViewHelperService.GetViewModel<ExceptionListViewModel>();
_logger.Error(error);

View File

@@ -12,6 +12,7 @@ using Nebula.Shared;
using Nebula.Shared.Configurations;
using Nebula.Shared.Services;
using Nebula.Shared.ViewHelper;
using Nebula.SharedModels;
namespace Nebula.Launcher.ViewModels.Pages;
@@ -70,12 +71,12 @@ public partial class ConfigurationViewModel : ViewModelBase
public void OpenDataFolder()
{
ExplorerUtils.OpenFolder(FileService.RootPath);
ExplorerUtils.OpenFolder(AppDataPath.RootPath);
}
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());
Directory.CreateDirectory(path);

View File

@@ -15,11 +15,11 @@ using Nebula.Launcher.Utils;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Launcher.Views.Pages;
using Nebula.Shared.FileApis;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper;
using Robust.LoaderApi;
namespace Nebula.Launcher.ViewModels.Pages;
@@ -199,9 +199,9 @@ public sealed class ExtContentExecutor
_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")
{
@@ -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 IContentEntry? Parent { get; set; }
public string? Name { get; set; }
public string IconPath => "/Assets/svg/file.svg";
private RobustManifestItem _manifestItem;
private HashApi _hashApi = default!;
private IFileApi _fileApi = 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;
Name = new ContentPath(manifestItem.Path).GetName();
_manifestItem = manifestItem;
_hashApi = api;
Name = fileName;
_fileApi = api;
_extContentExecutor = executor;
}
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;
var ext = Path.GetExtension(_manifestItem.Path);
var ext = Path.GetExtension(fullPath.GetName());
try
{
if (!_hashApi.TryOpen(_manifestItem, out var stream))
if (!_fileApi.TryOpen(fullPath.Path, out var stream))
return null;
var myTempFile = Path.Combine(Path.GetTempPath(), "tempie" + ext);
var sw = new FileStream(myTempFile, FileMode.Create, FileAccess.Write, FileShare.None);
stream.CopyTo(sw);
@@ -298,7 +295,7 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
public RobustUrl ServerUrl { get; private set; }
public HashApi FileApi { get; private set; } = default!;
public IFileApi FileApi { get; private set; } = default!;
private ExtContentExecutor _contentExecutor = default!;
@@ -315,12 +312,12 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
Task.Run(async () =>
{
var buildInfo = await ContentService.GetBuildInfo(serverUrl, CancellationService.Token);
FileApi = await ContentService.EnsureItems(buildInfo.RobustManifestInfo, loading,
FileApi = await ContentService.EnsureItems(buildInfo, loading,
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;
@@ -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();
BaseFolderContentEntry parent = this;
@@ -345,8 +342,8 @@ public sealed partial class ServerFolderContentEntry : BaseFolderContentEntry
parent = folderContentEntry as BaseFolderContentEntry ?? throw new InvalidOperationException();
}
var manifestContent = new ManifestContentEntry();
manifestContent.Init(Holder, manifestItem, FileApi, _contentExecutor);
var manifestContent = new FileContentEntry();
manifestContent.Init(Holder, FileApi, path.GetName(), _contentExecutor);
parent.AddChild(manifestContent);

View File

@@ -15,7 +15,7 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase
public override string Title => LocalizationService.GetString("popup-exception");
public override bool IsClosable => true;
public ObservableCollection<Exception> Errors { get; } = new();
public ObservableCollection<ExceptionCompound> Errors { get; } = new();
protected override void Initialise()
{
@@ -23,13 +23,18 @@ public sealed partial class ExceptionListViewModel : PopupViewModelBase
protected override void InitialiseInDesignMode()
{
var e = new Exception("TEST");
var e = new ExceptionCompound("TEST", "thrown in design mode");
AppendError(e);
}
public void AppendError(ExceptionCompound exception)
{
Errors.Add(exception);
}
public void AppendError(Exception exception)
{
Errors.Add(exception);
AppendError(new ExceptionCompound(exception));
if (exception.InnerException != null)
AppendError(exception.InnerException);
}

View File

@@ -18,7 +18,6 @@ namespace Nebula.Launcher.ViewModels;
public sealed partial class ServerCompoundEntryViewModel :
ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder
{
private ServerEntryModelView? _currentEntry;
[ObservableProperty] private string _message = "Loading server entry...";
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _loading = true;
@@ -29,22 +28,22 @@ public sealed partial class ServerCompoundEntryViewModel :
public ServerEntryModelView? CurrentEntry
{
get => _currentEntry;
get;
set
{
if (value == _currentEntry) return;
_currentEntry = value;
if (value == field) return;
if (_currentEntry != null)
field = value;
if (field != null)
{
_currentEntry.IsFavorite = IsFavorite;
_currentEntry.Name = Name;
_currentEntry.ProcessFilter(_currentFilter);
field.IsFavorite = IsFavorite;
field.Name = Name;
field.ProcessFilter(_currentFilter);
}
Loading = _currentEntry == null;
Loading = field == null;
OnPropertyChanged();
}
}

View File

@@ -230,6 +230,7 @@ public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsume
public void Dispose()
{
_logger.Dispose();
InstanceRunningContainer.IsRunningChanged -= IsRunningChanged;
}
}

View File

@@ -3,14 +3,14 @@
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="Nebula.Launcher.Views.ExceptionView"
x:DataType="system:Exception"
x:DataType="viewModels:ExceptionCompound"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
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>
<system:Exception />
<viewModels:ExceptionCompound />
</Design.DataContext>
<Border
BoxShadow="{StaticResource DefaultShadow}"

View File

@@ -2,6 +2,8 @@ using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Nebula.Launcher.Models;
using Nebula.Launcher.ViewModels;
namespace Nebula.Launcher.Views;
@@ -14,6 +16,6 @@ public partial class ExceptionView : UserControl
public ExceptionView(Exception exception): this()
{
DataContext = exception;
DataContext = new ExceptionCompound(exception);
}
}

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>
<Nullable>enable</Nullable>
</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>

View File

@@ -2,24 +2,36 @@
using System.Security.Cryptography;
using System.Text.Json;
using System.Text.Json.Serialization;
using Nebula.Shared;
using Nebula.SharedModels;
namespace Nebula.Packager;
public static class Program
{
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
{
FileName = "dotnet",
ArgumentList =
{
"publish",
Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"),
Path.Combine(rootPath, "Nebula.Launcher", "Nebula.Launcher.csproj"),
"-c", configuration,
}
};
@@ -57,18 +69,14 @@ public static class Program
entries.Add(new LauncherManifestEntry(hashStr, 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"));
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.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
@@ -16,6 +15,7 @@ public sealed class App(RunnerService runnerService, ContentService contentServi
public void Redial(Uri uri, string text = "")
{
throw new Exception($"Redial requested. Reason: {text}");
}
public async Task Run(string[] args1)
@@ -49,7 +49,7 @@ public sealed class App(RunnerService runnerService, ContentService contentServi
args.Add("--ss14-address");
args.Add(url.ToString());
await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandlerFactory(), cancelTokenSource.Token);
await runnerService.Run(args.ToArray(), buildInfo, this, new ConsoleLoadingHandlerFactory(), login, cancelTokenSource.Token);
}
catch (Exception e)
{

View File

@@ -7,6 +7,7 @@ using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
using Nebula.SharedModels;
using Robust.LoaderApi;
namespace Nebula.Runner.Services;
@@ -25,8 +26,8 @@ public sealed class RunnerService(
private bool MetricEnabled = false; //TODO: ADD METRIC THINKS LATER
public async Task Run(string[] runArgs, RobustBuildInfo buildInfo, IRedialApi redialApi,
ILoadingHandlerFactory loadingHandler,
CancellationToken cancellationToken)
ILoadingHandlerFactory loadingHandler, string? userDataPath = null,
CancellationToken cancellationToken = default)
{
_logger.Log("Start Content!");
@@ -35,14 +36,14 @@ public sealed class RunnerService(
if (engine is null)
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>
{
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);
@@ -80,6 +81,12 @@ public sealed class RunnerService(
metricServer = RunHelper.RunMetric(prometheusAssembly);
}
if (userDataPath is not null)
{
UserDataDirPatcher.UserPath = userDataPath;
UserDataDirPatcher.ApplyPatch(reflectionService, harmonyService);
}
loadingHandler.Dispose();
await Task.Run(() => loader.Main(args), cancellationToken);
@@ -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 IDisposable RunMetric(Assembly prometheusAssembly)

View File

@@ -34,8 +34,8 @@ public static class CurrentConVar
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.exe"},
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x86.exe"},
{"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"}
});

View File

@@ -3,6 +3,7 @@ namespace Nebula.Shared.Models;
public class RobustBuildInfo
{
public ServerInfo BuildInfo = default!;
public RobustManifestInfo RobustManifestInfo;
public RobustManifestInfo? RobustManifestInfo;
public RobustZipContentInfo? DownloadUri;
public RobustUrl Url = default!;
}

View File

@@ -1,3 +1,4 @@
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,
[property: JsonPropertyName("manifest_url")]
string ManifestUrl,
[property: JsonPropertyName("acz")] bool Acz,
[property: JsonPropertyName("acz")] bool? Acz,
[property: JsonPropertyName("hash")] string Hash,
[property: JsonPropertyName("manifest_hash")]
string ManifestHash);

View File

@@ -9,12 +9,14 @@
<EmbeddedResource Include="Utils\runtime.json">
<LogicalName>Utility.runtime.json</LogicalName>
</EmbeddedResource>
<PackageReference Include="JetBrains.Annotations" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions"/>
<PackageReference Include="Robust.Natives"/>
<PackageReference Include="SharpZstd.Interop"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
</ItemGroup>

View File

@@ -1,4 +1,5 @@
using System.Reflection;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
namespace Nebula.Shared;
@@ -42,6 +43,7 @@ public static class ServiceExt
}
}
[MeansImplicitUse]
public sealed class ServiceRegisterAttribute : Attribute
{
public ServiceRegisterAttribute(Type? inference = null, bool isSingleton = true)

View File

@@ -1,12 +1,15 @@
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.IO.Compression;
using System.Net.Http.Headers;
using System.Numerics;
using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Utils;
using Robust.LoaderApi;
namespace Nebula.Shared.Services;
@@ -14,6 +17,7 @@ public partial class ContentService
{
public readonly IReadWriteFileApi ContentFileApi = fileService.CreateFileApi("content");
public readonly IReadWriteFileApi ManifestFileApi = fileService.CreateFileApi("manifest");
public readonly IReadWriteFileApi ZipContentApi = fileService.CreateFileApi("zipContent");
public void SetServerHash(string address, string hash)
{
@@ -33,8 +37,20 @@ public partial class ContentService
{
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,
CancellationToken cancellationToken)
{
@@ -58,7 +74,41 @@ public partial class ContentService
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)
{
_logger.Log("Getting manifest: " + info.Hash);
@@ -90,10 +140,10 @@ public partial class ContentService
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");
var items = hashApi.Manifest.Values.ToList();
var items = hashApi.AllFiles.ToList();
loadingHandler.AppendJob(items.Count);
var options = new ParallelOptions
@@ -105,13 +155,13 @@ public partial class ContentService
{
if (hashApi.TryOpen(item, out var stream))
{
_logger.Log($"Unpack {item.Hash} to: {item.Path}");
otherApi.Save(item.Path, stream);
_logger.Log($"Unpack {item}");
otherApi.Save(item, stream);
stream.Close();
}
else
{
_logger.Error("Error while unpacking thinks " + item.Path);
_logger.Error($"Error while unpacking thinks {item}");
}
loadingHandler.AppendResolvedJob();

View File

@@ -19,7 +19,14 @@ public partial class ContentService(
info.Url = url;
var bi = await restService.GetAsync<ServerInfo>(url.InfoUri, cancellationToken);
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"),
bi.Build.ManifestHash)
: new RobustManifestInfo(new Uri(info.BuildInfo.Build.ManifestUrl),

View File

@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using System.Reflection;
using Nebula.Shared.Services.Logging;
using Nebula.SharedModels;
namespace Nebula.Shared.Services;
@@ -10,7 +11,7 @@ public class DebugService : IDisposable
public static bool DoFileLog;
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()
{

View File

@@ -2,50 +2,54 @@ using System.IO.Compression;
using System.Runtime.InteropServices;
using System.Text;
using Nebula.Shared.Utils;
using Nebula.SharedModels;
namespace Nebula.Shared.Services;
[ServiceRegister]
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();
public async Task<string> EnsureDotnet()
public async Task<string> EnsureDotnet(CancellationToken cancellationToken = default)
{
if (!Directory.Exists(FullPath))
await Download();
var dotnetEntry = new LauncherRuntimeInfo(
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);
debugLogger.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
);
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
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();
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))
{
using var zipArchive = new ZipArchive(stream);
zipArchive.ExtractToDirectory(FullPath, true);
await using var zipArchive = new ZipArchive(stream);
await zipArchive.ExtractToDirectoryAsync(fullPath, true, cancellationToken);
}
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{
TarUtils.ExtractTarGz(stream, FullPath);
TarUtils.ExtractTarGz(stream, fullPath);
}
else
{
@@ -55,36 +59,3 @@ public class DotnetResolverService(DebugService debugService, ConfigurationServi
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.Runtime.InteropServices;
using Nebula.Shared.FileApis;
using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Services.Logging;
using Nebula.SharedModels;
using Robust.LoaderApi;
namespace Nebula.Shared.Services;
@@ -10,23 +12,20 @@ namespace Nebula.Shared.Services;
[ServiceRegister]
public class FileService
{
public static string RootPath = Path.Join(Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData), "Datum");
private readonly ILogger _logger;
public FileService(DebugService debugService)
{
_logger = debugService.GetLogger(this);
if(!Directory.Exists(RootPath))
Directory.CreateDirectory(RootPath);
if(!Directory.Exists(AppDataPath.RootPath))
Directory.CreateDirectory(AppDataPath.RootPath);
}
public IReadWriteFileApi CreateFileApi(string 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)
@@ -59,7 +58,7 @@ public class FileService
public void RemoveAllFiles(string fileApiName,ILoadingHandler loadingHandler, CancellationToken cancellationToken)
{
_logger.Debug($"Deleting files from {fileApiName}");
var path = Path.Combine(RootPath, fileApiName);
var path = Path.Combine(AppDataPath.RootPath, fileApiName);
var di = new DirectoryInfo(path);
@@ -89,6 +88,7 @@ public class FileService
}
}
public sealed class ConsoleLoadingHandlerFactory : ILoadingHandlerFactory
{
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 Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.SharedModels;
using Robust.LoaderApi;
namespace Nebula.UnitTest.NebulaSharedTests;
@@ -36,7 +37,7 @@ public class FileServiceTests : BaseSharedTest
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}");
}
@@ -53,7 +54,7 @@ public class FileServiceTests : BaseSharedTest
[Test]
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))
{
var entry = archive.CreateEntry("test.txt");
@@ -93,7 +94,7 @@ public class FileServiceTests : BaseSharedTest
[Test]
public void RemoveAllFiles_DeletesAllFilesAndDirectories()
{
var testDir = Path.Combine(FileService.RootPath, "cleanup-test");
var testDir = Path.Combine(AppDataPath.RootPath, "cleanup-test");
Directory.CreateDirectory(testDir);
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.Logging;
using Nebula.Shared.Utils;
using Nebula.SharedModels;
namespace Nebula.UnitTest.NebulaSharedTests;
@@ -31,28 +32,26 @@ public class TarTest : BaseSharedTest
[Test]
public async Task DownloadTarAndUnzipTest()
{
DotnetUrlHelper.RidOverrideTest = "linux-x64";
Console.WriteLine($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
_configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!
);
Console.WriteLine($"Downloading dotnet linux-x64...");
if(!_configurationService.GetConfigValue(CurrentConVar.DotnetUrl)!.TryGetValue("linux-x64", out var url))
throw new NullReferenceException();
using var response = await _httpClient.GetAsync(url);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync();
Directory.CreateDirectory(FileService.RootPath);
Directory.CreateDirectory(AppDataPath.RootPath);
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
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)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{
TarUtils.ExtractTarGz(stream, FileService.RootPath);
TarUtils.ExtractTarGz(stream, AppDataPath.RootPath);
}
else
{

View File

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

View File

@@ -7,7 +7,8 @@ using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using Nebula.UpdateResolver.Configuration;
using Nebula.SharedModels;
using Nebula.UpdateResolver.Rest;
namespace Nebula.UpdateResolver;
@@ -15,19 +16,13 @@ public static class DotnetStandalone
{
private static readonly HttpClient HttpClient = new();
private static readonly string FullPath =
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)
public static async Task<Process?> Run(LauncherRuntimeInfo runtimeInfo, string dllPath)
{
await EnsureDotnet();
await EnsureDotnet(runtimeInfo);
return Process.Start(new ProcessStartInfo
{
FileName = ExecutePath,
FileName = runtimeInfo.GetExecutePath(),
Arguments = dllPath,
CreateNoWindow = true,
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))
await Download();
if (!File.Exists(runtimeInfo.GetExecutePath()))
await Download(runtimeInfo);
}
private static async Task Download()
private static async Task Download(LauncherRuntimeInfo runtimeInfo)
{
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!
);
var fullPath = runtimeInfo.GetFullPath();
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();
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))
{
using var zipArchive = new ZipArchive(stream);
zipArchive.ExtractToDirectory(FullPath, true);
await using var zipArchive = new ZipArchive(tempStream);
await zipArchive.ExtractToDirectoryAsync(fullPath, true);
}
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
{
TarUtils.ExtractTarGz(stream, FullPath);
TarUtils.ExtractTarGz(tempStream, fullPath);
}
else
{
@@ -74,32 +74,4 @@ public static class DotnetStandalone
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.IO;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using Nebula.SharedModels;
using Nebula.UpdateResolver.Configuration;
using Nebula.UpdateResolver.Rest;
@@ -14,31 +15,36 @@ namespace Nebula.UpdateResolver;
public partial class MainWindow : Window
{
public static readonly string RootPath = Path.Join(Environment.GetFolderPath(
Environment.SpecialFolder.ApplicationData), "Datum");
public static readonly string RootPath = AppDataPath.GetAppDataPath("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()
{
InitializeComponent();
LogStandalone.OnLog += (message, percentage) =>
{
ProgressLabel.Content = message;
if (percentage == 0)
PercentLabel.Content = "";
else
PercentLabel.Content = percentage + "%";
var percentText = "";
if (percentage != 0)
percentText = $"{percentage}%";
Dispatcher.UIThread.Invoke(() =>
{
ProgressLabel.Content = message;
PercentLabel.Content = percentText;
});
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);
LogStr += messageOut + "\n";
_logStr += messageOut + "\n";
};
LogStandalone.Log("Starting up");
if (!Design.IsDesignMode)
_ = Start();
Task.Run(Start);
else
LogStandalone.Log("Debug information", 51);
}
@@ -47,17 +53,21 @@ public partial class MainWindow : Window
{
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...");
foreach (var file in info.ToDelete)
{
LogStandalone.Log("Deleting " + file.Path);
FileApi.Remove(file.Path);
_fileApi.Remove(file.Path);
}
var loadedManifest = info.FilesExist;
Save(loadedManifest);
Save(loadedManifest, manifest.RuntimeInfo);
var count = info.ToDownload.Count;
var resolved = 0;
@@ -70,23 +80,23 @@ public partial class MainWindow : Window
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync();
FileApi.Save(file.Path, stream);
_fileApi.Save(file.Path, stream);
resolved++;
LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
loadedManifest.Add(file);
Save(loadedManifest);
Save(loadedManifest, manifest.RuntimeInfo);
}
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){
LogStandalone.LogError(e);
LogStandalone.Log("Network connection error...");
var logPath = Path.Join(RootPath,"updateResloverError.txt");
await File.WriteAllTextAsync(logPath, LogStr);
await File.WriteAllTextAsync(logPath, _logStr);
Process.Start(new ProcessStartInfo(){
FileName = "notepad",
Arguments = logPath
@@ -96,7 +106,7 @@ public partial class MainWindow : Window
{
LogStandalone.LogError(e);
var logPath = Path.Join(RootPath,"updateResloverError.txt");
await File.WriteAllTextAsync(logPath, LogStr);
await File.WriteAllTextAsync(logPath, _logStr);
Process.Start(new ProcessStartInfo(){
FileName = "notepad",
Arguments = logPath
@@ -108,11 +118,9 @@ public partial class MainWindow : Window
Environment.Exit(0);
}
private async Task<ManifestEnsureInfo> EnsureFiles()
private ManifestEnsureInfo EnsureFiles(HashSet<LauncherManifestEntry> entries)
{
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 toDelete = new HashSet<LauncherManifestEntry>();
@@ -124,13 +132,13 @@ public partial class MainWindow : Window
LogStandalone.Log("Delta manifest loaded!");
foreach (var file in currentManifest.Entries)
{
if (!manifest.Entries.Contains(file))
if (!entries.Contains(file))
toDelete.Add(EnsurePath(file));
else
filesExist.Add(EnsurePath(file));
}
foreach (var file in manifest.Entries)
foreach (var file in entries)
{
if(!currentManifest.Entries.Contains(file))
toDownload.Add(EnsurePath(file));
@@ -138,30 +146,48 @@ public partial class MainWindow : Window
}
else
{
toDownload = manifest.Entries;
toDownload = entries;
}
LogStandalone.Log("Saving launcher manifest...");
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)
{
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);
return entry;
}
}
public static class PathValidator
{
public static bool IsSafePath(string baseDirectory, string relativePath)

View File

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

View File

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

View File

@@ -1,5 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
@@ -9,31 +8,33 @@ namespace Nebula.UpdateResolver.Rest;
public static class Helper
{
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
{
var str = await content.ReadAsStringAsync();
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
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;
namespace Nebula.UpdateResolver;
@@ -9,13 +9,4 @@ public static class UpdateConVars
ConVarBuilder.Build<string>("update.url","https://durenko.tatar/nebula/manifest/");
public static readonly ConVar<LauncherManifest> CurrentLauncherManifest =
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.exe"},
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x86.exe"},
{"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">
<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_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>
@@ -29,6 +30,7 @@
<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_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_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_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>
@@ -40,6 +42,7 @@
<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_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_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>
@@ -77,7 +80,9 @@
<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_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_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_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>
@@ -96,23 +101,12 @@
&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/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;TestId&gt;NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.RestServiceTests&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;TestId&gt;NUnit3x::7430875B-ABAA-D4E0-C34F-0797C4762C66::net10.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest&lt;/TestId&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.Packager\Nebula.Packager.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.SourceGenerators\Nebula.SourceGenerators.csproj" Type="Classic C#" />
<Project Path="Nebula.UnitTest\Nebula.UnitTest.csproj" Type="Classic C#" />