diff --git a/Nebula.Launcher/Assets/lang/en-US.ftl b/Nebula.Launcher/Assets/lang/en-US.ftl index f12fa75..4cedaf0 100644 --- a/Nebula.Launcher/Assets/lang/en-US.ftl +++ b/Nebula.Launcher/Assets/lang/en-US.ftl @@ -70,4 +70,5 @@ popup-login-credentials-warning-cancel = Cancel popup-login-credentials-warning-proceed = Proceed goto-path-home = Root folder -tab-favorite = Favorite \ No newline at end of file +tab-favorite = Favorite +server-list-loading = Loading server list.. Please wait \ No newline at end of file diff --git a/Nebula.Launcher/Assets/lang/ru-RU.ftl b/Nebula.Launcher/Assets/lang/ru-RU.ftl index 04baa10..6410348 100644 --- a/Nebula.Launcher/Assets/lang/ru-RU.ftl +++ b/Nebula.Launcher/Assets/lang/ru-RU.ftl @@ -70,4 +70,5 @@ popup-login-credentials-warning-cancel = Отмена popup-login-credentials-warning-proceed = Продолжить goto-path-home = Корн. папка -tab-favorite = Избранное \ No newline at end of file +tab-favorite = Избранное +server-list-loading = Загрузка списка серверов. Пожалуйста, подождите... \ No newline at end of file diff --git a/Nebula.Launcher/Controls/ServerListView.axaml b/Nebula.Launcher/Controls/ServerListView.axaml deleted file mode 100644 index ec2de2c..0000000 --- a/Nebula.Launcher/Controls/ServerListView.axaml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - diff --git a/Nebula.Launcher/Controls/ServerListView.axaml.cs b/Nebula.Launcher/Controls/ServerListView.axaml.cs deleted file mode 100644 index 28b1c54..0000000 --- a/Nebula.Launcher/Controls/ServerListView.axaml.cs +++ /dev/null @@ -1,118 +0,0 @@ -using Avalonia.Controls; -using Nebula.Launcher.Models; -using Nebula.Launcher.ServerListProviders; -using Nebula.Launcher.ViewModels; -using Nebula.Launcher.ViewModels.Pages; - -namespace Nebula.Launcher.Controls; - -public partial class ServerListView : UserControl -{ - private IServerListProvider _provider = default!; - private ServerFilter? _currentFilter; - - public bool IsLoading { get; private set; } - - public ServerListView() - { - InitializeComponent(); - } - - public static ServerListView TakeFrom(IServerListProvider provider) - { - var serverListView = new ServerListView(); - if (provider is IServerListDirtyInvoker invoker) - { - invoker.Dirty += serverListView.OnDirty; - } - serverListView._provider = provider; - serverListView.RefreshFromProvider(); - return serverListView; - } - - public void RefreshFromProvider() - { - if (IsLoading) - return; - - Clear(); - StartLoading(); - - _provider.LoadServerList(); - - if (_provider.IsLoaded) PasteServersFromList(); - else _provider.OnLoaded += RefreshRequired; - } - - public void RequireStatusUpdate() - { - foreach (var rawView in ServerList.Items) - { - if (rawView is ServerEntryModelView serverEntryModelView) - { - //serverEntryModelView.UpdateStatusIfNecessary(); - } - } - } - - public void ApplyFilter(ServerFilter? filter) - { - _currentFilter = filter; - - if(IsLoading) - return; - - foreach (var serverView in ServerList.Items) - { - if(serverView is IFilterConsumer filterConsumer) - filterConsumer.ProcessFilter(filter); - } - } - - private void OnDirty() - { - RefreshFromProvider(); - } - - private void Clear() - { - ErrorList.Items.Clear(); - ServerList.Items.Clear(); - } - - private void PasteServersFromList() - { - foreach (var serverEntry in _provider.GetServers()) - { - ServerList.Items.Add(serverEntry); - if(serverEntry is IFilterConsumer serverFilter) - serverFilter.ProcessFilter(_currentFilter); - } - - foreach (var error in _provider.GetErrors()) - { - ErrorList.Items.Add(error); - } - - EndLoading(); - } - - private void RefreshRequired() - { - PasteServersFromList(); - _provider.OnLoaded -= RefreshRequired; - } - - private void StartLoading() - { - Clear(); - IsLoading = true; - LoadingLabel.IsVisible = true; - } - - private void EndLoading() - { - IsLoading = false; - LoadingLabel.IsVisible = false; - } -} \ No newline at end of file diff --git a/Nebula.Launcher/Converters/TypeConverters.cs b/Nebula.Launcher/Converters/TypeConverters.cs index 05c65c4..ce33663 100644 --- a/Nebula.Launcher/Converters/TypeConverters.cs +++ b/Nebula.Launcher/Converters/TypeConverters.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Data.Converters; using Avalonia.Media; using Avalonia.Platform; +using Nebula.Launcher.Utils; using Nebula.Launcher.ViewModels.Pages; using Color = System.Drawing.Color; diff --git a/Nebula.Launcher/Models/ContentLogConsumer.cs b/Nebula.Launcher/Models/ContentLogConsumer.cs index 881ebf4..1d9d750 100644 --- a/Nebula.Launcher/Models/ContentLogConsumer.cs +++ b/Nebula.Launcher/Models/ContentLogConsumer.cs @@ -1,3 +1,5 @@ +using System; +using System.Collections.Generic; using Nebula.Launcher.ProcessHelper; using Nebula.Launcher.ViewModels.Popup; using Nebula.Shared.Services; @@ -6,27 +8,53 @@ namespace Nebula.Launcher.Models; public sealed class ContentLogConsumer : IProcessLogConsumer { - private readonly LogPopupModelView _currLog; - private readonly PopupMessageService _popupMessageService; + private readonly List _outMessages = []; + + private LogPopupModelView? _currentLogPopup; + + public int MaxMessages { get; set; } = 100; - public ContentLogConsumer(LogPopupModelView currLog, PopupMessageService popupMessageService) + public void Popup(PopupMessageService popupMessageService) { - _currLog = currLog; - _popupMessageService = popupMessageService; + if(_currentLogPopup is not null) + return; + + _currentLogPopup = new LogPopupModelView(popupMessageService); + _currentLogPopup.OnDisposing += OnLogPopupDisposing; + + foreach (var message in _outMessages.ToArray()) + { + _currentLogPopup.Append(message); + } + + popupMessageService.Popup(_currentLogPopup); + } + + private void OnLogPopupDisposing(PopupViewModelBase obj) + { + if(_currentLogPopup == null) + return; + + _currentLogPopup.OnDisposing -= OnLogPopupDisposing; + _currentLogPopup = null; } public void Out(string text) { - _currLog.Append(text); + _outMessages.Add(text); + if(_outMessages.Count >= MaxMessages) + _outMessages.RemoveAt(0); + + _currentLogPopup?.Append(text); } public void Error(string text) { - _currLog.Append(text); + Out(text); } public void Fatal(string text) { - _popupMessageService.Popup("Fatal error while stop instance:" + text); + throw new Exception("Error while running programm: " + text); } } \ No newline at end of file diff --git a/Nebula.Launcher/Nebula.Launcher.csproj b/Nebula.Launcher/Nebula.Launcher.csproj index 1aac8fe..5f6b17a 100644 --- a/Nebula.Launcher/Nebula.Launcher.csproj +++ b/Nebula.Launcher/Nebula.Launcher.csproj @@ -34,21 +34,6 @@ - - - ServerListTab.axaml - Code - - - AddFavoriteView.axaml - Code - - - ServerListView.axaml - Code - - - diff --git a/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs b/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs index d522859..59700eb 100644 --- a/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs +++ b/Nebula.Launcher/ProcessHelper/GameRunnerPreparer.cs @@ -13,7 +13,7 @@ namespace Nebula.Launcher.ProcessHelper; [ServiceRegister] public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService contentService, EngineService engineService) { - public async Task> GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default) + public async Task GetGameProcessStartInfoProvider(RobustUrl address, ILoadingHandlerFactory loadingHandlerFactory, CancellationToken cancellationToken = default) { var buildInfo = await contentService.GetBuildInfo(address, cancellationToken); @@ -39,11 +39,9 @@ public sealed class GameRunnerPreparer(IServiceProvider provider, ContentService await stream.DisposeAsync(); } - var gameInfo = + return provider.GetService()!.WithBuildInfo(buildInfo.BuildInfo.Auth.PublicKey, address); - var gameProcessRunHandler = new ProcessRunHandler(gameInfo); - return gameProcessRunHandler; } } \ No newline at end of file diff --git a/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs b/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs index 35eeab8..7a1eda3 100644 --- a/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs +++ b/Nebula.Launcher/ProcessHelper/ProcessRunHandler.cs @@ -6,29 +6,27 @@ using Nebula.Shared.Services.Logging; namespace Nebula.Launcher.ProcessHelper; -public class ProcessRunHandler : IProcessConsumerCollection, IDisposable where T: IProcessStartInfoProvider +public class ProcessRunHandler : IDisposable { private ProcessStartInfo? _processInfo; private Task? _processInfoTask; private Process? _process; - private ProcessLogConsumerCollection _consumerCollection = new(); + private readonly IProcessLogConsumer _logConsumer; private string _lastError = string.Empty; - private readonly T _currentProcessStartInfoProvider; + private readonly IProcessStartInfoProvider _currentProcessStartInfoProvider; - public T GetCurrentProcessStartInfo() => _currentProcessStartInfoProvider; + public IProcessStartInfoProvider GetCurrentProcessStartInfo() => _currentProcessStartInfoProvider; public bool IsRunning => _processInfo is not null; - public Action>? OnProcessExited; - - public void RegisterLogger(IProcessLogConsumer consumer) - { - _consumerCollection.RegisterLogger(consumer); - } + public Action? OnProcessExited; - public ProcessRunHandler(T processStartInfoProvider) + public bool Disposed { get; private set; } + + public ProcessRunHandler(IProcessStartInfoProvider processStartInfoProvider, IProcessLogConsumer logConsumer) { _currentProcessStartInfoProvider = processStartInfoProvider; + _logConsumer = logConsumer; _processInfoTask = _currentProcessStartInfoProvider.GetProcessStartInfo(); _processInfoTask.GetAwaiter().OnCompleted(OnInfoProvided); } @@ -42,8 +40,18 @@ public class ProcessRunHandler : IProcessConsumerCollection, IDisposable wher _processInfoTask = null; } + private void CheckIfDisposed() + { + if (!Disposed) return; + throw new ObjectDisposedException(nameof(ProcessRunHandler)); + } + public void Start() { + CheckIfDisposed(); + if(_process is not null) + throw new InvalidOperationException("Already running"); + if (_processInfoTask != null) { _processInfoTask.Wait(); @@ -66,7 +74,8 @@ public class ProcessRunHandler : IProcessConsumerCollection, IDisposable wher public void Stop() { - _process?.CloseMainWindow(); + CheckIfDisposed(); + Dispose(); } private void OnExited(object? sender, EventArgs e) @@ -79,12 +88,13 @@ public class ProcessRunHandler : IProcessConsumerCollection, IDisposable wher if (_process.ExitCode != 0) - _consumerCollection.Fatal(_lastError); + _logConsumer.Fatal(_lastError); _process.Dispose(); _process = null; OnProcessExited?.Invoke(this); + Dispose(); } private void OnErrorDataReceived(object sender, DataReceivedEventArgs e) @@ -92,7 +102,7 @@ public class ProcessRunHandler : IProcessConsumerCollection, IDisposable wher if (e.Data != null) { _lastError = e.Data; - _consumerCollection.Error(e.Data); + _logConsumer.Error(e.Data); } } @@ -100,14 +110,22 @@ public class ProcessRunHandler : IProcessConsumerCollection, IDisposable wher { if (e.Data != null) { - _consumerCollection.Out(e.Data); + _logConsumer.Out(e.Data); } } public void Dispose() { + if (_process is not null) + { + _process.CloseMainWindow(); + return; + } + + CheckIfDisposed(); + _processInfoTask?.Dispose(); - _process?.Dispose(); + Disposed = true; } } diff --git a/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs b/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs index 468e067..ad8efb7 100644 --- a/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs +++ b/Nebula.Launcher/ServerListProviders/FavoriteServerListProvider.cs @@ -28,6 +28,7 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS public bool IsLoaded { get; private set; } public Action? OnLoaded { get; set; } + public Action? OnDisposed { get; set; } public Action? Dirty { get; set; } public IEnumerable GetServers() { @@ -108,9 +109,14 @@ public sealed partial class FavoriteServerListProvider : IServerListProvider, IS } private void InitialiseInDesignMode(){} + + public void Dispose() + { + OnDisposed?.Invoke(); + } } -public class AddFavoriteButton: Border, IListEntryModelView{ +public sealed class AddFavoriteButton: Border, IListEntryModelView{ private Button _addFavoriteButton = new Button(); public AddFavoriteButton(IServiceProvider serviceProvider) @@ -128,4 +134,9 @@ public class AddFavoriteButton: Border, IListEntryModelView{ Child = _addFavoriteButton; } public bool IsFavorite { get; set; } + + public void Dispose() + { + + } } \ No newline at end of file diff --git a/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs b/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs index a26f075..7e16b8e 100644 --- a/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs +++ b/Nebula.Launcher/ServerListProviders/HubServerListProvider.cs @@ -2,8 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using Microsoft.Extensions.DependencyInjection; -using Nebula.Launcher.ViewModels; using Nebula.Launcher.ViewModels.Pages; using Nebula.Shared; using Nebula.Shared.Models; @@ -22,6 +20,7 @@ public sealed partial class HubServerListProvider : IServerListProvider public bool IsLoaded { get; private set; } public Action? OnLoaded { get; set; } + public Action? OnDisposed { get; set; } private CancellationTokenSource? _cts; private readonly List _servers = []; @@ -83,4 +82,10 @@ public sealed partial class HubServerListProvider : IServerListProvider private void Initialise(){} private void InitialiseInDesignMode(){} + + public void Dispose() + { + OnDisposed?.Invoke(); + _cts?.Dispose(); + } } \ No newline at end of file diff --git a/Nebula.Launcher/ServerListProviders/IServerListProvider.cs b/Nebula.Launcher/ServerListProviders/IServerListProvider.cs index d7d2528..6d87045 100644 --- a/Nebula.Launcher/ServerListProviders/IServerListProvider.cs +++ b/Nebula.Launcher/ServerListProviders/IServerListProvider.cs @@ -5,10 +5,11 @@ using Nebula.Launcher.ViewModels.Pages; namespace Nebula.Launcher.ServerListProviders; -public interface IServerListProvider +public interface IServerListProvider : IDisposable { public bool IsLoaded { get; } public Action? OnLoaded { get; set; } + public Action? OnDisposed { get; set; } public IEnumerable GetServers(); public IEnumerable GetErrors(); diff --git a/Nebula.Launcher/ServerListProviders/TestServerList.cs b/Nebula.Launcher/ServerListProviders/TestServerList.cs index dca0fe0..5a8e863 100644 --- a/Nebula.Launcher/ServerListProviders/TestServerList.cs +++ b/Nebula.Launcher/ServerListProviders/TestServerList.cs @@ -10,6 +10,8 @@ public sealed class TestServerList : IServerListProvider { public bool IsLoaded => true; public Action? OnLoaded { get; set; } + public Action? OnDisposed { get; set; } + public IEnumerable GetServers() { return [new ServerEntryModelView(),new ServerEntryModelView()]; @@ -24,4 +26,9 @@ public sealed class TestServerList : IServerListProvider { } + + public void Dispose() + { + OnDisposed?.Invoke(); + } } \ No newline at end of file diff --git a/Nebula.Launcher/Services/InstanceRunningContainer.cs b/Nebula.Launcher/Services/InstanceRunningContainer.cs new file mode 100644 index 0000000..88eb987 --- /dev/null +++ b/Nebula.Launcher/Services/InstanceRunningContainer.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using Nebula.Launcher.Models; +using Nebula.Launcher.ProcessHelper; +using Nebula.Launcher.ViewModels; +using Nebula.Shared; +using Nebula.Shared.Services; + +namespace Nebula.Launcher.Services; + +[ServiceRegister] +public sealed class InstanceRunningContainer(PopupMessageService popupMessageService, DebugService debugService) +{ + private readonly InstanceKeyPool _keyPool = new(); + private readonly Dictionary _processCache = new(); + private readonly Dictionary _contentLoggerCache = new(); + private readonly Dictionary _keyCache = new(); + + public Action? IsRunningChanged; + + public InstanceKey RegisterInstance(IProcessStartInfoProvider provider) + { + var id = _keyPool.Take(); + + var currentContentLogConsumer = new ContentLogConsumer(); + var logBridge = new DebugLoggerBridge(debugService.GetLogger("PROCESS_"+id.Id)); + var logContainer = new ProcessLogConsumerCollection(); + logContainer.RegisterLogger(currentContentLogConsumer); + logContainer.RegisterLogger(logBridge); + + var handler = new ProcessRunHandler(provider, logContainer); + handler.OnProcessExited += OnProcessExited; + + _processCache[id] = handler; + _contentLoggerCache[id] = currentContentLogConsumer; + _keyCache[handler] = id; + + return id; + } + + public void Popup(InstanceKey instanceKey) + { + if(!_contentLoggerCache.TryGetValue(instanceKey, out var handler)) + return; + + handler.Popup(popupMessageService); + } + + public void Run(InstanceKey instanceKey) + { + if(!_processCache.TryGetValue(instanceKey, out var process)) + return; + + process.Start(); + IsRunningChanged?.Invoke(instanceKey, true); + } + + public void Stop(InstanceKey instanceKey) + { + if(!_processCache.TryGetValue(instanceKey, out var process)) + return; + + process.Stop(); + } + + public bool IsRunning(InstanceKey instanceKey) + { + return _processCache.ContainsKey(instanceKey); + } + + private void RemoveProcess(ProcessRunHandler handler) + { + if(handler.Disposed) return; + + var key = _keyCache[handler]; + IsRunningChanged?.Invoke(key, false); + _processCache.Remove(key); + _keyCache.Remove(handler); + _contentLoggerCache.Remove(key); + } + + private void OnProcessExited(ProcessRunHandler obj) + { + obj.OnProcessExited -= OnProcessExited; + RemoveProcess(obj); + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Services/LocalizationService.cs b/Nebula.Launcher/Services/LocalizationService.cs index d3fabaa..d0bb3d5 100644 --- a/Nebula.Launcher/Services/LocalizationService.cs +++ b/Nebula.Launcher/Services/LocalizationService.cs @@ -11,7 +11,7 @@ using Nebula.Shared.Services; namespace Nebula.Launcher.Services; [ConstructGenerator, ServiceRegister] -public partial class LocalizationService +public sealed partial class LocalizationService { [GenerateProperty] private ConfigurationService ConfigurationService { get; } [GenerateProperty] private DebugService DebugService { get; } @@ -40,7 +40,6 @@ public partial class LocalizationService Console.WriteLine(error); } - _currentMessageContext = mc; } catch (Exception e) { DebugService.GetLogger("localisationService").Error(e); diff --git a/Nebula.Launcher/ColorUtils.cs b/Nebula.Launcher/Utils/ColorUtils.cs similarity index 91% rename from Nebula.Launcher/ColorUtils.cs rename to Nebula.Launcher/Utils/ColorUtils.cs index 8b1a930..64c5e4a 100644 --- a/Nebula.Launcher/ColorUtils.cs +++ b/Nebula.Launcher/Utils/ColorUtils.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using System.Text; using Avalonia.Media; -namespace Nebula.Launcher.ViewModels.Pages; +namespace Nebula.Launcher.Utils; public static class ColorUtils { diff --git a/Nebula.Launcher/Services/ExplorerService.cs b/Nebula.Launcher/Utils/ExplorerUtils.cs similarity index 89% rename from Nebula.Launcher/Services/ExplorerService.cs rename to Nebula.Launcher/Utils/ExplorerUtils.cs index 192c30a..b70ff99 100644 --- a/Nebula.Launcher/Services/ExplorerService.cs +++ b/Nebula.Launcher/Utils/ExplorerUtils.cs @@ -1,12 +1,11 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; -using Nebula.Shared; -namespace Nebula.Launcher.Services; +namespace Nebula.Launcher.Utils; -public static class ExplorerHelper +public static class ExplorerUtils { public static void OpenFolder(string path) { diff --git a/Nebula.Launcher/Utils/VCRuntimeDllChecker.cs b/Nebula.Launcher/Utils/VCRuntimeDllChecker.cs new file mode 100644 index 0000000..db4f9f8 --- /dev/null +++ b/Nebula.Launcher/Utils/VCRuntimeDllChecker.cs @@ -0,0 +1,29 @@ +using System; +using System.IO; + +namespace Nebula.Launcher.Utils; + +public static class VCRuntimeDllChecker +{ + public static bool AreVCRuntimeDllsPresent() + { + if (!OperatingSystem.IsWindows()) return true; + + string systemDir = Environment.SystemDirectory; + string[] requiredDlls = { + "msvcp140.dll", + "vcruntime140.dll" + }; + + foreach (var dll in requiredDlls) + { + var path = Path.Combine(systemDir, dll); + if (!File.Exists(path)) + { + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/MainViewModel.cs b/Nebula.Launcher/ViewModels/MainViewModel.cs index 336a003..e0844f8 100644 --- a/Nebula.Launcher/ViewModels/MainViewModel.cs +++ b/Nebula.Launcher/ViewModels/MainViewModel.cs @@ -8,6 +8,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Nebula.Launcher.Models; using Nebula.Launcher.Services; +using Nebula.Launcher.Utils; using Nebula.Launcher.ViewModels.Pages; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views; @@ -208,7 +209,7 @@ public partial class MainViewModel : ViewModelBase public void OpenRootPath() { - ExplorerHelper.OpenFolder(FileService.RootPath); + ExplorerUtils.OpenFolder(FileService.RootPath); } public void OpenLink() @@ -248,16 +249,18 @@ public partial class MainViewModel : ViewModelBase else _viewQueue.Remove(viewModelBase); } - - - [RelayCommand] - private void TriggerPane() + + public void TriggerPane() { IsPaneOpen = !IsPaneOpen; } - [RelayCommand] - public void ClosePopup() + public void CloseCurrentPopup() + { + CurrentPopup?.Dispose(); + } + + private void ClosePopup() { var viewModelBase = _viewQueue.FirstOrDefault(); if (viewModelBase is null) @@ -272,29 +275,4 @@ public partial class MainViewModel : ViewModelBase CurrentPopup = viewModelBase; } -} - -public static class VCRuntimeDllChecker -{ - public static bool AreVCRuntimeDllsPresent() - { - if (!OperatingSystem.IsWindows()) return true; - - string systemDir = Environment.SystemDirectory; - string[] requiredDlls = { - "msvcp140.dll", - "vcruntime140.dll" - }; - - foreach (var dll in requiredDlls) - { - var path = Path.Combine(systemDir, dll); - if (!File.Exists(path)) - { - return false; - } - } - - return true; - } } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs index 0b2dee9..71d25f2 100644 --- a/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ConfigurationViewModel.cs @@ -5,6 +5,7 @@ using System.IO; using System.IO.Compression; using System.Threading.Tasks; using Nebula.Launcher.Services; +using Nebula.Launcher.Utils; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views.Pages; using Nebula.Shared; @@ -69,7 +70,7 @@ public partial class ConfigurationViewModel : ViewModelBase public void OpenDataFolder() { - ExplorerHelper.OpenFolder(FileService.RootPath); + ExplorerUtils.OpenFolder(FileService.RootPath); } public void ExportLogs() @@ -79,7 +80,7 @@ public partial class ConfigurationViewModel : ViewModelBase Directory.CreateDirectory(path); ZipFile.CreateFromDirectory(logPath, Path.Join(path, DateTime.Now.ToString("yyyy-MM-dd") + ".zip")); - ExplorerHelper.OpenFolder(path); + ExplorerUtils.OpenFolder(path); } public void RemoveAllContent() diff --git a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs index 5d3db86..53e66f3 100644 --- a/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ContentBrowserViewModel.cs @@ -11,6 +11,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Nebula.Launcher.Models; using Nebula.Launcher.Services; +using Nebula.Launcher.Utils; using Nebula.Launcher.ViewModels.Popup; using Nebula.Launcher.Views; using Nebula.Launcher.Views.Pages; @@ -63,7 +64,7 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol ContentService.Unpack(serverEntry.FileApi, myTempDir, loading.CreateLoadingContext()); loading.Dispose(); }); - ExplorerHelper.OpenFolder(tmpDir); + ExplorerUtils.OpenFolder(tmpDir); } public void OnGoEnter() @@ -80,10 +81,7 @@ public sealed partial class ContentBrowserViewModel : ViewModelBase, IContentHol var cur = ServiceProvider.GetService()!; cur.Init(this, ServerText.ToRobustUrl()); var curContent = cur.Go(new ContentPath(SearchText), CancellationService.Token); - if(curContent == null) - throw new NullReferenceException($"{SearchText} not found in {ServerText}"); - - CurrentEntry = curContent; + CurrentEntry = curContent ?? throw new NullReferenceException($"{SearchText} not found in {ServerText}"); } catch (Exception e) { diff --git a/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs index 382ec5f..4ba302d 100644 --- a/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs +++ b/Nebula.Launcher/ViewModels/Pages/ServerOverviewModel.cs @@ -23,22 +23,16 @@ namespace Nebula.Launcher.ViewModels.Pages; public partial class ServerOverviewModel : ViewModelBase { [ObservableProperty] private string _searchText = string.Empty; - [ObservableProperty] private bool _isFilterVisible; - - [ObservableProperty] private ServerListView _currentServerList = new(); public readonly ServerFilter CurrentFilter = new(); - [GenerateProperty] private IServiceProvider ServiceProvider { get; } [GenerateProperty] private ConfigurationService ConfigurationService { get; } [GenerateProperty] private FavoriteServerListProvider FavoriteServerListProvider { get; } public ObservableCollection Items { get; private set; } [ObservableProperty] private ServerListTabTemplate _selectedItem; - [GenerateProperty, DesignConstruct] private ServerViewContainer ServerViewContainer { get; } - - private Dictionary _viewCache = []; + [GenerateProperty, DesignConstruct] public ServerListViewModel CurrentServerList { get; } //Design think @@ -106,26 +100,19 @@ public partial class ServerOverviewModel : ViewModelBase { ServerViewContainer.Clear(); CurrentServerList.RefreshFromProvider(); - CurrentServerList.RequireStatusUpdate(); CurrentServerList.ApplyFilter(CurrentFilter); } partial void OnSelectedItemChanged(ServerListTabTemplate value) { - if (!_viewCache.TryGetValue(value.TabName, out var view)) - { - view = ServerListView.TakeFrom(value.ServerListProvider); - _viewCache[value.TabName] = view; - } - - CurrentServerList = view; + CurrentServerList.Provider = value.ServerListProvider; ApplyFilter(); } } [ServiceRegister] -public class ServerViewContainer +public sealed class ServerViewContainer { private readonly ViewHelperService _viewHelperService; private readonly List _favorites = []; @@ -212,6 +199,10 @@ public class ServerViewContainer public void Clear() { + foreach (var (_, value) in _entries) + { + value.Dispose(); + } _entries.Clear(); } @@ -244,7 +235,7 @@ public class ServerViewContainer } } -public interface IListEntryModelView +public interface IListEntryModelView : IDisposable { } diff --git a/Nebula.Launcher/ViewModels/Popup/PopupViewModelBase.cs b/Nebula.Launcher/ViewModels/Popup/PopupViewModelBase.cs index cab3ed2..3593bbd 100644 --- a/Nebula.Launcher/ViewModels/Popup/PopupViewModelBase.cs +++ b/Nebula.Launcher/ViewModels/Popup/PopupViewModelBase.cs @@ -9,10 +9,12 @@ public abstract class PopupViewModelBase : ViewModelBase, IDisposable public abstract string Title { get; } public abstract bool IsClosable { get; } + public Action? OnDisposing; public void Dispose() { OnDispose(); + OnDisposing?.Invoke(this); PopupMessageService.ClosePopup(this); } diff --git a/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs index 7e32636..40b8ca5 100644 --- a/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerCompoundEntryModelView.cs @@ -2,8 +2,6 @@ using System; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; -using Avalonia.Media; -using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.Extensions.DependencyInjection; using Nebula.Launcher.Models; @@ -13,7 +11,6 @@ using Nebula.Launcher.Views; using Nebula.Shared.Models; using Nebula.Shared.Services; using Nebula.Shared.ViewHelper; -using BindingFlags = System.Reflection.BindingFlags; namespace Nebula.Launcher.ViewModels; @@ -23,7 +20,6 @@ public sealed partial class ServerCompoundEntryViewModel : ViewModelBase, IFavoriteEntryModelView, IFilterConsumer, IListEntryModelView, IEntryNameHolder { [ObservableProperty] private ServerEntryModelView? _currentEntry; - [ObservableProperty] private Control? _entryControl; [ObservableProperty] private string _message = "Loading server entry..."; [ObservableProperty] private bool _isFavorite; [ObservableProperty] private bool _loading = true; @@ -68,7 +64,7 @@ public sealed partial class ServerCompoundEntryViewModel : Message = "Loading server entry..."; var status = await RestService.GetAsync(_url.StatusUri, cancellationToken); - CurrentEntry = ServiceProvider.GetService()!.WithData(_url,name, status); + CurrentEntry = ServiceProvider.GetService()!.WithData(_url, name, status); CurrentEntry.IsFavorite = IsFavorite; CurrentEntry.Loading = false; CurrentEntry.ProcessFilter(_currentFilter); @@ -102,4 +98,9 @@ public sealed partial class ServerCompoundEntryViewModel : if(CurrentEntry is IFilterConsumer filterConsumer) filterConsumer.ProcessFilter(serverFilter); } + + public void Dispose() + { + CurrentEntry?.Dispose(); + } } \ No newline at end of file diff --git a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs index 6cd2ff4..c801993 100644 --- a/Nebula.Launcher/ViewModels/ServerEntryModelView.cs +++ b/Nebula.Launcher/ViewModels/ServerEntryModelView.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Collections.ObjectModel; -using System.Linq; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; @@ -23,7 +21,7 @@ namespace Nebula.Launcher.ViewModels; [ViewModelRegister(typeof(ServerEntryView), false)] [ConstructGenerator] -public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, IListEntryModelView, IFavoriteEntryModelView, IEntryNameHolder +public sealed partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, IListEntryModelView, IFavoriteEntryModelView, IEntryNameHolder { [ObservableProperty] private string _description = "Fetching info..."; [ObservableProperty] private bool _expandInfo; @@ -42,10 +40,7 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis private ILogger _logger; private ServerInfo? _serverInfo; - private ContentLogConsumer _currentContentLogConsumer; - private ProcessRunHandler? _currentInstance; - - public LogPopupModelView CurrLog; + private InstanceKey _instanceKey; public RobustUrl Address { get; private set; } [GenerateProperty] private AccountInfoViewModel AccountInfoViewModel { get; } [GenerateProperty] private CancellationService CancellationService { get; } = default!; @@ -56,6 +51,7 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis [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( @@ -101,14 +97,19 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis ["rp:hrp", "18+"], "Antag", 15, 5, 1, false , DateTime.Now, 100); - Address = "ss14://localhost".ToRobustUrl(); + Address = "ss14://localhost"; } protected override void Initialise() { _logger = DebugService.GetLogger(this); - CurrLog = ViewHelperService.GetViewModel(); - _currentContentLogConsumer = new(CurrLog, PopupMessageService); + InstanceRunningContainer.IsRunningChanged += IsRunningChanged; + } + + private void IsRunningChanged(InstanceKey arg1, bool isRunning) + { + if(arg1.Equals(_instanceKey)) + RunVisible = !isRunning; } public void ProcessFilter(ServerFilter? serverFilter) @@ -162,13 +163,11 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis public void RunInstance() { - CurrLog.Clear(); Task.Run(async ()=> await RunInstanceAsync()); } public void RunInstanceIgnoreAuth() { - CurrLog.Clear(); Task.Run(async ()=> await RunInstanceAsync(true)); } @@ -190,14 +189,11 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis viewModelLoading.LoadingName = "Loading instance..."; PopupMessageService.Popup(viewModelLoading); - _currentInstance = + var currProcessStartProvider = await GameRunnerPreparer.GetGameProcessStartInfoProvider(Address, viewModelLoading, CancellationService.Token); _logger.Log("Preparing instance..."); - _currentInstance.RegisterLogger(_currentContentLogConsumer); - _currentInstance.RegisterLogger(new DebugLoggerBridge(DebugService.GetLogger($"PROCESS_{Random.Shared.Next(65535)}"))); - _currentInstance.OnProcessExited += OnProcessExited; - RunVisible = false; - _currentInstance.Start(); + _instanceKey = InstanceRunningContainer.RegisterInstance(currProcessStartProvider); + InstanceRunningContainer.Run(_instanceKey); _logger.Log("Starting instance..." + RealName); } catch (Exception e) @@ -205,28 +201,17 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis var error = new Exception("Error while attempt run instance", e); _logger.Error(error); PopupMessageService.Popup(error); - RunVisible = true; } } - private void OnProcessExited(ProcessRunHandler obj) - { - RunVisible = true; - if (_currentInstance == null) return; - - _currentInstance.OnProcessExited -= OnProcessExited; - _currentInstance.Dispose(); - _currentInstance = null; - } - public void StopInstance() { - _currentInstance?.Stop(); + InstanceRunningContainer.Stop(_instanceKey); } public void ReadLog() { - PopupMessageService.Popup(CurrLog); + InstanceRunningContainer.Popup(_instanceKey); } public async void ExpandInfoRequired() @@ -243,9 +228,39 @@ public partial class ServerEntryModelView : ViewModelBase, IFilterConsumer, ILis if (info.Links is null) return; foreach (var link in info.Links) Links.Add(link); } + + public void Dispose() + { + _logger.Dispose(); + } } -public class LinkGoCommand : ICommand +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, + IComparable +{ + 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() { diff --git a/Nebula.Launcher/ViewModels/ServerListViewModel.cs b/Nebula.Launcher/ViewModels/ServerListViewModel.cs new file mode 100644 index 0000000..d9cd09b --- /dev/null +++ b/Nebula.Launcher/ViewModels/ServerListViewModel.cs @@ -0,0 +1,148 @@ +using System; +using System.Collections.ObjectModel; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using Nebula.Launcher.Models; +using Nebula.Launcher.ServerListProviders; +using Nebula.Launcher.ViewModels.Pages; +using Nebula.Launcher.Views; +using Nebula.Shared.ViewHelper; + +namespace Nebula.Launcher.ViewModels; + +[ViewModelRegister(typeof(ServerListView), false)] +public partial class ServerListViewModel : ViewModelBase +{ + [ObservableProperty] private bool _isLoading; + + public ServerListViewModel() + { + if (Design.IsDesignMode) + { + Provider = new TestServerList(); + } + } + + private IServerListProvider? _provider; + + public ObservableCollection ServerList { get; } = new(); + public ObservableCollection 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; + } + + if(!_provider.IsLoaded) + RefreshFromProvider(); + else + { + Clear(); + PasteServersFromList(); + } + } + } + + private void OnProviderDisposed() + { + Provider.OnLoaded -= RefreshRequired; + Provider.OnDisposed -= OnProviderDisposed; + if (Provider is IServerListDirtyInvoker invoker) + { + invoker.Dirty -= OnDirty; + } + + _provider = null; + } + + 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(); + } + + private void Clear() + { + ErrorList.Clear(); + 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 Initialise() + { + } +} \ No newline at end of file diff --git a/Nebula.Launcher/Views/MainView.axaml b/Nebula.Launcher/Views/MainView.axaml index c97c5aa..6285864 100644 --- a/Nebula.Launcher/Views/MainView.axaml +++ b/Nebula.Launcher/Views/MainView.axaml @@ -77,7 +77,7 @@