5 Commits

Author SHA1 Message Date
63a4b39aa9 - tweak: change mirror 2026-04-08 21:58:51 +03:00
8bf665d1f1 - fix: code logic 2026-03-16 12:39:25 +03:00
7a77af2d80 - add: game service 2026-03-15 15:47:20 +03:00
830cb38d9f - tweak: less memory tweak part 2 2026-03-13 20:29:53 +03:00
8f66bf9f09 - tweak: less memory tweak 2026-03-12 21:25:38 +03:00
24 changed files with 699 additions and 602 deletions

View File

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

View File

@@ -36,7 +36,7 @@ public static class LauncherConVar
new AuthServerCredentials( new AuthServerCredentials(
"WizDen", "WizDen",
[ [
"https://harpy.durenko.tatar/auth-api/", "https://feline.durenko.tatar/auth-api/",
"https://auth.spacestation14.com/", "https://auth.spacestation14.com/",
"https://auth.fallback.spacestation14.com/", "https://auth.fallback.spacestation14.com/",
]), ]),
@@ -48,13 +48,13 @@ public static class LauncherConVar
]); ]);
public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [ public static readonly ConVar<ServerHubRecord[]> Hub = ConVarBuilder.Build<ServerHubRecord[]>("launcher.hub.v2", [
new ServerHubRecord("WizDen", "https://harpy.durenko.tatar/hub-api/api/servers"), new ServerHubRecord("WizDen", "https://feline.durenko.tatar/hub-api/api/servers"),
new ServerHubRecord("AltHub","https://hub.singularity14.co.uk/api/servers") new ServerHubRecord("AltHub","https://hub.singularity14.co.uk/api/servers")
]); ]);
public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", CultureInfo.CurrentCulture.Name); public static readonly ConVar<string> CurrentLang = ConVarBuilder.Build<string>("launcher.language", CultureInfo.CurrentCulture.Name);
public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url", public static readonly ConVar<string> ILSpyUrl = ConVarBuilder.Build<string>("decompiler.url",
"https://github.com/icsharpcode/ILSpy/releases/download/v10.0-preview2/ILSpy_selfcontained_10.0.0.8282-preview2-x64.zip"); "https://feline.durenko.tatar/ILSpy_selfcontained_10.0.0.8330-x64.zip");
public static readonly ConVar<string> ILSpyVersion = ConVarBuilder.Build<string>("dotnet.version", "10"); public static readonly ConVar<string> ILSpyVersion = ConVarBuilder.Build<string>("dotnet.version", "10");
} }

View File

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

View File

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

View File

@@ -1,83 +1,107 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Threading;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared; using Nebula.Shared;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.Utils;
namespace Nebula.Launcher.ServerListProviders; namespace Nebula.Launcher.ServerListProviders;
[ServiceRegister(null, false), ConstructGenerator] [ServiceRegister(null, false), ConstructGenerator]
public sealed partial class HubServerListProvider : IServerListProvider public sealed partial class HubServerListProvider : IServerListProvider, IDisposable
{ {
private CancellationTokenSource? _cts;
private readonly SemaphoreSlim _loadLock = new(1, 1);
[GenerateProperty] private RestService RestService { get; } [GenerateProperty] private RestService RestService { get; }
[GenerateProperty] private ServerViewContainer ServerViewContainer { get; } [GenerateProperty] private ServerViewContainer ServerViewContainer { get; }
public string HubUrl { get; set; }
public bool IsLoaded { get; private set; }
public Action? OnLoaded { get; set; }
public Action? OnDisposed { get; set; }
private CancellationTokenSource? _cts; private string _hubUrl;
private readonly List<IListEntryModelView> _servers = [];
private readonly List<Exception> _errors = [];
public HubServerListProvider With(string hubUrl) public HubServerListProvider With(string hubUrl)
{ {
HubUrl = hubUrl; _hubUrl = hubUrl;
return this; return this;
} }
public IEnumerable<IListEntryModelView> GetServers() public void LoadServerList(
AvaloniaList<IListEntryModelView> servers,
AvaloniaList<Exception> exceptions)
{ {
return _servers; servers.Add(new LoadingServerEntry());
Task.Run(() => LoadServerListAsync(servers, exceptions));
} }
public IEnumerable<Exception> GetErrors() private void SyncServers(List<IListEntryModelView> servers,
AvaloniaList<IListEntryModelView> collection)
{ {
return _errors; collection.Clear();
collection.AddRange(servers);
} }
public async void LoadServerList() private async Task LoadServerListAsync(
AvaloniaList<IListEntryModelView> servers,
AvaloniaList<Exception> exceptions)
{ {
if (_cts != null) CancellationTokenSource localCts;
{
await _cts.CancelAsync();
_cts = null;
}
_servers.Clear(); await _loadLock.WaitAsync();
_errors.Clear(); try
IsLoaded = false; {
_cts = new CancellationTokenSource(); _cts?.Cancel();
_cts?.Dispose();
_cts = new CancellationTokenSource();
localCts = _cts;
}
finally
{
_loadLock.Release();
}
try try
{ {
var servers = var serversRaw = await RestService.GetAsync<List<ServerHubInfo>>(
await RestService.GetAsync<List<ServerHubInfo>>(new Uri(HubUrl), _cts.Token); new Uri(_hubUrl),
localCts.Token
servers.Sort(new ServerComparer());
if(_cts.Token.IsCancellationRequested) return;
_servers.AddRange(
servers.Select(h=>
ServerViewContainer.Get(h.Address.ToRobustUrl(), h.StatusData)
)
); );
serversRaw.Sort(new ServerComparer());
localCts.Token.ThrowIfCancellationRequested();
Dispatcher.UIThread.Invoke(() =>
{
var serverList = new List<IListEntryModelView>();
foreach (var info in serversRaw)
{
serverList.Add(ServerViewContainer.Get(info.Address, info.StatusData));
}
SyncServers(serverList, servers);
});
}
catch (OperationCanceledException)
{
// Ignore cancel think
} }
catch (Exception e) catch (Exception e)
{ {
_errors.Add(new Exception($"Some error while loading server list from {HubUrl}. See inner exception", e)); Console.WriteLine(e);
_errors.Add(e); exceptions.Add(
new Exception(
$"Some error while loading server list from {_hubUrl}. See inner exception",
e
)
);
} }
IsLoaded = true;
OnLoaded?.Invoke();
} }
private void Initialise(){} private void Initialise(){}
@@ -85,7 +109,17 @@ public sealed partial class HubServerListProvider : IServerListProvider
public void Dispose() public void Dispose()
{ {
OnDisposed?.Invoke();
_cts?.Dispose(); _cts?.Dispose();
} }
}
public sealed class LoadingServerEntry : Label, IListEntryModelView
{
public LoadingServerEntry()
{
HorizontalAlignment = HorizontalAlignment.Center;
Content = LocalizationService.GetString("server-list-loading");
}
public void Dispose()
{}
} }

View File

@@ -1,23 +1,12 @@
using System; using System;
using System.Collections.Generic; using Avalonia.Collections;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Pages;
namespace Nebula.Launcher.ServerListProviders; namespace Nebula.Launcher.ServerListProviders;
public interface IServerListProvider : IDisposable public interface IServerListProvider
{ {
public bool IsLoaded { get; } public void LoadServerList(
public Action? OnLoaded { get; set; } AvaloniaList<IListEntryModelView> servers,
public Action? OnDisposed { get; set; } AvaloniaList<Exception> exceptions);
public IEnumerable<IListEntryModelView> GetServers();
public IEnumerable<Exception> GetErrors();
public void LoadServerList();
}
public interface IServerListDirtyInvoker
{
public Action? Dirty { get; set; }
} }

View File

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

View File

@@ -4,6 +4,7 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.IO.Pipelines;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
@@ -15,6 +16,7 @@ using Nebula.Shared.FileApis.Interfaces;
using Nebula.Shared.Models; using Nebula.Shared.Models;
using Nebula.Shared.Services; using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging; using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
using Nebula.SharedModels; using Nebula.SharedModels;
namespace Nebula.Launcher.Services; namespace Nebula.Launcher.Services;
@@ -91,10 +93,17 @@ public sealed partial class DecompilerService
private async Task Download(){ private async Task Download(){
using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>(); using var loading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
loading.LoadingName = "Download ILSpy"; loading.LoadingName = "Download ILSpy";
loading.CreateLoadingContext().SetJobsCount(1); var context = loading.CreateLoadingContext();
PopupMessageService.Popup(loading); PopupMessageService.Popup(loading);
using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl)); using var response = await _httpClient.GetAsync(ConfigurationService.GetConfigValue(LauncherConVar.ILSpyUrl));
using var zipArchive = new ZipArchive(await response.Content.ReadAsStreamAsync()); Console.WriteLine(response.StatusCode);
context.SetJobsCount(response.Content.Headers.ContentLength ?? 1000);
using var stream = await response.Content.ReadAsStreamAsync();
using var memoryStream = new MemoryStream();
stream.CopyTo(memoryStream, context);
using var zipArchive = new ZipArchive(memoryStream);
Directory.CreateDirectory(FullPath); Directory.CreateDirectory(FullPath);
zipArchive.ExtractToDirectory(FullPath); zipArchive.ExtractToDirectory(FullPath);
} }

View File

@@ -0,0 +1,162 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Nebula.Launcher.Models;
using Nebula.Launcher.ProcessHelper;
using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Shared;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
namespace Nebula.Launcher.Services;
[ServiceRegister]
public class GameRunnerService
{
private readonly PopupMessageService _popupMessageService;
private readonly ViewHelperService _viewHelperService;
private readonly GameRunnerPreparer _gameRunnerPreparer;
private readonly InstanceRunningContainer _instanceRunningContainer;
private readonly AccountInfoViewModel _accountInfoViewModel;
private readonly ServerViewContainer _container;
private readonly MainViewModel _mainViewModel;
private readonly FavoriteServerListProvider _favoriteServerListProvider;
private readonly RestService _restService;
private readonly CancellationService _cancellationService;
private readonly ILogger _logger;
private readonly Dictionary<InstanceKey, RobustUrl> _robustUrls = new();
private readonly Dictionary<RobustUrl, InstanceKey> _robustKeys = new();
public GameRunnerService(PopupMessageService popupMessageService,
DebugService debugService,
ViewHelperService viewHelperService,
GameRunnerPreparer gameRunnerPreparer,
InstanceRunningContainer instanceRunningContainer,
AccountInfoViewModel accountInfoViewModel,
ServerViewContainer container,
MainViewModel mainViewModel,
FavoriteServerListProvider favoriteServerListProvider,
RestService restService,
CancellationService cancellationService)
{
_popupMessageService = popupMessageService;
_viewHelperService = viewHelperService;
_gameRunnerPreparer = gameRunnerPreparer;
_instanceRunningContainer = instanceRunningContainer;
_accountInfoViewModel = accountInfoViewModel;
_container = container;
_mainViewModel = mainViewModel;
_favoriteServerListProvider = favoriteServerListProvider;
_restService = restService;
_cancellationService = cancellationService;
_logger = debugService.GetLogger("GameRunnerService");
_instanceRunningContainer.IsRunningChanged += IsRunningChanged;
}
private void IsRunningChanged(InstanceKey key, bool isRunning)
{
_logger.Debug($"IsRunningChanged {key}: {isRunning}");
if (!_robustUrls.TryGetValue(key, out var robustUrl)) return;
if (_container.Get(robustUrl) is IRunningSignalConsumer signalConsumer)
{
_logger.Debug($"IsRunningChanged conf {robustUrl}: {isRunning}");
signalConsumer.ProcessRunningSignal(isRunning);
}
if (!isRunning)
{
_robustKeys.Remove(robustUrl);
_robustUrls.Remove(key);
}
}
public void StopInstance(RobustUrl robustUrl)
{
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
{
_instanceRunningContainer.Stop(instanceKey);
}
}
public void ReadInstanceLog(RobustUrl robustUrl)
{
if (_robustKeys.TryGetValue(robustUrl, out var instanceKey))
{
_instanceRunningContainer.Popup(instanceKey);
}
}
public void OpenContentViewer(RobustUrl robustUrl)
{
_mainViewModel.RequirePage<ContentBrowserViewModel>().Go(robustUrl, ContentPath.Empty);
}
public void AddFavorite(RobustUrl robustUrl)
{
_favoriteServerListProvider.AddFavorite(robustUrl);
}
public void RemoveFavorite(RobustUrl robustUrl)
{
_favoriteServerListProvider.RemoveFavorite(robustUrl);
}
public void EditName(RobustUrl robustUrl, string? oldName)
{
var popup = _viewHelperService.GetViewModel<EditServerNameViewModel>();
popup.IpInput = robustUrl.ToString();
popup.NameInput = oldName ?? string.Empty;
_popupMessageService.Popup(popup);
}
public async Task<InstanceKey?> RunInstanceAsync(ServerEntryViewModel serverEntryViewModel, CancellationToken cancellationToken, bool ignoreLoginCredentials = false)
{
_logger.Log("Running instance..." + serverEntryViewModel.RealName);
if (!ignoreLoginCredentials && _accountInfoViewModel.Credentials.Value is null)
{
var warningContext = _viewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
.WithServerEntry(serverEntryViewModel);
_popupMessageService.Popup(warningContext);
return null;
}
try
{
using var viewModelLoading = _viewHelperService.GetViewModel<LoadingContextViewModel>();
viewModelLoading.LoadingName = "Loading instance...";
_popupMessageService.Popup(viewModelLoading);
var currProcessStartProvider =
await _gameRunnerPreparer.GetGameProcessStartInfoProvider(serverEntryViewModel.Address, viewModelLoading, cancellationToken);
_logger.Log("Preparing instance...");
var instanceKey = _instanceRunningContainer.RegisterInstance(currProcessStartProvider);
_robustUrls.Add(instanceKey, serverEntryViewModel.Address);
_robustKeys.Add(serverEntryViewModel.Address, instanceKey);
_instanceRunningContainer.Run(instanceKey);
_logger.Log($"Starting instance... {instanceKey.Id} " + serverEntryViewModel.RealName);
return instanceKey;
}
catch (Exception e)
{
var error = new Exception("Error while attempt run instance", e);
_logger.Error(error);
_popupMessageService.Popup(error);
return null;
}
}
public ServerEntryViewModel GetServerEntry(RobustUrl url, string customName, ServerStatus serverStatus)
{
return new ServerEntryViewModel(_restService, _cancellationService, this)
.WithData(url, customName, serverStatus);
}
}

View File

@@ -3,13 +3,17 @@ using System.Collections.Generic;
using Nebula.Launcher.Models; using Nebula.Launcher.Models;
using Nebula.Launcher.ProcessHelper; using Nebula.Launcher.ProcessHelper;
using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Shared; using Nebula.Shared;
using Nebula.Shared.Services; using Nebula.Shared.Services;
namespace Nebula.Launcher.Services; namespace Nebula.Launcher.Services;
[ServiceRegister] [ServiceRegister]
public sealed class InstanceRunningContainer(PopupMessageService popupMessageService, DebugService debugService) public sealed class InstanceRunningContainer(
PopupMessageService popupMessageService,
DebugService debugService
)
{ {
private readonly InstanceKeyPool _keyPool = new(); private readonly InstanceKeyPool _keyPool = new();
private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new(); private readonly Dictionary<InstanceKey, ProcessRunHandler> _processCache = new();

View File

@@ -0,0 +1,13 @@
using System;
namespace Nebula.Launcher.ViewModels;
public record struct InstanceKey(int Id):
IEquatable<int>,
IComparable<InstanceKey>
{
public static implicit operator InstanceKey(int id) => new InstanceKey(id);
public static implicit operator int(InstanceKey id) => id.Id;
public bool Equals(int other) => Id == other;
public int CompareTo(InstanceKey other) => Id.CompareTo(other.Id);
};

View File

@@ -0,0 +1,16 @@
namespace Nebula.Launcher.ViewModels;
public sealed class InstanceKeyPool
{
private int _nextId = 1;
public InstanceKey Take()
{
return new InstanceKey(_nextId++);
}
public void Free(InstanceKey id)
{
// TODO: make some free logic later
}
}

View File

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

View File

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

View File

@@ -9,7 +9,7 @@ namespace Nebula.Launcher.ViewModels.Popup;
[ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))] [ConstructGenerator, ViewModelRegister(typeof(IsLoginCredentialsNullPopupView))]
public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
{ {
private ServerEntryModelView _entry; private ServerEntryViewModel _entryView;
[GenerateProperty] public override PopupMessageService PopupMessageService { get; } [GenerateProperty] public override PopupMessageService PopupMessageService { get; }
[GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; } [GenerateProperty, DesignConstruct] private ViewHelperService ViewHelperService { get; }
@@ -22,15 +22,15 @@ public partial class IsLoginCredentialsNullPopupViewModel : PopupViewModelBase
{ {
} }
public IsLoginCredentialsNullPopupViewModel WithServerEntry(ServerEntryModelView entryModelView) public IsLoginCredentialsNullPopupViewModel WithServerEntry(ServerEntryViewModel entryViewModel)
{ {
_entry = entryModelView; _entryView = entryViewModel;
return this; return this;
} }
public void Proceed() public void Proceed()
{ {
_entry.RunInstanceIgnoreAuth(); _entryView.RunInstanceIgnoreAuth();
Dispose(); Dispose();
} }

View File

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

View File

@@ -1,281 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Models;
using Nebula.Launcher.ProcessHelper;
using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Services.Logging;
using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper;
namespace Nebula.Launcher.ViewModels;
[ViewModelRegister(typeof(ServerEntryView), false)]
[ConstructGenerator]
public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, IListEntryModelView, IFavoriteEntryModelView, IEntryNameHolder
{
[ObservableProperty] private string _description = "Fetching info...";
[ObservableProperty] private bool _expandInfo;
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _isVisible;
[ObservableProperty] private bool _runVisible = true;
[ObservableProperty] private string _realName;
public string? Name
{
get => RealName;
set => RealName = value ?? Status.Name;
}
private ILogger _logger;
private ServerInfo? _serverInfo;
private InstanceKey _instanceKey;
public RobustUrl Address { get; private set; }
[GenerateProperty] private AccountInfoViewModel AccountInfoViewModel { get; }
[GenerateProperty] private CancellationService CancellationService { get; } = default!;
[GenerateProperty] private DebugService DebugService { get; } = default!;
[GenerateProperty] private PopupMessageService PopupMessageService { get; } = default!;
[GenerateProperty] private ViewHelperService ViewHelperService { get; } = default!;
[GenerateProperty] private RestService RestService { get; } = default!;
[GenerateProperty] private MainViewModel MainViewModel { get; } = default!;
[GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; } = default!;
[GenerateProperty] private GameRunnerPreparer GameRunnerPreparer { get; } = default!;
[GenerateProperty] private InstanceRunningContainer InstanceRunningContainer { get; } = default!;
public ServerStatus Status { get; private set; } =
new(
"Fetching data...",
"Loading...", [],
"",
-1,
-1,
-1,
false,
DateTime.Now,
-1
);
public ObservableCollection<ServerLink> Links { get; } = new();
public ObservableCollection<string> Tags { get; } = [];
public ICommand OnLinkGo { get; } = new LinkGoCommand();
public async Task<ServerInfo?> GetServerInfo()
{
if (_serverInfo == null)
try
{
_serverInfo = await RestService.GetAsync<ServerInfo>(Address.InfoUri, CancellationService.Token);
}
catch (Exception e)
{
Description = e.Message;
_logger.Error(e);
}
return _serverInfo;
}
protected override void InitialiseInDesignMode()
{
IsVisible = true;
RealName = "TEST.TEST";
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
Status = new ServerStatus("Ameba",
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
["rp:hrp", "18+"],
"Antag", 15, 5, 1, false
, DateTime.Now, 100);
Address = "ss14://localhost";
}
protected override void Initialise()
{
_logger = DebugService.GetLogger(this);
InstanceRunningContainer.IsRunningChanged += IsRunningChanged;
}
private void IsRunningChanged(InstanceKey arg1, bool isRunning)
{
if(arg1.Equals(_instanceKey))
RunVisible = !isRunning;
}
public void ProcessFilter(ServerFilter? serverFilter)
{
if (serverFilter == null)
{
IsVisible = true;
return;
}
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
}
public void SetStatus(ServerStatus serverStatus)
{
Status = serverStatus;
Tags.Clear();
foreach (var tag in Status.Tags) Tags.Add(tag);
OnPropertyChanged(nameof(Status));
}
public ServerEntryModelView WithData(RobustUrl url, string? name, ServerStatus serverStatus)
{
Address = url;
SetStatus(serverStatus);
Name = name;
return this;
}
public void EditName()
{
var popup = ViewHelperService.GetViewModel<EditServerNameViewModel>();
popup.IpInput = Address.ToString();
popup.NameInput = Name ?? string.Empty;
PopupMessageService.Popup(popup);
}
public void OpenContentViewer()
{
MainViewModel.RequirePage<ContentBrowserViewModel>().Go(Address, ContentPath.Empty);
}
public void ToggleFavorites()
{
IsFavorite = !IsFavorite;
if(IsFavorite)
FavoriteServerListProvider.AddFavorite(this);
else
FavoriteServerListProvider.RemoveFavorite(this);
}
public void RunInstance()
{
Task.Run(async ()=> await RunInstanceAsync());
}
public void RunInstanceIgnoreAuth()
{
Task.Run(async ()=> await RunInstanceAsync(true));
}
private async Task RunInstanceAsync(bool ignoreLoginCredentials = false)
{
_logger.Log("Running instance..." + RealName);
if (!ignoreLoginCredentials && AccountInfoViewModel.Credentials.Value is null)
{
var warningContext = ViewHelperService.GetViewModel<IsLoginCredentialsNullPopupViewModel>()
.WithServerEntry(this);
PopupMessageService.Popup(warningContext);
return;
}
try
{
using var viewModelLoading = ViewHelperService.GetViewModel<LoadingContextViewModel>();
viewModelLoading.LoadingName = "Loading instance...";
PopupMessageService.Popup(viewModelLoading);
var currProcessStartProvider =
await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, viewModelLoading, CancellationService.Token);
_logger.Log("Preparing instance...");
_instanceKey = InstanceRunningContainer.RegisterInstance(currProcessStartProvider);
InstanceRunningContainer.Run(_instanceKey);
_logger.Log("Starting instance..." + RealName);
}
catch (Exception e)
{
var error = new Exception("Error while attempt run instance", e);
_logger.Error(error);
PopupMessageService.Popup(error);
}
}
public void StopInstance()
{
InstanceRunningContainer.Stop(_instanceKey);
}
public void ReadLog()
{
InstanceRunningContainer.Popup(_instanceKey);
}
public async void ExpandInfoRequired()
{
ExpandInfo = !ExpandInfo;
if (Design.IsDesignMode) return;
var info = await GetServerInfo();
if (info == null) return;
Description = info.Desc;
Links.Clear();
if (info.Links is null) return;
foreach (var link in info.Links) Links.Add(link);
}
public void Dispose()
{
_logger.Dispose();
InstanceRunningContainer.IsRunningChanged -= IsRunningChanged;
}
}
public sealed class InstanceKeyPool
{
private int _nextId = 1;
public InstanceKey Take()
{
return new InstanceKey(_nextId++);
}
public void Free(InstanceKey id)
{
// TODO: make some free logic later
}
}
public record struct InstanceKey(int Id):
IEquatable<int>,
IComparable<InstanceKey>
{
public static implicit operator InstanceKey(int id) => new InstanceKey(id);
public static implicit operator int(InstanceKey id) => id.Id;
public bool Equals(int other) => Id == other;
public int CompareTo(InstanceKey other) => Id.CompareTo(other.Id);
};
public sealed class LinkGoCommand : ICommand
{
public LinkGoCommand()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
if (parameter is not string str) return;
Helper.SafeOpenBrowser(str);
}
public event EventHandler? CanExecuteChanged;
}

View File

@@ -0,0 +1,209 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using Nebula.Launcher.Models;
using Nebula.Launcher.ServerListProviders;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewModels.Pages;
using Nebula.Launcher.ViewModels.Popup;
using Nebula.Launcher.Views;
using Nebula.Shared.Models;
using Nebula.Shared.Services;
using Nebula.Shared.Utils;
using Nebula.Shared.ViewHelper;
namespace Nebula.Launcher.ViewModels;
[ViewModelRegister(typeof(ServerEntryView), false)]
public sealed partial class ServerEntryViewModel(
RestService restService,
CancellationService cancellationService,
GameRunnerService gameRunnerService
) :
ViewModelBase,
IFilterConsumer,
IListEntryModelView,
IFavoriteEntryModelView,
IEntryNameHolder,
IRunningSignalConsumer
{
[ObservableProperty] private string _description = "Fetching info...";
[ObservableProperty] private bool _expandInfo;
[ObservableProperty] private bool _isFavorite;
[ObservableProperty] private bool _isVisible;
[ObservableProperty] private bool _runVisible = true;
[ObservableProperty] private string _realName = string.Empty;
public string? Name
{
get => RealName;
set => RealName = value ?? Status.Name;
}
private ServerInfo? _serverInfo;
public RobustUrl Address { get; private set; }
public ServerStatus Status { get; private set; } =
new(
"Fetching data...",
"Loading...", [],
"",
-1,
-1,
-1,
false,
DateTime.Now,
-1
);
public ObservableCollection<ServerLink> Links { get; } = new();
public ObservableCollection<string> Tags { get; } = [];
public ICommand OnLinkGo { get; } = new LinkGoCommand();
public async Task<ServerInfo?> GetServerInfo()
{
if (_serverInfo != null)
return _serverInfo;
try
{
_serverInfo = await restService.GetAsync<ServerInfo>(Address.InfoUri, cancellationService.Token);
}
catch (Exception e)
{
Description = e.Message;
}
return _serverInfo;
}
protected override void InitialiseInDesignMode()
{
IsVisible = true;
RealName = "TEST.TEST";
Description = "Server of meow girls! Nya~ \nNyaMeow\nOOOINK!!";
Links.Add(new ServerLink("Discord", "discord", "https://cinka.ru"));
Status = new ServerStatus("Ameba",
"Locala meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow meow ",
["rp:hrp", "18+"],
"Antag", 15, 5, 1, false
, DateTime.Now, 100);
Address = "ss14://localhost";
}
protected override void Initialise()
{
}
public void ProcessFilter(ServerFilter? serverFilter)
{
if (serverFilter == null)
{
IsVisible = true;
return;
}
IsVisible = serverFilter.IsMatch(Status.Name, Tags);
}
public void SetStatus(ServerStatus serverStatus)
{
Status = serverStatus;
Tags.Clear();
foreach (var tag in Status.Tags) Tags.Add(tag);
OnPropertyChanged(nameof(Status));
}
public ServerEntryViewModel WithData(RobustUrl url, string? name, ServerStatus serverStatus)
{
Address = url;
SetStatus(serverStatus);
Name = name;
return this;
}
public void OpenContentViewer()
{
gameRunnerService.OpenContentViewer(Address);
}
public void ToggleFavorites()
{
IsFavorite = !IsFavorite;
if(IsFavorite)
gameRunnerService.AddFavorite(Address);
else
gameRunnerService.RemoveFavorite(Address);
}
public void RunInstance()
{
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token));
}
public void RunInstanceIgnoreAuth()
{
Task.Run(async ()=> await gameRunnerService.RunInstanceAsync(this, cancellationService.Token, true));
}
public void StopInstance()
{
gameRunnerService.StopInstance(Address);
}
public void ReadLog()
{
gameRunnerService.ReadInstanceLog(Address);
}
public void EditName()
{
gameRunnerService.EditName(Address, Name);
}
public async void ExpandInfoRequired()
{
ExpandInfo = !ExpandInfo;
if (Design.IsDesignMode) return;
var info = await GetServerInfo();
if (info == null) return;
Description = info.Desc;
Links.Clear();
if (info.Links is null) return;
foreach (var link in info.Links) Links.Add(link);
}
public void ProcessRunningSignal(bool isRunning)
{
RunVisible = !isRunning;
}
}
public sealed class LinkGoCommand : ICommand
{
public LinkGoCommand()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
if (parameter is not string str) return;
Helper.SafeOpenBrowser(str);
}
public event EventHandler? CanExecuteChanged;
}

View File

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

View File

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

View File

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

View File

@@ -29,20 +29,37 @@ public class HarmonyService(ReflectionService reflectionService)
} }
/// <summary> /// <summary>
/// Я помню пенис большой,Я помню пенис большой, Я помню пенис большой, я помню.... /// Я не понимаю суть античитов в сосаке.
/// Эту хуйню может обойти любой школьник!
/// Нет.. я не хочу вводить читы, просто мне нужно поменять некоторые штучки :)
/// </summary> /// </summary>
private void UnShittyWizard() private void UnShittyWizard()
{ {
var method = reflectionService.GetType("Robust.Client.GameController").TypeInitializer; var method = reflectionService.GetType("Robust.Client.GameController").TypeInitializer;
_instance!.Harmony.Patch(method, new HarmonyMethod(Prefix)); _instance!.Harmony.Patch(method, new HarmonyMethod(IgnorePrefix));
var method2 = typeof(Type).Method(nameof(Type.GetType), new[] { typeof(string) });
_instance!.Harmony.Patch(method2, new HarmonyMethod(HidifyPrefix));
} }
static bool Prefix() static bool IgnorePrefix()
{ {
return false; return false;
} }
static bool HidifyPrefix(ref Type? __result, string typeName)
{
if (typeName.Contains("Harmony"))
{
__result = null;
return false;
}
return true;
}
} }
public class HarmonyInstance public class HarmonyInstance
{ {
public readonly Harmony Harmony; public readonly Harmony Harmony;

View File

@@ -8,14 +8,14 @@ public static class CurrentConVar
{ {
public static readonly ConVar<string[]> EngineManifestUrl = public static readonly ConVar<string[]> EngineManifestUrl =
ConVarBuilder.Build<string[]>("engine.manifestUrl", [ ConVarBuilder.Build<string[]>("engine.manifestUrl", [
"https://harpy.durenko.tatar/manifests/manifest", "https://feline.durenko.tatar/engine-cdn/manifest.json",
"https://robust-builds.fallback.cdn.spacestation14.com/manifest.json" "https://robust-builds.fallback.cdn.spacestation14.com/manifest.json"
]); ]);
public static readonly ConVar<string[]> EngineModuleManifestUrl = public static readonly ConVar<string[]> EngineModuleManifestUrl =
ConVarBuilder.Build<string[]>("engine.moduleManifestUrl", ConVarBuilder.Build<string[]>("engine.moduleManifestUrl",
[ [
"https://harpy.durenko.tatar/manifests/modules", "https://feline.durenko.tatar/engine-cdn/modules.json",
"https://robust-builds.fallback.cdn.spacestation14.com/modules.json" "https://robust-builds.fallback.cdn.spacestation14.com/modules.json"
]); ]);

View File

@@ -28,6 +28,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuncValueConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe91c13e7e24d7ba324e0e6eb12a24ea8c7761299d3c4703e55c86dd120835e61_003FFuncValueConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuncValueConverter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fe91c13e7e24d7ba324e0e6eb12a24ea8c7761299d3c4703e55c86dd120835e61_003FFuncValueConverter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFunc_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fab_003F4dac48f4_003FFunc_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFunc_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003Fab_003F4dac48f4_003FFunc_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AFuture_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb3575a2f41d7c2dbfaa36e866b8a361e11dd7223ff82bc574c1d5d4b7522f735_003FFuture_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AGC_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fb35fce9cd25198662cd7a8324941fd794f2d31c98c32a35a18b355bc79386_003FGC_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpBaseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5c9ea82983a677ae263ed0c49dd93a5e32866ad7ae97beea733f6df197e995_003FHttpBaseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpBaseStream_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5c9ea82983a677ae263ed0c49dd93a5e32866ad7ae97beea733f6df197e995_003FHttpBaseStream_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc439425da351c75ac7d966a1cc8324b51a9c471865af79d2f2f3fcb65e392_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1d7280b53be4f32b5e9b2587f54915348ec89107b99282d2748ac94b8c1_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpClient_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd1d7280b53be4f32b5e9b2587f54915348ec89107b99282d2748ac94b8c1_003FHttpClient_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -41,6 +42,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86529590f9604f327a3b1b19aec3ff2310f0654aa06bb8cef2e3d820ea3bfd_003FHttpRequestMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpRequestMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F86529590f9604f327a3b1b19aec3ff2310f0654aa06bb8cef2e3d820ea3bfd_003FHttpRequestMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AHttpResponseMessage_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F4cfeb8b377bc81e1fbb5f7d7a02492cb6ac23e88c8c9d7155944f0716f3d4b_003FHttpResponseMessage_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDispatcherImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F22d92db124764b1ab49745245c66f01b1e1a00_003F0f_003F01061787_003FIDispatcherImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDispatcherImpl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F22d92db124764b1ab49745245c66f01b1e1a00_003F0f_003F01061787_003FIDispatcherImpl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003F4a_003Ff21bf9b5_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIDisposable_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fa6b7f037ba7b44df80b8d3aa7e58eeb2e8e938_003F98_003Fd1b23281_003FIDisposable_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEnumerable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003Fbc_003F64378026_003FIEnumerable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIEnumerable_00601_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F24f5857a073841e189d805de9660178ef49910_003Fbc_003F64378026_003FIEnumerable_00601_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F22d92db124764b1ab49745245c66f01b1e1a00_003F_005F2c742_003FIGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AIGeometryContext_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F22d92db124764b1ab49745245c66f01b1e1a00_003F_005F2c742_003FIGeometryContext_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -63,6 +65,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableCollection_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e2c48e6b3ec8b39cf721287f93972c7f3df25d306753bcc539eaad73126c68_003FObservableCollection_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObservableObject_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F3e432edeee9469b7cfdb81d6e6bd278cf57afb9e54ab75649b8bb2f52cdde69_003FObservableObject_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AObsoleteAttribute_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fd6ed53c3c6ac5794ce2e51aa4bcfdb5734b7f78ccfeccd5ba93ac6a0da3b2_003FObsoleteAttribute_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanelContainerGenerator_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F66d34460cd4eaf18a9301eeb1e7ae63cfc928ce72cc7e064e398d2bcfb9628_003FPanelContainerGenerator_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003APanel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F9b699722324e3615b57977447b25bf953fccb2d6e912ae584f16b7e691ad9d3_003FPanel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F36fd1a9641998bb3afbf2091e26eafa6aaafabcb494bc746c0ba7471db513143_003FParallel_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AParallel_002EForEachAsync_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fc1d1ed6be2d5d4de542b4af5b36e82f6d1d1a389a35a4e4f9748d137d1c651_003FParallel_002EForEachAsync_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
@@ -86,6 +89,7 @@
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcfbd5689fdab68d1c02f6a9b3c5921abcc409b8743dcc958da77cc1cfcb8e_003FStyle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AStyle_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003Fcfbd5689fdab68d1c02f6a9b3c5921abcc409b8743dcc958da77cc1cfcb8e_003FStyle_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F43273dba3ac6a4e11aefe78fbbccf5d36f07542ca37ecebffb25c95d1a1c16b_003FTextBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ATextBox_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F43273dba3ac6a4e11aefe78fbbccf5d36f07542ca37ecebffb25c95d1a1c16b_003FTextBox_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cde391207de75962d7bacb899ca2bd3985c86911b152d185b58999a422bf0_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F5cde391207de75962d7bacb899ca2bd3985c86911b152d185b58999a422bf0_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AType_002ECoreCLR_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F76bfc310d2c1ee89b5a3bb7f318b5c55015f66ebdf298211386b17a57fc25b_003FType_002ECoreCLR_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUri_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FSourcesCache_003F6a1fb5a19c4883d19f63515be2d0cce5e0e9929bb30469a912a58ad2e1e6152_003FUri_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F76_003F1f1e9043_003FUserControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AUserControl_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F0ceaca09f3944680b668dee8e1e0370b100a00_003F76_003F1f1e9043_003FUserControl_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValuePrinter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F80d1676fb411442983574149e0b6aebc72e00_003F2f_003F26a40f58_003FValuePrinter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AValuePrinter_002Ecs_002Fl_003A_002E_002E_003F_002E_002E_003FUsers_003FCinka_003FAppData_003FRoaming_003FJetBrains_003FRider2024_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F80d1676fb411442983574149e0b6aebc72e00_003F2f_003F26a40f58_003FValuePrinter_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>