AnotherReplayReader/MainWindow.xaml.cs
2021-10-19 23:17:30 +02:00

423 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace AnotherReplayReader
{
internal sealed class MainWindowProperties : INotifyPropertyChanged
{
public MainWindowProperties()
{
const RegistryHive hklm = RegistryHive.LocalMachine;
RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
if (string.IsNullOrWhiteSpace(userDataLeafName))
{
userDataLeafName = "Red Alert 3";
}
if (string.IsNullOrWhiteSpace(replayFolderName))
{
replayFolderName = "Replays";
}
RA3ReplayFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, replayFolderName);
ReplayFolderPath = RA3ReplayFolderPath;
CustomMapsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), userDataLeafName, "Maps");
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
}
public event PropertyChangedEventHandler? PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
{
if (!Equals(target, newValue))
{
target = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public string? RA3Directory { get; }
public string RA3ReplayFolderPath { get; }
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
public string CustomMapsDirectory { get; }
public string ModsDirectory { get; }
public string ReplayFolderPath
{
get => _replayFolderPath;
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
}
public string? ReplayDetails
{
get => _replayDetails;
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
}
public bool ReplaySelected => CurrentReplay != null;
public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
public bool ReplayDamaged => CurrentReplay?.HasFooter == false;
public Replay? CurrentReplay
{
get => _currentReplay;
set
{
SetAndNotifyPropertyChanged(ref _currentReplay, value);
ReplayDetails = "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayPlayable)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplaySelected)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayDamaged)));
}
}
private string _replayFolderPath = null!;
private string? _replayDetails;
private Replay? _currentReplay;
}
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private readonly TaskQueue _taskQueue;
private readonly MainWindowProperties _properties = new();
private readonly Cache _cache = new();
private readonly PlayerIdentity _playerIdentity;
private readonly BigMinimapCache _minimapCache;
private readonly MinimapReader _minimapReader;
private readonly CancelManager _cancelLoadReplays = new();
private readonly CancelManager _cancelFilterReplays = new();
private readonly CancelManager _cancelDisplayReplays = new();
private ReplayPinyinList _replayList;
private ImmutableArray<string> _filterStrings = ImmutableArray<string>.Empty;
private ImmutableArray<Replay> _filteredReplays = ImmutableArray<Replay>.Empty;
public MainWindow()
{
_taskQueue = new(Dispatcher);
_playerIdentity = new PlayerIdentity(_cache);
_minimapCache = new BigMinimapCache(_properties.RA3Directory);
_minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
_replayList = new(_playerIdentity);
DataContext = _properties;
InitializeComponent();
Closing += (sender, eventArgs) =>
{
_cache.Save();
Application.Current.Shutdown();
};
}
private async void OnMainWindowLoaded(object sender, EventArgs e)
{
Debug.Initialize();
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
}
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
_properties.CurrentReplay = null;
_image.Source = null;
if (_dataGrid.Items.Count > 0)
{
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
cancelToken.ThrowIfCancellationRequested();
_replayList = new(_playerIdentity);
var path = _properties.ReplayFolderPath;
if (!Directory.Exists(path))
{
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
return;
}
var result = await Task.Run(() =>
{
var list = new List<Replay>();
var clock = new Stopwatch();
clock.Start();
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
{
cancelToken.ThrowIfCancellationRequested();
try
{
var replay = new Replay(replayPath);
list.Add(replay);
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
continue;
}
if (clock.ElapsedMilliseconds > 300)
{
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
clock.Restart();
}
}
return new ReplayPinyinList(list.ToImmutableArray(), _playerIdentity);
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
_replayList = result;
await FilterReplays(string.Empty, nextSelected, filterToken);
}
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
_cancelDisplayReplays.Reset(cancelToken);
_filteredReplays = _replayList.Replays;
_properties.CurrentReplay = null;
_dataGrid.SelectedItem = null;
_image.Source = null;
if (_filterStrings.Any())
{
_properties.ReplayDetails = "正在筛选符合条件的录像…";
if (_dataGrid.Items.Count > 0)
{
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
var result = await Task.Run(() =>
{
var query = from replay in list.AsParallel().WithCancellation(cancelToken)
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
select replay.Replay;
return query.ToImmutableArray();
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
_filteredReplays = result;
}
_properties.CurrentReplay = null;
_dataGrid.SelectedItem = null;
_dataGrid.ItemsSource = _filteredReplays;
_dataGrid.Items.Refresh();
_properties.ReplayDetails = message;
if (nextSelected is not null)
{
for (var i = 0; i < _dataGrid.Items.Count; ++i)
{
if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
{
_dataGrid.SelectedIndex = i;
break;
}
}
}
}
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
_properties.CurrentReplay = null;
_image.Source = null;
_properties.ReplayDetails = replayDetails;
// 开始获取小地图
var mapPath = replay.MapPath;
var minimapTask = _minimapReader.TryReadTargaAsync(replay);
// 解析录像内容
try
{
replay = await Task.Run(() => new Replay(replay.Path, true));
cancelToken.ThrowIfCancellationRequested();
}
catch (Exception e) when (e is not OperationCanceledException)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{e}\r\n";
return;
}
// 假如小地图变了(这……),那么重新加载小地图
if (replay.MapPath != mapPath)
{
minimapTask.Forget();
minimapTask = _minimapReader.TryReadTargaAsync(replay);
}
var newDetails = replay.GetDetails(_playerIdentity);
if (_properties.ReplayDetails != newDetails)
{
if (_replayList.Replays.FindIndex(r => r.PathEquals(replay)) is int index)
{
_replayList = _replayList.SetItem(index, replay.CloneHeader());
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
await FilterReplays(newDetails, replay.Path, token);
return;
}
}
_properties.CurrentReplay = replay;
_properties.ReplayDetails = newDetails;
try
{
var newSource = await minimapTask;
cancelToken.ThrowIfCancellationRequested();
_image.Source = newSource;
/* _image.Width = source.Width;
_image.Height = source.Height; */
}
catch (Exception e) when (e is not OperationCanceledException)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
}
}
private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
{
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
}
private async void OnReplaySelectionChanged(object sender, EventArgs e)
{
if (_dataGrid.SelectedItem is not Replay replay)
{
return;
}
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
_properties.ReplayDetails = replay.GetDetails(_playerIdentity);
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
}
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
{
var aboutWindow = new About();
aboutWindow.ShowDialog();
}
private void OnBrowseButtonClick(object sender, RoutedEventArgs e)
{
try
{
var openFileDialog = new OpenFileDialog
{
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
InitialDirectory = _properties.ReplayFolderPath,
};
var result = openFileDialog.ShowDialog();
if (result == true)
{
var fileName = openFileDialog.FileName;
var directoryName = Path.GetDirectoryName(fileName);
_properties.ReplayFolderPath = directoryName;
}
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Cannot set replay folder: \r\n{exception}\r\n";
}
}
private void OnDetailsButtonClick(object sender, RoutedEventArgs e)
{
var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity);
detailsWindow.ShowDialog();
}
private void OnDebugButtonClick(object sender, RoutedEventArgs e) => Debug.ShowDialog();
private void OnPlayReplayButtonClick(object sender, RoutedEventArgs e)
{
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
}
private async void OnFixReplayButtonClick(object sender, RoutedEventArgs e)
{
var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay");
try
{
var saveFileDialog = new SaveFileDialog
{
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
InitialDirectory = Path.GetDirectoryName(replay.Path),
OverwritePrompt = true,
Title = "保存已被修复的录像"
};
var result = saveFileDialog.ShowDialog(this);
if (result == true)
{
using var file = saveFileDialog.OpenFile();
using var writer = new BinaryWriter(file);
writer.Write(replay);
}
}
catch (Exception exception)
{
MessageBox.Show($"无法修复录像:\r\n{exception}");
}
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token);
}
private async void OnReplayFilterBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
_filterStrings = _replayFilterBox.Text
.Split(',', ' ', '')
.Select(x => x.ToPinyin())
.Where(x => !string.IsNullOrEmpty(x))
.ToImmutableArray()!;
var currentReplayPath = _properties?.CurrentReplay?.Path;
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
}
}
}