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(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; } /// /// MainWindow.xaml 的交互逻辑 /// public partial class MainWindow : Window { private readonly TaskQueue _taskQueue; private readonly MainWindowProperties _properties = new(); private readonly Cache _cache = new(); 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 _filterStrings = ImmutableArray.Empty; private ImmutableArray _filteredReplays = ImmutableArray.Empty; public MainWindow() { _taskQueue = new(Dispatcher); _minimapCache = new BigMinimapCache(_properties.RA3Directory); _minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory); _replayList = new(); DataContext = _properties; InitializeComponent(); Closing += (sender, eventArgs) => { _cache.Save().Wait(); Application.Current.Shutdown(); }; } private async void OnMainWindowLoaded(object sender, EventArgs eventArgs) { Debug.Initialize(); await _cache.Initialization; ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath); var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); _ = _taskQueue.Enqueue(() => LoadReplays(null, token), token); const string permissionKey = "questionAsked"; if (_cache.GetOrDefault(permissionKey, false) is not true) { _cache.Set(permissionKey, true); var sb = new StringWriter(); sb.WriteLine("要不要自动检查更新呢?"); sb.WriteLine("之后也可以在“关于”窗口里,设置自动更新的选项"); var choice = MessageBox.Show(this, sb.ToString(), App.Name, MessageBoxButton.YesNo); _cache.Set(UpdateChecker.CheckForUpdatesKey, choice is MessageBoxResult.Yes); await _cache.Save(); } _ = UpdateChecker.CheckForUpdates(_cache).ContinueWith(t => Dispatcher.InvokeAsync(() => { var updateData = t.Result; if (updateData.IsNewVersion()) { var about = new About(_cache, updateData) { Owner = this }; about.ShowDialog(); } })); } 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(); _dataGrid.Items.Refresh(); } cancelToken.ThrowIfCancellationRequested(); _replayList = new(); var path = _properties.ReplayFolderPath; if (!Directory.Exists(path)) { await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken); return; } var result = await Task.Run(() => { var list = new List(); 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()); }, 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(); _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(); 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); var text = _replayFolderPathBox.Text; const string assemblyMagic = "!DreamSign"; const string jsonMagic = "!FantasySeal"; switch (_replayFolderPathBox.Text) { case "!SpellCard": _replayDetailsBox.Text = $"{assemblyMagic}\r\n"; _replayDetailsBox.Text += $"{jsonMagic}\r\n"; break; case assemblyMagic: break; case jsonMagic: UpdateChecker.Sign(); break; } } 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(); await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token); } private void OnAboutButtonClick(object sender, RoutedEventArgs e) { var aboutWindow = new About(_cache) { Owner = this }; 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(this); 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!) { Owner = this }; 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(this, $"无法修复录像:\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); } } }