8 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
53 changed files with 1155 additions and 851 deletions

View File

@@ -2,5 +2,6 @@
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
<mapping directory="$PROJECT_DIR$/Robust.LoaderApi" vcs="Git" />
</component>
</project>

View File

@@ -36,7 +36,7 @@ public static class LauncherConVar
new AuthServerCredentials(
"WizDen",
[
"https://harpy.durenko.tatar/auth-api/",
"https://feline.durenko.tatar/auth-api/",
"https://auth.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", [
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")
]);
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",
"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");
}

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>
<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,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
@@ -17,50 +19,27 @@ using Nebula.Shared.Utils;
namespace Nebula.Launcher.ServerListProviders;
[ServiceRegister, ConstructGenerator]
public sealed partial class FavoriteServerListProvider : IServerListProvider, IServerListDirtyInvoker
public sealed partial class FavoriteServerListProvider : IServerListProvider
{
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
private List<IListEntryModelView> _serverLists = [];
private string[] rawServerLists = [];
public Action? OnRefreshRequired;
public bool IsLoaded { get; private set; }
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();
}
private string[] _rawServerLists = [];
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)
@@ -71,10 +50,10 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
if(ServerViewContainer.Get(robustUrl) is IFavoriteEntryModelView favoriteView) favoriteView.IsFavorite = true;
}
public void RemoveFavorite(ServerEntryModelView entryModelView)
public void RemoveFavorite(ServerEntryViewModel entryViewModel)
{
var servers = GetFavoriteEntries();
servers.Remove(entryModelView.Address.ToString());
servers.Remove(entryViewModel.Address.ToString());
ConfigurationService.SetConfigValue(LauncherConVar.Favorites, servers.ToArray());
}
@@ -87,7 +66,7 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
private List<string> GetFavoriteEntries()
{
return rawServerLists.ToList();
return _rawServerLists.ToList();
}
private void Initialise()
@@ -99,26 +78,20 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS
{
if (value == null)
{
rawServerLists = [];
Dirty?.Invoke();
_rawServerLists = [];
return;
}
rawServerLists = value;
Dirty?.Invoke();
_rawServerLists = value;
OnRefreshRequired?.Invoke();
}
private void InitialiseInDesignMode(){}
public void Dispose()
{
OnDisposed?.Invoke();
}
}
public sealed class AddFavoriteButton: Border, IListEntryModelView{
private Button _addFavoriteButton = new Button();
private readonly Button _addFavoriteButton = new();
public AddFavoriteButton(IServiceProvider serviceProvider)
{
Margin = new Thickness(5, 5, 5, 20);
@@ -133,10 +106,5 @@ public sealed class AddFavoriteButton: Border, IListEntryModelView{
_addFavoriteButton.Content = "Add Favorite";
Child = _addFavoriteButton;
}
public bool IsFavorite { get; set; }
public void Dispose()
{
}
public void Dispose(){}
}

View File

@@ -1,83 +1,107 @@
using System;
using System.Collections.Generic;
using System.Linq;
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.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ServerListProviders;
[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 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 readonly List<IListEntryModelView> _servers = [];
private readonly List<Exception> _errors = [];
private string _hubUrl;
public HubServerListProvider With(string hubUrl)
{
HubUrl = hubUrl;
_hubUrl = hubUrl;
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)
{
await _cts.CancelAsync();
_cts = null;
}
CancellationTokenSource localCts;
_servers.Clear();
_errors.Clear();
IsLoaded = false;
_cts = new CancellationTokenSource();
await _loadLock.WaitAsync();
try
{
_cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
localCts = _cts;
}
finally
{
_loadLock.Release();
}
try
{
var servers =
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(HubUrl), _cts.Token);
servers.Sort(new ServerComparer());
if(_cts.Token.IsCancellationRequested) return;
_servers.AddRange(
servers.Select(h=>
ServerViewContainer.Get(h.Address.ToRobustUrl(), h.StatusData)
)
var serversRaw = await RestService.GetAsync<List<ServerHubInfo>>(
new Uri(_hubUrl),
localCts.Token
);
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)
{
_errors.Add(new Exception($"Some error while loading server list from {HubUrl}. See inner exception", e));
_errors.Add(e);
Console.WriteLine(e);
exceptions.Add(
new Exception(
$"Some error while loading server list from {_hubUrl}. See inner exception",
e
)
);
}
IsLoaded = true;
OnLoaded?.Invoke();
}
private void Initialise(){}
@@ -85,7 +109,17 @@ public sealed partial class HubServerListProvider : IServerListProvider
public void Dispose()
{
OnDisposed?.Invoke();
_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.Collections.Generic;
using Nebula.Launcher.ViewModels;
using Avalonia.Collections;
using Nebula.Launcher.ViewModels.Pages;
namespace Nebula.Launcher.ServerListProviders;
public interface IServerListProvider : IDisposable
public interface IServerListProvider
{
public bool IsLoaded { get; }
public Action? OnLoaded { get; set; }
public Action? OnDisposed { get; set; }
public IEnumerable<IListEntryModelView> GetServers();
public IEnumerable<Exception> GetErrors();
public void LoadServerList();
}
public interface IServerListDirtyInvoker
{
public Action? Dirty { get; set; }
public void LoadServerList(
AvaloniaList<IListEntryModelView> servers,
AvaloniaList<Exception> exceptions);
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using Avalonia.Collections;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
@@ -7,27 +8,14 @@ namespace Nebula.Launcher.ServerListProviders;
public sealed class TestServerList : IServerListProvider
{
public bool IsLoaded => true;
public Action? OnLoaded { get; set; }
public Action? OnDisposed { get; set; }
public IEnumerable<IListEntryModelView> GetServers()
{
return [new ServerEntryModelView(),new ServerEntryModelView()];
}
public IEnumerable<Exception> GetErrors()
{
return [new Exception("On no!")];
}
public void LoadServerList()
public void LoadServerList(
AvaloniaList<IListEntryModelView> servers,
AvaloniaList<Exception> exceptions)
{
}
public void Dispose()
{
OnDisposed?.Invoke();
//servers.Add(new ServerEntryViewModel());
//servers.Add(new ServerEntryViewModel());
exceptions.Add(new Exception("Oh no!"));
}
}

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Threading;
@@ -15,6 +16,8 @@ using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
using Nebula.SharedModels;
namespace Nebula.Launcher.Services;
@@ -32,7 +35,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){
@@ -90,10 +93,17 @@ public sealed partial class DecompilerService
private async Task Download(){
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
loading.LoadingName = "Download ILSpy";
loading.CreateLoadingContext().SetJobsCount(1);
var context = loading.CreateLoadingContext();
PopupMessageService.Popup(loading);
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);
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.ProcessHelper;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared;
using Nebula.Shared.Services;
namespace Nebula.Launcher.Services;
[ServiceRegister]
public sealed class InstanceRunningContainer(PopupMessageService popupMessageService, DebugService debugService)
public sealed class InstanceRunningContainer(
PopupMessageService popupMessageService,
DebugService debugService
)
{
private readonly InstanceKeyPool _keyPool = new();
private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new();

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

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

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

@@ -2,7 +2,9 @@ using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using JetBrains.Annotations;
using Microsoft.Extensions.DependencyInjection;
@@ -14,6 +16,7 @@ using Nebula.Launcher.Views.Pages;
using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper;
namespace Nebula.Launcher.ViewModels.Pages;
@@ -24,16 +27,17 @@ public partial class ServerOverviewModel : ViewModelBase
{
[ObservableProperty] private string _searchText = string.Empty;
[ObservableProperty] private bool _isFilterVisible;
public readonly ServerFilter CurrentFilter = new();
[GenerateProperty] private IServiceProvider ServiceProvider { get; }
[GenerateProperty] private ConfigurationService ConfigurationService { get; }
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; }
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
[ObservableProperty] private ServerListTabTemplate _selectedItem;
[GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; }
[GenerateProperty, DesignConstruct] public ServerListViewModel CurrentServerList { get; }
public ServerFilter CurrentFilter { get; } = new();
public ObservableCollection<ServerListTabTemplate> Items { get; private set; }
//Design think
protected override void InitialiseInDesignMode()
@@ -48,9 +52,18 @@ public partial class ServerOverviewModel : ViewModelBase
//real think
protected override void Initialise()
{
FavoriteServerListProvider.OnRefreshRequired += OnFavoriteRefreshRequired;
ConfigurationService.SubscribeVarChanged(LauncherConVar.Hub, OnHubListChanged, true);
}
private void OnFavoriteRefreshRequired()
{
if(CurrentServerList.Provider is FavoriteServerListProvider favoriteServerListProvider)
{
RefreshProvider();
}
}
private void OnHubListChanged(ServerHubRecord[]? value)
{
var tempItems = new List<ServerListTabTemplate>();
@@ -73,13 +86,9 @@ public partial class ServerOverviewModel : ViewModelBase
ApplyFilter();
}
public void ApplyFilter()
private void ApplyFilter()
{
foreach (var entry in ServerViewContainer.Items)
{
if(entry is IFilterConsumer filterConsumer)
filterConsumer.ProcessFilter(CurrentFilter);
}
ServerViewContainer.ApplyFilter(CurrentFilter);
}
public void OnFilterChanged(FilterBoxChangedEventArgs args)
@@ -99,16 +108,21 @@ public partial class ServerOverviewModel : ViewModelBase
public void UpdateRequired()
{
ServerViewContainer.Clear();
RefreshProvider();
}
private void RefreshProvider()
{
CurrentServerList.ClearProvider();
CurrentServerList.RefreshFromProvider();
CurrentServerList.ApplyFilter(CurrentFilter);
}
partial void OnSelectedItemChanged(ServerListTabTemplate value)
{
CurrentServerList.Provider = value.ServerListProvider;
CurrentServerList.ClearProvider();
CurrentServerList.SetProvider(value.ServerListProvider);
ApplyFilter();
}
}
[ServiceRegister]
@@ -119,6 +133,7 @@ public sealed class ServerViewContainer
private readonly Dictionary<string, string> _customNames = [];
private readonly Dictionary<string, WeakReference<IListEntryModelView>> _entries = new();
private ServerFilter? _currentFilter;
public ICollection<IListEntryModelView> Items =>
_entries.Values
@@ -143,13 +158,15 @@ public sealed class ServerViewContainer
{
foreach (var (_, weakRef) in _entries)
{
if (weakRef.TryGetTarget(out var value))
value.Dispose();
if (weakRef.TryGetTarget(out var value) && value is IDisposable disposable)
disposable.Dispose();
}
_entries.Clear();
}
public IListEntryModelView Get(string url, ServerStatus? serverStatus = null) => Get(url.ToRobustUrl(), serverStatus);
public IListEntryModelView Get(RobustUrl url, ServerStatus? serverStatus = null)
{
var key = url.ToString();
@@ -157,38 +174,63 @@ public sealed class ServerViewContainer
lock (_entries)
{
_customNames.TryGetValue(key, out var customName);
if (_entries.TryGetValue(key, out var weakEntry)
&& weakEntry.TryGetTarget(out entry))
{
return entry;
}
if (serverStatus is not null)
{
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;
}
entry = Create(url, serverStatus);
_entries[key] = new WeakReference<IListEntryModelView>(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)
{
@@ -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 bool IsFavorite { get; set; }

View File

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

View File

@@ -9,7 +9,7 @@ namespace Nebula.Launcher.ViewModels.Popup;
[ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))]
public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
{
private ServerEntryModelView _entry;
private ServerEntryViewModel _entryView;
[GenerateProperty] public override PopupMessageService PopupMessageService { 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;
}
public void Proceed()
{
_entry.RunInstanceIgnoreAuth();
_entryView.RunInstanceIgnoreAuth();
Dispose();
}

View File

@@ -26,7 +26,7 @@ public sealed partial class ServerCompoundEntryViewModel :
private RobustUrl? _url;
private ServerFilter? _currentFilter;
public ServerEntryModelView? CurrentEntry
public ServerEntryViewModel? CurrentEntry
{
get;
set
@@ -74,7 +74,7 @@ public sealed partial class ServerCompoundEntryViewModel :
{
}
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryModelView? entry)
public ServerCompoundEntryViewModel LoadWithEntry(ServerEntryViewModel? entry)
{
CurrentEntry = entry;
return this;
@@ -101,7 +101,7 @@ public sealed partial class ServerCompoundEntryViewModel :
Message = "Loading server entry...";
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;
}
@@ -130,9 +130,4 @@ public sealed partial class ServerCompoundEntryViewModel :
if(CurrentEntry is IFilterConsumer filterConsumer)
filterConsumer.ProcessFilter(serverFilter);
}
public void Dispose()
{
CurrentEntry?.Dispose();
}
}

View File

@@ -1,281 +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();
InstanceRunningContainer.IsRunningChanged -= IsRunningChanged;
}
}
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.Collections.ObjectModel;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Models;
using Avalonia.Collections;
using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.Views;
@@ -11,135 +9,46 @@ using Nebula.Shared.ViewHelper;
namespace Nebula.Launcher.ViewModels;
[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();
}
}
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)
if (serverEntry is IDisposable disposable)
{
invoker.Dirty += OnDirty;
}
if(!_provider.IsLoaded)
RefreshFromProvider();
else
{
Clear();
PasteServersFromList();
disposable.Dispose();
}
}
ServerList.Clear();
ErrorList.Clear();
GC.Collect();
GC.WaitForPendingFinalizers();
}
private void OnProviderDisposed()
public void SetProvider(IServerListProvider provider)
{
Provider.OnLoaded -= RefreshRequired;
Provider.OnDisposed -= OnProviderDisposed;
if (Provider is IServerListDirtyInvoker invoker)
{
invoker.Dirty -= OnDirty;
}
Provider = provider;
_provider = null;
}
private ServerFilter? _currentFilter;
public void RefreshFromProvider()
{
if (IsLoading)
return;
OnPropertyChanged(nameof(ServerList));
OnPropertyChanged(nameof(ErrorList));
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();
}
private void Clear()
public void RefreshFromProvider()
{
ErrorList.Clear();
ServerList.Clear();
Provider?.LoadServerList(ServerList, ErrorList);
}
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()
{
SetProvider(new TestServerList());
}
protected override void Initialise()

View File

@@ -3,7 +3,7 @@
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="Nebula.Launcher.Views.ServerEntryView"
x:DataType="viewModels:ServerEntryModelView"
x:DataType="viewModels:ServerEntryViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:converters="clr-namespace:Nebula.Launcher.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -16,7 +16,7 @@
xmlns:services="clr-namespace:Nebula.Launcher.Services"
IsVisible="{Binding IsVisible}">
<Design.DataContext>
<viewModels:ServerEntryModelView />
<viewModels:ServerEntryViewModel />
</Design.DataContext>
<Border
@@ -213,7 +213,7 @@
BoxShadow="0 0 13 -1 #121212"
CornerRadius="10">
<Button
Command="{Binding $parent[views:ServerEntryView].((viewModels:ServerEntryModelView)DataContext).OnLinkGo}"
Command="{Binding $parent[views:ServerEntryView].((viewModels:ServerEntryViewModel)DataContext).OnLinkGo}"
CommandParameter="{Binding Url}"
Margin="3">
<StackPanel Orientation="Horizontal" Spacing="5">

View File

@@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:services="clr-namespace:Nebula.Launcher.Services"
xmlns:viewModels1="clr-namespace:Nebula.Launcher.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Nebula.Launcher.Views.ServerListView"
@@ -15,16 +14,10 @@
Margin="5,0,0,10"
Padding="0,0,10,0">
<StackPanel Margin="0,0,0,30">
<Label IsVisible="{Binding IsLoading}"
x:Name="LoadingLabel"
Margin="10" HorizontalAlignment="Center"
Content="{services:LocaledText 'server-list-loading'}"/>
<ItemsControl
ItemsSource="{Binding ErrorList}"
Margin="10,0,10,0" />
<ItemsControl
ItemsSource="{Binding ServerList}"
Padding="0" />
<ItemsControl ItemsSource="{Binding ErrorList}"
Margin="10,0,10,0" />
<ItemsControl ItemsSource="{Binding ServerList}"
Padding="0" />
</StackPanel>
</ScrollViewer>
</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>
<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

@@ -29,20 +29,37 @@ public class HarmonyService(ReflectionService reflectionService)
}
/// <summary>
/// Я помню пенис большой,Я помню пенис большой, Я помню пенис большой, я помню....
/// Я не понимаю суть античитов в сосаке.
/// Эту хуйню может обойти любой школьник!
/// Нет.. я не хочу вводить читы, просто мне нужно поменять некоторые штучки :)
/// </summary>
private void UnShittyWizard()
{
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;
}
static bool HidifyPrefix(ref Type? __result, string typeName)
{
if (typeName.Contains("Harmony"))
{
__result = null;
return false;
}
return true;
}
}
public class HarmonyInstance
{
public readonly Harmony Harmony;

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

@@ -8,14 +8,14 @@ public static class CurrentConVar
{
public static readonly ConVar<string[]> EngineManifestUrl =
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"
]);
public static readonly ConVar<string[]> EngineModuleManifestUrl =
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"
]);

View File

@@ -16,6 +16,7 @@
</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

@@ -81,7 +81,7 @@ public partial class ContentService
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
var response = await _http.GetAsync(info.DownloadUri, cancellationToken);
var response = await _http.GetAsync(info.DownloadUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
loadingHandler.SetLoadingMessage("Downloading zip content");

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(CancellationToken cancellationToken = default)
{
if (!Directory.Exists(FullPath))
await Download(cancellationToken);
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(CancellationToken cancellationToken = default)
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, cancellationToken);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken);
Directory.CreateDirectory(FullPath);
Directory.CreateDirectory(fullPath);
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
{
await using var zipArchive = new ZipArchive(stream);
await zipArchive.ExtractToDirectoryAsync(FullPath, true, cancellationToken);
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.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">
<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>
@@ -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_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_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_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>
@@ -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_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_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_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>
@@ -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_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_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_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>
@@ -83,6 +89,7 @@
<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>
<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_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>
@@ -98,23 +105,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#" />