// // DocumentViewModel.cs // // Copyright (c) František Boháček. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.IO; using System.Linq; using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Dock.Model.Mvvm.Controls; using DynamicData.Binding; using NosSmooth.Comms.Data.Messages; using NosSmooth.Comms.Local; using NosSmooth.Core.Contracts; using NosSmooth.Core.Extensions; using NosSmooth.Core.Stateful; using PacketLogger.Models; using PacketLogger.Models.Filters; using PacketLogger.Models.Packets; using PacketLogger.ViewModels.Log; using PacketLogger.ViewModels.Sender; using PacketLogger.ViewModels.Settings; using ReactiveUI; using Remora.Results; namespace PacketLogger.ViewModels; /// public class DocumentViewModel : Document, INotifyPropertyChanged, IDisposable { private readonly ObservableCollection _providers; private readonly NostaleProcesses _processes; private readonly Action _onDocumentUnloaded; private CancellationTokenSource _ctSource; private IPacketProvider? _packetProvider; private IDisposable? _cleanUp; /// /// Initializes a new instance of the class. /// /// The filter profiles. /// The injector. /// The repository. /// The providers. /// The NosTale processes collection. /// The action to call on loaded. /// The action to call on document unloaded/closed. public DocumentViewModel ( FilterProfiles filterProfiles, CommsInjector injector, StatefulRepository repository, ObservableCollection providers, NostaleProcesses processes, Action onDocumentLoaded, Action onDocumentUnloaded ) { FilterProfiles = filterProfiles; _ctSource = new CancellationTokenSource(); _providers = providers; _processes = processes; _onDocumentUnloaded = onDocumentUnloaded; OpenDummy = ReactiveCommand.CreateFromTask ( () => Task.Run ( () => { Loading = true; _packetProvider = new DummyPacketProvider(Title); NestedViewModel = new PacketLogViewModel(_packetProvider, filterProfiles); Loaded = true; onDocumentLoaded(this); } ) ); OpenFile = ReactiveCommand.CreateFromTask ( async () => { var mainWindow = (App.Current!.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime) ?.MainWindow; var result = await new OpenFileDialog { AllowMultiple = false, InitialFileName = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName }.ShowAsync(mainWindow!); if (result is null || result.Length == 0) { return; } Loading = true; var path = result[0]; var provider = new FilePacketProvider(path); _packetProvider = provider; var openResult = await provider.Open(); if (!openResult.IsSuccess) { Error = "File could not be opened. " + openResult.ToFullString(); Loading = false; return; } Title = Path.GetFileName(path); NestedViewModel = new PacketLogViewModel(provider, filterProfiles); Loaded = true; Loading = false; onDocumentLoaded(this); } ); OpenProcess = ReactiveCommand.CreateFromTask ( async (process, ct) => { Loading = true; var connectionResult = await injector.EstablishNamedPipesConnectionAsync (process.Process, _ctSource.Token, ct); if (!connectionResult.IsDefined(out var connection)) { Error = "An error has occurred upon establishing a connection: " + connectionResult.ToFullString(); Loading = false; return; } var provider = new CommsPacketProvider(process, connection); _packetProvider = provider; repository.SetEntity(connection.Client, provider); var contractResult = await connection.Connection.ContractHanshake (new HandshakeRequest("PacketLogger", true, false)) .WaitForAsync(DefaultStates.ResponseObtained, ct: ct); if (!contractResult.IsDefined(out var handshakeResponse)) { repository.Remove(connection.Client); Error = "An error has occurred upon sending handshake: " + contractResult.ToFullString(); Loading = false; return; } var handshakeInitResponse = handshakeResponse.InitializationErrorfulResult ?? Result.FromSuccess(); if (!handshakeInitResponse.IsSuccess) { repository.Remove(connection.Client); Error = "An error has occurred during handshaking: " + handshakeInitResponse.ToFullString(); Loading = false; return; } _cleanUp = process.WhenPropertyChanged(x => x.CharacterString) .ObserveOn(RxApp.MainThreadScheduler) .Do ( _ => { Title = (process.BrowserManager.IsInGame ? process.BrowserManager.PlayerManager.Player.Name : null) ?? $"Not in game ({process.Process.Id})"; } ) .Subscribe(); NestedViewModel = new PacketLogViewModel(provider, filterProfiles); Title = handshakeResponse.CharacterName ?? $"Not in game ({process.Process.Id})"; Loading = false; Loaded = true; onDocumentLoaded(this); } ); OpenSender = ReactiveCommand.Create ( provider => { Loading = true; NestedViewModel = new PacketSenderViewModel(provider); Title = $"Sender ({provider.Name})"; Loaded = true; Loading = false; } ); ClearError = ReactiveCommand.Create(() => Error = null); OpenSettings = ReactiveCommand.Create( () => { Title = "Settings"; NestedViewModel = new SettingsViewModel(filterProfiles); Loaded = true; }); } /// /// Gets the filter profiles. /// public FilterProfiles FilterProfiles { get; } /// /// Gets the processes observable. /// public ObservableCollection Processes => _processes.Processes; /// /// Gets packet provider. /// public IPacketProvider? Provider => _packetProvider; /// /// Gets the open providers. /// public ObservableCollection Providers => _providers; /// /// Gets whether the document is currently being loaded. /// public bool Loading { get; private set; } /// /// Gets whether a document has been loaded. /// public bool Loaded { get; private set; } /// /// Gets or sets the current error. /// public string? Error { get; private set; } /// /// Gets or sets whether there is an error. /// public bool HasError => Error is not null; /// /// Gets the log tab view model. /// public ViewModelBase? NestedViewModel { get; private set; } /// /// Gets command for opening a dummy. /// public ReactiveCommand OpenSender { get; } /// /// Gets command for opening a dummy. /// public ReactiveCommand OpenDummy { get; } /// /// Gets command for opening a file. /// public ReactiveCommand OpenFile { get; } /// /// Gets the command to clear the error. /// public ReactiveCommand ClearError { get; } /// /// Gets the command for opening a process / connecting to a process. /// public ReactiveCommand OpenProcess { get; } /// /// Get open settings command. /// public ReactiveCommand OpenSettings { get; } /// public override bool OnClose() { _onDocumentUnloaded(this); _packetProvider?.Close().GetAwaiter().GetResult(); Dispose(); return base.OnClose(); } /// public void Dispose() { _cleanUp?.Dispose(); _ctSource.Cancel(); _ctSource.Dispose(); (NestedViewModel as IDisposable)?.Dispose(); OpenDummy.Dispose(); OpenFile.Dispose(); OpenProcess.Dispose(); OpenSender.Dispose(); } }