Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 63a4b39aa9 | |||
| 8bf665d1f1 | |||
| 7a77af2d80 | |||
| 830cb38d9f | |||
| 8f66bf9f09 | |||
| 755fa51adc | |||
| a15d187550 | |||
| 6e6ebffb62 |
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);
|
||||
}
|
||||
@@ -62,5 +62,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj"/>
|
||||
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false"/>
|
||||
<ProjectReference Include="..\Nebula.Runner\Nebula.Runner.csproj"
|
||||
ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -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>
|
||||
|
||||
37
Nebula.Packager/CommandLineParser.cs
Normal file
37
Nebula.Packager/CommandLineParser.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
namespace Nebula.Packager;
|
||||
|
||||
public class CommandLineParser
|
||||
{
|
||||
public string Configuration { get; set; } = "Release";
|
||||
public string RootPath { get; set; } = string.Empty;
|
||||
|
||||
public static CommandLineParser Parse(IReadOnlyList<string> args)
|
||||
{
|
||||
using var enumerator = args.GetEnumerator();
|
||||
|
||||
var parsed = new CommandLineParser();
|
||||
|
||||
while (enumerator.MoveNext())
|
||||
{
|
||||
var arg = enumerator.Current;
|
||||
|
||||
if (arg == "--configuration")
|
||||
{
|
||||
if (!enumerator.MoveNext())
|
||||
throw new InvalidOperationException("Missing args for --configuration");
|
||||
|
||||
parsed.Configuration = enumerator.Current;
|
||||
}
|
||||
|
||||
if (arg == "--root-path")
|
||||
{
|
||||
if(!enumerator.MoveNext())
|
||||
throw new InvalidOperationException("Missing args for --root-path");
|
||||
|
||||
parsed.RootPath = enumerator.Current;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
@@ -4,4 +4,11 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
|
||||
<ProjectReference Include="..\Nebula.Shared\Nebula.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection"/>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,24 +2,36 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Nebula.Shared;
|
||||
using Nebula.SharedModels;
|
||||
|
||||
namespace Nebula.Packager;
|
||||
public static class Program
|
||||
{
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
Pack("","Release");
|
||||
var parsedArgs = CommandLineParser.Parse(args);
|
||||
|
||||
Pack(parsedArgs.RootPath, parsedArgs.Configuration);
|
||||
}
|
||||
|
||||
private static void Pack(string rootPath,string configuration)
|
||||
private static string ShowEmptyOrValue(string? value)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(value)) return "<empty>";
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void Pack(string rootPath, string configuration)
|
||||
{
|
||||
Console.WriteLine($"Packaging with arguments: RootPath {ShowEmptyOrValue(rootPath)} and Configuration {configuration}");
|
||||
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "dotnet",
|
||||
ArgumentList =
|
||||
{
|
||||
"publish",
|
||||
Path.Combine(rootPath,"Nebula.Launcher", "Nebula.Launcher.csproj"),
|
||||
Path.Combine(rootPath, "Nebula.Launcher", "Nebula.Launcher.csproj"),
|
||||
"-c", configuration,
|
||||
}
|
||||
};
|
||||
@@ -57,18 +69,14 @@ public static class Program
|
||||
entries.Add(new LauncherManifestEntry(hashStr, fileNameCut));
|
||||
Console.WriteLine($"Added {hashStr} file name {fileNameCut}");
|
||||
}
|
||||
|
||||
var manifestRuntimeInfo = new LauncherRuntimeInfo(
|
||||
CurrentConVar.DotnetVersion.DefaultValue!,
|
||||
CurrentConVar.DotnetUrl.DefaultValue!
|
||||
);
|
||||
|
||||
using var manifest = File.CreateText(Path.Combine(destinationDirectory, "manifest.json"));
|
||||
manifest.AutoFlush = true;
|
||||
manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries)));
|
||||
manifest.Write(JsonSerializer.Serialize(new LauncherManifest(entries, manifestRuntimeInfo)));
|
||||
}
|
||||
}
|
||||
|
||||
public record struct LauncherManifest(
|
||||
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries
|
||||
);
|
||||
|
||||
public record struct LauncherManifestEntry(
|
||||
[property: JsonPropertyName("hash")] string Hash,
|
||||
[property: JsonPropertyName("path")] string Path
|
||||
);
|
||||
@@ -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"
|
||||
]);
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
|
||||
<ProjectReference Include="..\Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
<ProjectReference Include="..\Robust.LoaderApi\Robust.LoaderApi\Robust.LoaderApi.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class ContentService
|
||||
|
||||
var loadingHandler = loadingFactory.CreateLoadingContext(new FileLoadingFormater());
|
||||
|
||||
var response = await _http.GetAsync(info.DownloadUri, cancellationToken);
|
||||
var response = await _http.GetAsync(info.DownloadUri, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
loadingHandler.SetLoadingMessage("Downloading zip content");
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
8
Nebula.SharedModels/LauncherManifest.cs
Normal file
8
Nebula.SharedModels/LauncherManifest.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nebula.SharedModels;
|
||||
|
||||
public record struct LauncherManifest(
|
||||
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries,
|
||||
[property: JsonPropertyName("runtime_info")] LauncherRuntimeInfo RuntimeInfo
|
||||
);
|
||||
8
Nebula.SharedModels/LauncherManifestEntry.cs
Normal file
8
Nebula.SharedModels/LauncherManifestEntry.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nebula.SharedModels;
|
||||
|
||||
public record struct LauncherManifestEntry(
|
||||
[property: JsonPropertyName("hash")] string Hash,
|
||||
[property: JsonPropertyName("path")] string Path
|
||||
);
|
||||
23
Nebula.SharedModels/LauncherRuntimeInfo.cs
Normal file
23
Nebula.SharedModels/LauncherRuntimeInfo.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nebula.SharedModels;
|
||||
|
||||
public record struct LauncherRuntimeInfo(
|
||||
[property: JsonPropertyName("version")] string RuntimeVersion,
|
||||
[property: JsonPropertyName("runtimes")] Dictionary<string, string> DotnetRuntimes);
|
||||
|
||||
public static class LauncherManifestEntryHelper
|
||||
{
|
||||
public static string GetFullPath(this LauncherRuntimeInfo runtimeInfo)
|
||||
{
|
||||
return Path.Join(AppDataPath.RootPath,
|
||||
$"dotnet.{runtimeInfo.RuntimeVersion}",
|
||||
DotnetUrlHelper.GetRuntimeIdentifier());
|
||||
}
|
||||
|
||||
public static string GetExecutePath(this LauncherRuntimeInfo runtimeInfo )
|
||||
{
|
||||
return Path.Join(GetFullPath(runtimeInfo),
|
||||
$"dotnet{DotnetUrlHelper.GetExtension()}");
|
||||
}
|
||||
}
|
||||
9
Nebula.SharedModels/Nebula.SharedModels.csproj
Normal file
9
Nebula.SharedModels/Nebula.SharedModels.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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)
|
||||
|
||||
@@ -7,7 +7,8 @@ using System.Net.Http;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Nebula.UpdateResolver.Configuration;
|
||||
using Nebula.SharedModels;
|
||||
using Nebula.UpdateResolver.Rest;
|
||||
|
||||
namespace Nebula.UpdateResolver;
|
||||
|
||||
@@ -15,19 +16,13 @@ public static class DotnetStandalone
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
private static readonly string FullPath =
|
||||
Path.Join(MainWindow.RootPath, $"dotnet.{ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetVersion)}",
|
||||
DotnetUrlHelper.GetRuntimeIdentifier());
|
||||
|
||||
private static readonly string ExecutePath = Path.Join(FullPath, "dotnet" + DotnetUrlHelper.GetExtension());
|
||||
|
||||
public static async Task<Process?> Run(string dllPath)
|
||||
public static async Task<Process?> Run(LauncherRuntimeInfo runtimeInfo, string dllPath)
|
||||
{
|
||||
await EnsureDotnet();
|
||||
await EnsureDotnet(runtimeInfo);
|
||||
|
||||
return Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = ExecutePath,
|
||||
FileName = runtimeInfo.GetExecutePath(),
|
||||
Arguments = dllPath,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
@@ -37,35 +32,40 @@ public static class DotnetStandalone
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task EnsureDotnet()
|
||||
private static async Task EnsureDotnet(LauncherRuntimeInfo runtimeInfo)
|
||||
{
|
||||
if (!Directory.Exists(FullPath))
|
||||
await Download();
|
||||
if (!File.Exists(runtimeInfo.GetExecutePath()))
|
||||
await Download(runtimeInfo);
|
||||
}
|
||||
|
||||
private static async Task Download()
|
||||
private static async Task Download(LauncherRuntimeInfo runtimeInfo)
|
||||
{
|
||||
LogStandalone.Log($"Downloading dotnet {DotnetUrlHelper.GetRuntimeIdentifier()}...");
|
||||
|
||||
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(
|
||||
ConfigurationStandalone.GetConfigValue(UpdateConVars.DotnetUrl)!
|
||||
);
|
||||
var fullPath = runtimeInfo.GetFullPath();
|
||||
|
||||
using var response = await HttpClient.GetAsync(url);
|
||||
var url = DotnetUrlHelper.GetCurrentPlatformDotnetUrl(runtimeInfo.DotnetRuntimes);
|
||||
|
||||
UrlValidator.EnsureDomainValid(url, "microsoft.com");
|
||||
|
||||
using var response = await HttpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var stream = await response.Content.ReadAsStreamAsync();
|
||||
await using var tempStream = new MemoryStream();
|
||||
stream.CopyTo(tempStream,"dotnet", response.Content.Headers.ContentLength ?? 0);
|
||||
await stream.DisposeAsync();
|
||||
|
||||
Directory.CreateDirectory(FullPath);
|
||||
Directory.CreateDirectory(fullPath);
|
||||
|
||||
if (url.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
using var zipArchive = new ZipArchive(stream);
|
||||
zipArchive.ExtractToDirectory(FullPath, true);
|
||||
await using var zipArchive = new ZipArchive(tempStream);
|
||||
await zipArchive.ExtractToDirectoryAsync(fullPath, true);
|
||||
}
|
||||
else if (url.EndsWith(".tar.gz", StringComparison.OrdinalIgnoreCase)
|
||||
|| url.EndsWith(".tgz", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
TarUtils.ExtractTarGz(stream, FullPath);
|
||||
TarUtils.ExtractTarGz(tempStream, fullPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -74,32 +74,4 @@ public static class DotnetStandalone
|
||||
|
||||
LogStandalone.Log("Downloading dotnet complete.");
|
||||
}
|
||||
}
|
||||
|
||||
public static class DotnetUrlHelper
|
||||
{
|
||||
public static string GetExtension()
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) return ".exe";
|
||||
return "";
|
||||
}
|
||||
|
||||
public static string GetCurrentPlatformDotnetUrl(Dictionary<string, string> dotnetUrl)
|
||||
{
|
||||
var rid = GetRuntimeIdentifier();
|
||||
|
||||
if (dotnetUrl.TryGetValue(rid, out var url)) return url;
|
||||
|
||||
throw new PlatformNotSupportedException($"No download URL available for the current platform: {rid}");
|
||||
}
|
||||
|
||||
public static string GetRuntimeIdentifier()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return Environment.Is64BitProcess ? "win-x64" : "win-x86";
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) return "linux-x64";
|
||||
|
||||
throw new PlatformNotSupportedException("Unsupported operating system");
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Nebula.UpdateResolver;
|
||||
|
||||
public record struct LauncherManifest(
|
||||
[property: JsonPropertyName("entries")] HashSet<LauncherManifestEntry> Entries
|
||||
);
|
||||
|
||||
public record struct LauncherManifestEntry(
|
||||
[property: JsonPropertyName("hash")] string Hash,
|
||||
[property: JsonPropertyName("path")] string Path
|
||||
);
|
||||
@@ -3,10 +3,11 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Threading;
|
||||
using Nebula.SharedModels;
|
||||
using Nebula.UpdateResolver.Configuration;
|
||||
using Nebula.UpdateResolver.Rest;
|
||||
|
||||
@@ -14,31 +15,36 @@ namespace Nebula.UpdateResolver;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public static readonly string RootPath = Path.Join(Environment.GetFolderPath(
|
||||
Environment.SpecialFolder.ApplicationData), "Datum");
|
||||
public static readonly string RootPath = AppDataPath.GetAppDataPath("Datum");
|
||||
|
||||
private readonly HttpClient _httpClient = new();
|
||||
private readonly FileApi _fileApi = new(Path.Join(RootPath, "app"));
|
||||
private string _logStr = "";
|
||||
|
||||
private readonly HttpClient _httpClient = new HttpClient();
|
||||
public readonly FileApi FileApi = new FileApi(Path.Join(RootPath,"app"));
|
||||
private string LogStr = "";
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
LogStandalone.OnLog += (message, percentage) =>
|
||||
{
|
||||
ProgressLabel.Content = message;
|
||||
if (percentage == 0)
|
||||
PercentLabel.Content = "";
|
||||
else
|
||||
PercentLabel.Content = percentage + "%";
|
||||
|
||||
var percentText = "";
|
||||
if (percentage != 0)
|
||||
percentText = $"{percentage}%";
|
||||
|
||||
Dispatcher.UIThread.Invoke(() =>
|
||||
{
|
||||
ProgressLabel.Content = message;
|
||||
PercentLabel.Content = percentText;
|
||||
});
|
||||
|
||||
var messageOut =
|
||||
$"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {PercentLabel.Content}";
|
||||
$"[{DateTime.Now.ToUniversalTime():yyyy-MM-dd HH:mm:ss}]: {message} {percentText}";
|
||||
Console.WriteLine(messageOut);
|
||||
LogStr += messageOut + "\n";
|
||||
_logStr += messageOut + "\n";
|
||||
};
|
||||
|
||||
LogStandalone.Log("Starting up");
|
||||
if (!Design.IsDesignMode)
|
||||
_ = Start();
|
||||
Task.Run(Start);
|
||||
else
|
||||
LogStandalone.Log("Debug information", 51);
|
||||
}
|
||||
@@ -47,17 +53,21 @@ public partial class MainWindow : Window
|
||||
{
|
||||
try
|
||||
{
|
||||
var info = await EnsureFiles();
|
||||
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
|
||||
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
|
||||
|
||||
var info = EnsureFiles(FilterEntries(manifest.Entries));
|
||||
|
||||
LogStandalone.Log("Downloading files...");
|
||||
|
||||
foreach (var file in info.ToDelete)
|
||||
{
|
||||
LogStandalone.Log("Deleting " + file.Path);
|
||||
FileApi.Remove(file.Path);
|
||||
_fileApi.Remove(file.Path);
|
||||
}
|
||||
|
||||
var loadedManifest = info.FilesExist;
|
||||
Save(loadedManifest);
|
||||
Save(loadedManifest, manifest.RuntimeInfo);
|
||||
|
||||
var count = info.ToDownload.Count;
|
||||
var resolved = 0;
|
||||
@@ -70,23 +80,23 @@ public partial class MainWindow : Window
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
FileApi.Save(file.Path, stream);
|
||||
_fileApi.Save(file.Path, stream);
|
||||
resolved++;
|
||||
LogStandalone.Log("Saving " + file.Path, (int)(resolved / (float)count * 100f));
|
||||
|
||||
loadedManifest.Add(file);
|
||||
Save(loadedManifest);
|
||||
Save(loadedManifest, manifest.RuntimeInfo);
|
||||
}
|
||||
|
||||
LogStandalone.Log("Download finished. Running launcher...");
|
||||
|
||||
await DotnetStandalone.Run(Path.Join(FileApi.RootPath, "Nebula.Launcher.dll"));
|
||||
await DotnetStandalone.Run(manifest.RuntimeInfo, Path.Join(_fileApi.RootPath, "Nebula.Launcher.dll"));
|
||||
}
|
||||
catch(HttpRequestException e){
|
||||
LogStandalone.LogError(e);
|
||||
LogStandalone.Log("Network connection error...");
|
||||
var logPath = Path.Join(RootPath,"updateResloverError.txt");
|
||||
await File.WriteAllTextAsync(logPath, LogStr);
|
||||
await File.WriteAllTextAsync(logPath, _logStr);
|
||||
Process.Start(new ProcessStartInfo(){
|
||||
FileName = "notepad",
|
||||
Arguments = logPath
|
||||
@@ -96,7 +106,7 @@ public partial class MainWindow : Window
|
||||
{
|
||||
LogStandalone.LogError(e);
|
||||
var logPath = Path.Join(RootPath,"updateResloverError.txt");
|
||||
await File.WriteAllTextAsync(logPath, LogStr);
|
||||
await File.WriteAllTextAsync(logPath, _logStr);
|
||||
Process.Start(new ProcessStartInfo(){
|
||||
FileName = "notepad",
|
||||
Arguments = logPath
|
||||
@@ -108,11 +118,9 @@ public partial class MainWindow : Window
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
private async Task<ManifestEnsureInfo> EnsureFiles()
|
||||
private ManifestEnsureInfo EnsureFiles(HashSet<LauncherManifestEntry> entries)
|
||||
{
|
||||
LogStandalone.Log("Ensuring launcher manifest...");
|
||||
var manifest = await RestStandalone.GetAsync<LauncherManifest>(
|
||||
new Uri(ConfigurationStandalone.GetConfigValue(UpdateConVars.UpdateCacheUrl)! + "/manifest.json"), CancellationToken.None);
|
||||
|
||||
var toDownload = new HashSet<LauncherManifestEntry>();
|
||||
var toDelete = new HashSet<LauncherManifestEntry>();
|
||||
@@ -124,13 +132,13 @@ public partial class MainWindow : Window
|
||||
LogStandalone.Log("Delta manifest loaded!");
|
||||
foreach (var file in currentManifest.Entries)
|
||||
{
|
||||
if (!manifest.Entries.Contains(file))
|
||||
if (!entries.Contains(file))
|
||||
toDelete.Add(EnsurePath(file));
|
||||
else
|
||||
filesExist.Add(EnsurePath(file));
|
||||
}
|
||||
|
||||
foreach (var file in manifest.Entries)
|
||||
foreach (var file in entries)
|
||||
{
|
||||
if(!currentManifest.Entries.Contains(file))
|
||||
toDownload.Add(EnsurePath(file));
|
||||
@@ -138,30 +146,48 @@ public partial class MainWindow : Window
|
||||
}
|
||||
else
|
||||
{
|
||||
toDownload = manifest.Entries;
|
||||
toDownload = entries;
|
||||
}
|
||||
|
||||
LogStandalone.Log("Saving launcher manifest...");
|
||||
|
||||
return new ManifestEnsureInfo(toDownload, toDelete, filesExist);
|
||||
}
|
||||
|
||||
|
||||
private void Save(HashSet<LauncherManifestEntry> entries)
|
||||
private HashSet<LauncherManifestEntry> FilterEntries(IEnumerable<LauncherManifestEntry> entries)
|
||||
{
|
||||
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries));
|
||||
var filtered = new HashSet<LauncherManifestEntry>();
|
||||
var runtimeIdentifier = DotnetUrlHelper.GetRuntimeIdentifier();
|
||||
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
var splited = entry.Path.Split("/");
|
||||
|
||||
if(splited.Length < 2 ||
|
||||
splited[0] != "runtimes" ||
|
||||
splited[1] == runtimeIdentifier)
|
||||
{
|
||||
filtered.Add(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
private void Save(HashSet<LauncherManifestEntry> entries, LauncherRuntimeInfo info)
|
||||
{
|
||||
ConfigurationStandalone.SetConfigValue(UpdateConVars.CurrentLauncherManifest, new LauncherManifest(entries, info));
|
||||
}
|
||||
|
||||
private LauncherManifestEntry EnsurePath(LauncherManifestEntry entry)
|
||||
{
|
||||
if(!PathValidator.IsSafePath(FileApi.RootPath, entry.Path))
|
||||
if(!PathValidator.IsSafePath(_fileApi.RootPath, entry.Path))
|
||||
throw new ArgumentException("Path contains invalid characters. Manifest hash: " + entry.Hash);
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class PathValidator
|
||||
{
|
||||
public static bool IsSafePath(string baseDirectory, string relativePath)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using Nebula.SharedModels;
|
||||
|
||||
namespace Nebula.UpdateResolver;
|
||||
|
||||
|
||||
@@ -26,4 +26,8 @@
|
||||
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Nebula.SharedModels\Nebula.SharedModels.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
@@ -9,31 +8,33 @@ namespace Nebula.UpdateResolver.Rest;
|
||||
public static class Helper
|
||||
{
|
||||
public static readonly JsonSerializerOptions JsonWebOptions = new(JsonSerializerDefaults.Web);
|
||||
public static void SafeOpenBrowser(string uri)
|
||||
{
|
||||
if (!Uri.TryCreate(uri, UriKind.Absolute, out var parsedUri))
|
||||
{
|
||||
Console.WriteLine("Unable to parse URI in server-provided link: {Link}", uri);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsedUri.Scheme is not ("http" or "https"))
|
||||
{
|
||||
Console.WriteLine("Refusing to open server-provided link {Link}, only http/https are allowed", parsedUri);
|
||||
return;
|
||||
}
|
||||
|
||||
OpenBrowser(parsedUri.ToString());
|
||||
}
|
||||
public static void OpenBrowser(string url)
|
||||
{
|
||||
Process.Start(new ProcessStartInfo(url) { UseShellExecute = true });
|
||||
}
|
||||
|
||||
public static async Task<T> AsJson<T>(this HttpContent content) where T : notnull
|
||||
{
|
||||
var str = await content.ReadAsStringAsync();
|
||||
return JsonSerializer.Deserialize<T>(str, JsonWebOptions) ??
|
||||
throw new JsonException("AsJson: did not expect null response");
|
||||
}
|
||||
|
||||
public static void CopyTo(this Stream input, Stream output, string fileName, long totalLength)
|
||||
{
|
||||
const int bufferSize = 81920;
|
||||
var buffer = new byte[bufferSize];
|
||||
|
||||
int skipStep = 0;
|
||||
long totalRead = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
|
||||
{
|
||||
output.Write(buffer, 0, bytesRead);
|
||||
totalRead += bytesRead;
|
||||
|
||||
skipStep++;
|
||||
|
||||
if(skipStep < 50) continue;
|
||||
|
||||
skipStep = 0;
|
||||
|
||||
LogStandalone.Log($"Saving {fileName}", (int)((totalRead / (float)totalLength) * 100));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using Nebula.SharedModels;
|
||||
using Nebula.UpdateResolver.Configuration;
|
||||
|
||||
namespace Nebula.UpdateResolver;
|
||||
@@ -9,13 +9,4 @@ public static class UpdateConVars
|
||||
ConVarBuilder.Build<string>("update.url","https://durenko.tatar/nebula/manifest/");
|
||||
public static readonly ConVar<LauncherManifest> CurrentLauncherManifest =
|
||||
ConVarBuilder.Build<LauncherManifest>("update.manifest");
|
||||
|
||||
public static readonly ConVar<Dictionary<string,string>> DotnetUrl = ConVarBuilder.Build<Dictionary<string,string>>("dotnet.url",
|
||||
new(){
|
||||
{"win-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x64.zip"},
|
||||
{"win-x86", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-win-x86.zip"},
|
||||
{"linux-x64", "https://builds.dotnet.microsoft.com/dotnet/Runtime/10.0.2/dotnet-runtime-10.0.2-linux-x64.tar.gz"}
|
||||
});
|
||||
|
||||
public static readonly ConVar<string> DotnetVersion = ConVarBuilder.Build<string>("dotnet.version", "10.0.2");
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArchiving_002EUtils_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F27e9f12ad1e4318b9b02849ec3e6a502fa3ee761c4f0522ba756ab30cde1c_003FArchiving_002EUtils_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AArray_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003F45_003F049a0c03_003FArray_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAssembly_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F501151723a8d43558c75acbd334f26322066fa4b1c82b1297291314bf92ff_003FAssembly_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAuthenticationHeaderValue_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F88b338246f59cffdb6f3dc3d8dbcfc169599dc71d6f44a8f2732983db7f73a_003FAuthenticationHeaderValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAvaloniaList_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3cc366334cc52275393f9def48cfcbccc8382175579fbd4f75b8c0e4bf33_003FAvaloniaList_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -27,8 +28,10 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuncValueConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe91c13e7e24d7ba324e0e6eb12a24ea8c7761299d3c4703e55c86dd120835e61_003FFuncValueConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFunc_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fab_003F4dac48f4_003FFunc_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGC_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb35fce9cd25198662cd7a8324941fd794f2d31c98c32a35a18b355bc79386_003FGC_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpBaseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5c9ea82983a677ae263ed0c49dd93a5e32866ad7ae97beea733f6df197e995_003FHttpBaseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1d7280b53be4f32b5e9b2587f54915348ec89107b99282d2748ac94b8c1_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpCompletionOption_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Ffcc079c54e9940c5ac59f0141dda9ad01b4928_003F28_003Fe60e6194_003FHttpCompletionOption_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpContent_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9657cc383c70851dc2bdcf91eff27f21196844abfe552fc9c3243ff36974cd_003FHttpContent_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpListener_002EWindows_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Ffb21cfde6c1ffa9b6be622d15d56f666ad94ada7dd7d81451418d807b98f2_003FHttpListener_002EWindows_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -39,7 +42,9 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86529590f9604f327a3b1b19aec3ff2310f0654aa06bb8cef2e3d820ea3bfd_003FHttpRequestMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDispatcherImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F22d92db124764b1ab49745245c66f01b1e1a00_003F0f_003F01061787_003FIDispatcherImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003F4a_003Ff21bf9b5_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEnumerable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003Fbc_003F64378026_003FIEnumerable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F22d92db124764b1ab49745245c66f01b1e1a00_003F_005F2c742_003FIGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AImage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2b95745d8f2ddf7b8ad6130e01c5b2782e253ff11247a9aeefcef47277b1ab_003FImage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIndex_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F2a1a813823579c69832f1304f97761e7be433bd6aa928f351d138050b56a38_003FIndex_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -60,6 +65,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanelContainerGenerator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F66d34460cd4eaf18a9301eeb1e7ae63cfc928ce72cc7e064e398d2bcfb9628_003FPanelContainerGenerator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -83,6 +89,7 @@
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcfbd5689fdab68d1c02f6a9b3c5921abcc409b8743dcc958da77cc1cfcb8e_003FStyle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F43273dba3ac6a4e11aefe78fbbccf5d36f07542ca37ecebffb25c95d1a1c16b_003FTextBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cde391207de75962d7bacb899ca2bd3985c86911b152d185b58999a422bf0_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F76bfc310d2c1ee89b5a3bb7f318b5c55015f66ebdf298211386b17a57fc25b_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F76_003F1f1e9043_003FUserControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValuePrinter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F80d1676fb411442983574149e0b6aebc72e00_003F2f_003F26a40f58_003FValuePrinter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
|
||||
@@ -98,23 +105,12 @@
|
||||
</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>
|
||||
@@ -2,6 +2,7 @@
|
||||
<Project Path="Nebula.Launcher\Nebula.Launcher.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.Packager\Nebula.Packager.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.Runner\Nebula.Runner.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.SharedModels\Nebula.SharedModels.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.Shared\Nebula.Shared.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.SourceGenerators\Nebula.SourceGenerators.csproj" Type="Classic C#" />
|
||||
<Project Path="Nebula.UnitTest\Nebula.UnitTest.csproj" Type="Classic C#" />
|
||||
|
||||
Reference in New Issue
Block a user