Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a4b39aa9 | |||
| 8bf665d1f1 | |||
| 7a77af2d80 | |||
| 830cb38d9f | |||
| 8f66bf9f09 | |||
| 755fa51adc | |||
| a15d187550 |
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
1
.idea/.idea.Nebula/.idea/vcs.xml
generated
@@ -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>
|
||||
@@ -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");
|
||||
}
|
||||
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
6
Nebula.Launcher/Models/IRunningSignalConsumer.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Nebula.Launcher.Models;
|
||||
|
||||
public interface IRunningSignalConsumer
|
||||
{
|
||||
public void ProcessRunningSignal(bool isRunning);
|
||||
}
|
||||
@@ -1,7 +1,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(){}
|
||||
}
|
||||
@@ -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()
|
||||
{}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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!"));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
162
Nebula.Launcher/Services/GameRunnerService.cs
Normal file
@@ -0,0 +1,162 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Nebula.Launcher.Models;
|
||||
using Nebula.Launcher.ProcessHelper;
|
||||
using Nebula.Launcher.ServerListProviders;
|
||||
using Nebula.Launcher.ViewModels;
|
||||
using Nebula.Launcher.ViewModels.Pages;
|
||||
using Nebula.Launcher.ViewModels.Popup;
|
||||
using Nebula.Shared;
|
||||
using Nebula.Shared.Models;
|
||||
using Nebula.Shared.Services;
|
||||
using Nebula.Shared.Services.Logging;
|
||||
|
||||
namespace Nebula.Launcher.Services;
|
||||
|
||||
[ServiceRegister]
|
||||
public class GameRunnerService
|
||||
{
|
||||
private readonly PopupMessageService _popupMessageService;
|
||||
private readonly ViewHelperService _viewHelperService;
|
||||
private readonly GameRunnerPreparer _gameRunnerPreparer;
|
||||
private readonly InstanceRunningContainer _instanceRunningContainer;
|
||||
private readonly AccountInfoViewModel _accountInfoViewModel;
|
||||
private readonly ServerViewContainer _container;
|
||||
private readonly MainViewModel _mainViewModel;
|
||||
private readonly FavoriteServerListProvider _favoriteServerListProvider;
|
||||
private readonly RestService _restService;
|
||||
private readonly CancellationService _cancellationService;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
private readonly Dictionary<InstanceKey, RobustUrl> _robustUrls = new();
|
||||
private readonly Dictionary<RobustUrl, InstanceKey> _robustKeys = new();
|
||||
|
||||
public GameRunnerService(PopupMessageService popupMessageService,
|
||||
DebugService debugService,
|
||||
ViewHelperService viewHelperService,
|
||||
GameRunnerPreparer gameRunnerPreparer,
|
||||
InstanceRunningContainer instanceRunningContainer,
|
||||
AccountInfoViewModel accountInfoViewModel,
|
||||
ServerViewContainer container,
|
||||
MainViewModel mainViewModel,
|
||||
FavoriteServerListProvider favoriteServerListProvider,
|
||||
RestService restService,
|
||||
CancellationService cancellationService)
|
||||
{
|
||||
_popupMessageService = popupMessageService;
|
||||
_viewHelperService = viewHelperService;
|
||||
_gameRunnerPreparer = gameRunnerPreparer;
|
||||
_instanceRunningContainer = instanceRunningContainer;
|
||||
_accountInfoViewModel = accountInfoViewModel;
|
||||
_container = container;
|
||||
_mainViewModel = mainViewModel;
|
||||
_favoriteServerListProvider = favoriteServerListProvider;
|
||||
_restService = restService;
|
||||
_cancellationService = cancellationService;
|
||||
|
||||
_logger = debugService.GetLogger("GameRunnerService");
|
||||
_instanceRunningContainer.IsRunningChanged += IsRunningChanged;
|
||||
}
|
||||
|
||||
private void IsRunningChanged(InstanceKey key, bool isRunning)
|
||||
{
|
||||
_logger.Debug($"IsRunningChanged {key}: {isRunning}");
|
||||
if (!_robustUrls.TryGetValue(key, out var robustUrl)) return;
|
||||
|
||||
if (_container.Get(robustUrl) is IRunningSignalConsumer signalConsumer)
|
||||
{
|
||||
_logger.Debug($"IsRunningChanged conf {robustUrl}: {isRunning}");
|
||||
signalConsumer.ProcessRunningSignal(isRunning);
|
||||
}
|
||||
|
||||
if (!isRunning)
|
||||
{
|
||||
_robustKeys.Remove(robustUrl);
|
||||
_robustUrls.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
public void StopInstance(RobustUrl robustUrl)
|
||||
{
|
||||
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||
{
|
||||
_instanceRunningContainer.Stop(instanceKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void ReadInstanceLog(RobustUrl robustUrl)
|
||||
{
|
||||
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
|
||||
{
|
||||
_instanceRunningContainer.Popup(instanceKey);
|
||||
}
|
||||
}
|
||||
|
||||
public void OpenContentViewer(RobustUrl robustUrl)
|
||||
{
|
||||
_mainViewModel.RequirePage<ContentBrowserViewModel>().Go(robustUrl, ContentPath.Empty);
|
||||
}
|
||||
|
||||
public void AddFavorite(RobustUrl robustUrl)
|
||||
{
|
||||
_favoriteServerListProvider.AddFavorite(robustUrl);
|
||||
}
|
||||
|
||||
public void RemoveFavorite(RobustUrl robustUrl)
|
||||
{
|
||||
_favoriteServerListProvider.RemoveFavorite(robustUrl);
|
||||
}
|
||||
|
||||
public void EditName(RobustUrl robustUrl, string? oldName)
|
||||
{
|
||||
var popup = _viewHelperService.GetViewModel<EditServerNameViewModel>();
|
||||
popup.IpInput = robustUrl.ToString();
|
||||
popup.NameInput = oldName ?? string.Empty;
|
||||
_popupMessageService.Popup(popup);
|
||||
}
|
||||
|
||||
public async Task<InstanceKey?> RunInstanceAsync(ServerEntryViewModel serverEntryViewModel, CancellationToken cancellationToken, bool ignoreLoginCredentials = false)
|
||||
{
|
||||
_logger.Log("Running instance..." + serverEntryViewModel.RealName);
|
||||
if (!ignoreLoginCredentials && _accountInfoViewModel.Credentials.Value is null)
|
||||
{
|
||||
var warningContext = _viewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
|
||||
.WithServerEntry(serverEntryViewModel);
|
||||
|
||||
_popupMessageService.Popup(warningContext);
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var viewModelLoading = _viewHelperService.GetViewModel<LoadingContextViewModel>();
|
||||
viewModelLoading.LoadingName = "Loading instance...";
|
||||
|
||||
_popupMessageService.Popup(viewModelLoading);
|
||||
var currProcessStartProvider =
|
||||
await _gameRunnerPreparer.GetGameProcessStartInfoProvider(serverEntryViewModel.Address, viewModelLoading, cancellationToken);
|
||||
_logger.Log("Preparing instance...");
|
||||
var instanceKey = _instanceRunningContainer.RegisterInstance(currProcessStartProvider);
|
||||
_robustUrls.Add(instanceKey, serverEntryViewModel.Address);
|
||||
_robustKeys.Add(serverEntryViewModel.Address, instanceKey);
|
||||
_instanceRunningContainer.Run(instanceKey);
|
||||
_logger.Log($"Starting instance... {instanceKey.Id} " + serverEntryViewModel.RealName);
|
||||
return instanceKey;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
var error = new Exception("Error while attempt run instance", e);
|
||||
_logger.Error(error);
|
||||
_popupMessageService.Popup(error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public ServerEntryViewModel GetServerEntry(RobustUrl url, string customName, ServerStatus serverStatus)
|
||||
{
|
||||
return new ServerEntryViewModel(_restService, _cancellationService, this)
|
||||
.WithData(url, customName, serverStatus);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
13
Nebula.Launcher/ViewModels/InstanceKey.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using System;
|
||||
|
||||
namespace Nebula.Launcher.ViewModels;
|
||||
|
||||
public record struct InstanceKey(int Id):
|
||||
IEquatable<int>,
|
||||
IComparable<InstanceKey>
|
||||
{
|
||||
public static implicit operator InstanceKey(int id) => new InstanceKey(id);
|
||||
public static implicit operator int(InstanceKey id) => id.Id;
|
||||
public bool Equals(int other) => Id == other;
|
||||
public int CompareTo(InstanceKey other) => Id.CompareTo(other.Id);
|
||||
};
|
||||
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
16
Nebula.Launcher/ViewModels/InstanceKeyPool.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Nebula.Launcher.ViewModels;
|
||||
|
||||
public sealed class InstanceKeyPool
|
||||
{
|
||||
private int _nextId = 1;
|
||||
|
||||
public InstanceKey Take()
|
||||
{
|
||||
return new InstanceKey(_nextId++);
|
||||
}
|
||||
|
||||
public void Free(InstanceKey id)
|
||||
{
|
||||
// TODO: make some free logic later
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -55,4 +55,10 @@ public partial class AddFavoriteViewModel : PopupViewModelBase
|
||||
_logger.Error(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDispose()
|
||||
{
|
||||
base.OnDispose();
|
||||
_logger.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
209
Nebula.Launcher/ViewModels/ServerEntryViewModel.cs
Normal file
@@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Avalonia.Controls;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Nebula.Launcher.Models;
|
||||
using Nebula.Launcher.ServerListProviders;
|
||||
using Nebula.Launcher.Services;
|
||||
using Nebula.Launcher.ViewModels.Pages;
|
||||
using Nebula.Launcher.ViewModels.Popup;
|
||||
using Nebula.Launcher.Views;
|
||||
using Nebula.Shared.Models;
|
||||
using Nebula.Shared.Services;
|
||||
using Nebula.Shared.Utils;
|
||||
using Nebula.Shared.ViewHelper;
|
||||
|
||||
namespace Nebula.Launcher.ViewModels;
|
||||
|
||||
[ViewModelRegister(typeof(ServerEntryView), false)]
|
||||
public sealed partial class ServerEntryViewModel(
|
||||
RestService restService,
|
||||
CancellationService cancellationService,
|
||||
GameRunnerService gameRunnerService
|
||||
) :
|
||||
ViewModelBase,
|
||||
IFilterConsumer,
|
||||
IListEntryModelView,
|
||||
IFavoriteEntryModelView,
|
||||
IEntryNameHolder,
|
||||
IRunningSignalConsumer
|
||||
{
|
||||
[ObservableProperty] private string _description = "Fetching info...";
|
||||
[ObservableProperty] private bool _expandInfo;
|
||||
[ObservableProperty] private bool _isFavorite;
|
||||
[ObservableProperty] private bool _isVisible;
|
||||
[ObservableProperty] private bool _runVisible = true;
|
||||
[ObservableProperty] private string _realName = string.Empty;
|
||||
|
||||
public string? Name
|
||||
{
|
||||
get => RealName;
|
||||
set => RealName = value ?? Status.Name;
|
||||
}
|
||||
|
||||
private ServerInfo? _serverInfo;
|
||||
|
||||
public RobustUrl Address { get; private set; }
|
||||
|
||||
public ServerStatus Status { get; private set; } =
|
||||
new(
|
||||
"Fetching data...",
|
||||
"Loading...", [],
|
||||
"",
|
||||
-1,
|
||||
-1,
|
||||
-1,
|
||||
false,
|
||||
DateTime.Now,
|
||||
-1
|
||||
);
|
||||
|
||||
public ObservableCollection<ServerLink> Links { get; } = new();
|
||||
public ObservableCollection<string> Tags { get; } = [];
|
||||
public ICommand OnLinkGo { get; } = new LinkGoCommand();
|
||||
|
||||
public async Task<ServerInfo?> GetServerInfo()
|
||||
{
|
||||
if (_serverInfo != null)
|
||||
return _serverInfo;
|
||||
|
||||
try
|
||||
{
|
||||
_serverInfo = await restService.GetAsync<ServerInfo>(Address.InfoUri, cancellationService.Token);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Description = e.Message;
|
||||
}
|
||||
|
||||
return _serverInfo;
|
||||
}
|
||||
|
||||
protected override void InitialiseInDesignMode()
|
||||
{
|
||||
IsVisible = true;
|
||||
RealName = "TEST.TEST";
|
||||
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
|
||||
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
|
||||
Status = new ServerStatus("Ameba",
|
||||
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
|
||||
["rp:hrp", "18+"],
|
||||
"Antag", 15, 5, 1, false
|
||||
, DateTime.Now, 100);
|
||||
Address = "ss14://localhost";
|
||||
}
|
||||
|
||||
protected override void Initialise()
|
||||
{
|
||||
}
|
||||
|
||||
public void ProcessFilter(ServerFilter? serverFilter)
|
||||
{
|
||||
if (serverFilter == null)
|
||||
{
|
||||
IsVisible = true;
|
||||
return;
|
||||
}
|
||||
|
||||
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
|
||||
}
|
||||
|
||||
public void SetStatus(ServerStatus serverStatus)
|
||||
{
|
||||
Status = serverStatus;
|
||||
Tags.Clear();
|
||||
foreach (var tag in Status.Tags) Tags.Add(tag);
|
||||
OnPropertyChanged(nameof(Status));
|
||||
}
|
||||
|
||||
public ServerEntryViewModel WithData(RobustUrl url, string? name, ServerStatus serverStatus)
|
||||
{
|
||||
Address = url;
|
||||
SetStatus(serverStatus);
|
||||
Name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public void OpenContentViewer()
|
||||
{
|
||||
gameRunnerService.OpenContentViewer(Address);
|
||||
}
|
||||
|
||||
public void ToggleFavorites()
|
||||
{
|
||||
IsFavorite = !IsFavorite;
|
||||
if(IsFavorite)
|
||||
gameRunnerService.AddFavorite(Address);
|
||||
else
|
||||
gameRunnerService.RemoveFavorite(Address);
|
||||
}
|
||||
|
||||
public void RunInstance()
|
||||
{
|
||||
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token));
|
||||
}
|
||||
|
||||
public void RunInstanceIgnoreAuth()
|
||||
{
|
||||
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token, true));
|
||||
}
|
||||
|
||||
public void StopInstance()
|
||||
{
|
||||
gameRunnerService.StopInstance(Address);
|
||||
}
|
||||
|
||||
public void ReadLog()
|
||||
{
|
||||
gameRunnerService.ReadInstanceLog(Address);
|
||||
}
|
||||
|
||||
public void EditName()
|
||||
{
|
||||
gameRunnerService.EditName(Address, Name);
|
||||
}
|
||||
|
||||
public async void ExpandInfoRequired()
|
||||
{
|
||||
ExpandInfo = !ExpandInfo;
|
||||
if (Design.IsDesignMode) return;
|
||||
|
||||
var info = await GetServerInfo();
|
||||
if (info == null) return;
|
||||
|
||||
Description = info.Desc;
|
||||
|
||||
Links.Clear();
|
||||
if (info.Links is null) return;
|
||||
foreach (var link in info.Links) Links.Add(link);
|
||||
}
|
||||
|
||||
|
||||
public void ProcessRunningSignal(bool isRunning)
|
||||
{
|
||||
RunVisible = !isRunning;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LinkGoCommand : ICommand
|
||||
{
|
||||
public LinkGoCommand()
|
||||
{
|
||||
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public bool CanExecute(object? parameter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Execute(object? parameter)
|
||||
{
|
||||
if (parameter is not string str) return;
|
||||
Helper.SafeOpenBrowser(str);
|
||||
}
|
||||
|
||||
public event EventHandler? CanExecuteChanged;
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
]);
|
||||
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
124
Nebula.SharedModels/AppDataHelper.cs
Normal file
124
Nebula.SharedModels/AppDataHelper.cs
Normal file
@@ -0,0 +1,124 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Nebula.SharedModels;
|
||||
|
||||
public static class AppDataPath
|
||||
{
|
||||
public static string RootPath { get; private set; } = GetAppDataPath("Datum");
|
||||
|
||||
public static void SetTestRootPath(string rootPath)
|
||||
{
|
||||
Console.WriteLine($"REWRITE ROOT PATH TO {rootPath}");
|
||||
RootPath = rootPath;
|
||||
}
|
||||
|
||||
public static string GetAppDataPath(string appName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(appName))
|
||||
throw new ArgumentException("appName cannot be null or empty.", nameof(appName));
|
||||
|
||||
string basePath;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
basePath = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
basePath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||
"Library",
|
||||
"Application Support"
|
||||
);
|
||||
}
|
||||
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
basePath = Environment.GetEnvironmentVariable("XDG_CONFIG_HOME")
|
||||
?? Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Personal),
|
||||
".config"
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new PlatformNotSupportedException("Unsupported operating system.");
|
||||
}
|
||||
|
||||
return Path.Combine(basePath, appName);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UrlValidator
|
||||
{
|
||||
public static bool IsInDomainUrl(string url, string allowedDomain)
|
||||
{
|
||||
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
|
||||
return false;
|
||||
|
||||
if (uri.Scheme != Uri.UriSchemeHttps)
|
||||
return false;
|
||||
|
||||
var host = uri.Host.ToLowerInvariant();
|
||||
return host == allowedDomain || host.EndsWith("." + allowedDomain);
|
||||
}
|
||||
|
||||
public static void EnsureDomainValid(string url, string allowedDomain)
|
||||
{
|
||||
if(!IsInDomainUrl(url, allowedDomain))
|
||||
throw new InvalidOperationException($"URL {url} is not in domain {allowedDomain}.");
|
||||
}
|
||||
}
|
||||
|
||||
public static class DotnetUrlHelper
|
||||
{
|
||||
public static string GetExtension()
|
||||
{
|
||||
return OperatingSystem.IsWindows() ? ".exe" : string.Empty;
|
||||
}
|
||||
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
||||
{
|
||||
var rid = GetRuntimeIdentifier();
|
||||
|
||||
if (dotnetUrl.TryGetValue(rid, out var url)) return url;
|
||||
|
||||
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
||||
}
|
||||
|
||||
public static string GetRuntimeIdentifier()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
{
|
||||
return RuntimeInformation.ProcessArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "win-x64",
|
||||
Architecture.X86 => "win-x86",
|
||||
Architecture.Arm64 => "win-arm64",
|
||||
_ => throw new PlatformNotSupportedException($"Unsupported Windows architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||
};
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
|
||||
{
|
||||
return RuntimeInformation.ProcessArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "linux-x64",
|
||||
Architecture.X86 => "linux-x86",
|
||||
Architecture.Arm => "linux-arm",
|
||||
Architecture.Arm64 => "linux-arm64",
|
||||
_ => throw new PlatformNotSupportedException($"Unsupported Linux architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||
};
|
||||
}
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
|
||||
{
|
||||
return RuntimeInformation.ProcessArchitecture switch
|
||||
{
|
||||
Architecture.X64 => "osx-x64",
|
||||
Architecture.Arm64 => "osx-arm64",
|
||||
_ => throw new PlatformNotSupportedException($"Unsupported macOS architecture: {RuntimeInformation.ProcessArchitecture}")
|
||||
};
|
||||
}
|
||||
|
||||
throw new PlatformNotSupportedException($"Unsupported operating system: {RuntimeInformation.OSDescription}");
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,20 @@ namespace Nebula.SharedModels;
|
||||
|
||||
public record struct LauncherRuntimeInfo(
|
||||
[property: JsonPropertyName("version")] string RuntimeVersion,
|
||||
[property: JsonPropertyName("runtimes")] Dictionary<string, string> DotnetRuntimes);
|
||||
[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()}");
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -16,21 +16,13 @@ public static class DotnetStandalone
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
private static string GetExecutePath(LauncherRuntimeInfo runtimeInfo)
|
||||
{
|
||||
return Path.Join(MainWindow.RootPath,
|
||||
$"dotnet.{runtimeInfo.RuntimeVersion}",
|
||||
DotnetUrlHelper.GetRuntimeIdentifier(),
|
||||
$"dotnet{DotnetUrlHelper.GetExtension()}");
|
||||
}
|
||||
|
||||
public static async Task<Process?> Run(LauncherRuntimeInfo runtimeInfo, string dllPath)
|
||||
{
|
||||
await EnsureDotnet(runtimeInfo);
|
||||
|
||||
return Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = GetExecutePath(runtimeInfo),
|
||||
FileName = runtimeInfo.GetExecutePath(),
|
||||
Arguments = dllPath,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
@@ -42,7 +34,7 @@ public static class DotnetStandalone
|
||||
|
||||
private static async Task EnsureDotnet(LauncherRuntimeInfo runtimeInfo)
|
||||
{
|
||||
if (!Directory.Exists(GetExecutePath(runtimeInfo)))
|
||||
if (!File.Exists(runtimeInfo.GetExecutePath()))
|
||||
await Download(runtimeInfo);
|
||||
}
|
||||
|
||||
@@ -50,9 +42,11 @@ public static class DotnetStandalone
|
||||
{
|
||||
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||
|
||||
var fullPath = GetExecutePath(runtimeInfo);
|
||||
var fullPath = runtimeInfo.GetFullPath();
|
||||
|
||||
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
|
||||
|
||||
UrlValidator.EnsureDomainValid(url, "microsoft.com");
|
||||
|
||||
using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
@@ -80,58 +74,4 @@ public static class DotnetStandalone
|
||||
|
||||
LogStandalone.Log("Downloading dotnet complete.");
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using System.Net.Http;
|
||||
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,11 +15,10 @@ 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 HttpClient();
|
||||
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly FileApi _fileApi = new(Path.Join(RootPath, "app"));
|
||||
private string _logStr = "";
|
||||
|
||||
public MainWindow()
|
||||
@@ -26,20 +26,25 @@ public partial class MainWindow : Window
|
||||
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";
|
||||
};
|
||||
|
||||
LogStandalone.Log("Starting up");
|
||||
if (!Design.IsDesignMode)
|
||||
_ = Start();
|
||||
Task.Run(Start);
|
||||
else
|
||||
LogStandalone.Log("Debug information", 51);
|
||||
}
|
||||
@@ -58,7 +63,7 @@ public partial class MainWindow : Window
|
||||
foreach (var file in info.ToDelete)
|
||||
{
|
||||
LogStandalone.Log("Deleting " + file.Path);
|
||||
FileApi.Remove(file.Path);
|
||||
_fileApi.Remove(file.Path);
|
||||
}
|
||||
|
||||
var loadedManifest = info.FilesExist;
|
||||
@@ -75,7 +80,7 @@ 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));
|
||||
|
||||
@@ -85,7 +90,7 @@ public partial class MainWindow : Window
|
||||
|
||||
LogStandalone.Log("Download finished. Running launcher...");
|
||||
|
||||
await DotnetStandalone.Run(manifest.RuntimeInfo, 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);
|
||||
@@ -176,14 +181,13 @@ public partial class MainWindow : Window
|
||||
|
||||
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)
|
||||
|
||||
@@ -20,13 +20,21 @@ public static class Helper
|
||||
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;
|
||||
LogStandalone.Log($"Saving {fileName}", (int)(((float)totalLength / totalRead) * 100));
|
||||
|
||||
skipStep++;
|
||||
|
||||
if(skipStep < 50) continue;
|
||||
|
||||
skipStep = 0;
|
||||
|
||||
LogStandalone.Log($"Saving {fileName}", (int)((totalRead / (float)totalLength) * 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<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>
|
||||
@@ -41,6 +42,7 @@
|
||||
<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>
|
||||
@@ -63,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>
|
||||
@@ -86,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>
|
||||
@@ -101,23 +105,12 @@
|
||||
</AssemblyExplorer></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"><SessionState ContinuousTestingMode="0" IsActive="True" Name="RestServiceTests" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<s:String x:Key="/Default/Environment/UnitTesting/UnitTestSessionStore/Sessions/=44ee9fbc_002Dddaf_002D4efa_002Da7df_002D48c0ce9aec7e/@EntryIndexedValue"><SessionState ContinuousTestingMode="0" IsActive="True" Name="CryptographicTest" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session">
|
||||
<TestAncestor>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.RestServiceTests</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.PopupMessageServiceTests.PopupMessageServiceTest</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.PopupMessageServiceTests.DisposeTest</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.CreateFileApi_CreatesCorrectPath</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.EnsureTempDir_CreatesDirectoryAndReturnsApi</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.OpenZip_ReturnsZipFileApi_WhenValid</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.RemoveAllFiles_DeletesAllFilesAndDirectories</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.FileServiceTests.OpenZip_ThrowsException_WhenFileApiFails</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.EngineServiceTests.GetVersionInfo_ReturnsCorrectBuildInfo</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.EngineServiceTests.TryGetVersionInfo_ReturnsTrue</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests.WriteConVarTest</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests.WriteArrayConvarTest</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.ConfigurationServiceTests</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest.EncryptDecrypt</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest</TestId>
|
||||
<TestId>NUnit3x::735691F8-949C-4476-B9E4-5DF6FF8D3D0B::net9.0::Nebula.UnitTest.NebulaSharedTests.TarTest.Download</TestId>
|
||||
<TestId>NUnit3x::7430875B-ABAA-D4E0-C34F-0797C4762C66::net10.0::Nebula.UnitTest.NebulaSharedTests.CryptographicTest</TestId>
|
||||
</TestAncestor>
|
||||
</SessionState></s:String></wpf:ResourceDictionary>
|
||||
</SessionState></s:String>
|
||||
|
||||
|
||||
|
||||
</wpf:ResourceDictionary>
|
||||
Reference in New Issue
Block a user