423 lines
17 KiB
C#
423 lines
17 KiB
C#
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);
|
||
}
|
||
}
|
||
}
|