using Microsoft.Win32; using System; using System.Collections.Generic; 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; namespace AnotherReplayReader { internal sealed class MainWindowProperties : INotifyPropertyChanged { public MainWindowProperties() { string userDataLeafName = null; string replayFolderName = null; using (var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32)) using (var ra3Key = view32.OpenSubKey(@"Software\Electronic Arts\Electronic Arts\Red Alert 3", false)) { RA3Directory = ra3Key?.GetValue("Install Dir") as string; userDataLeafName = ra3Key?.GetValue("UserDataLeafName") as string; replayFolderName = ra3Key?.GetValue("ReplayFolderName") as string; } 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 NotifyPropertyChanged(T value, [CallerMemberName] string propertyName = "") { 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 { return _replayFolderPath; } set { _replayFolderPath = value; NotifyPropertyChanged(_replayFolderPath); } } public string ReplayFilterString { get { return _replayFilterString; } set { _replayFilterString = value; NotifyPropertyChanged(_replayFilterString); } } public string ReplayDetails { get { return _replayDetails; } set { _replayDetails = value; NotifyPropertyChanged(_replayDetails); } } 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 { return _currentReplay; } set { _currentReplay = value; NotifyPropertyChanged(_currentReplay); ReplayDetails = ""; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayPlayable")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplaySelected")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayDamaged")); } } private volatile string _replayFolderPath; private volatile string _replayDetails; private volatile string _replayFilterString; private volatile Replay _currentReplay; } /// /// MainWindow.xaml 的交互逻辑 /// public partial class MainWindow : Window { private MainWindowProperties _properties = new MainWindowProperties(); private volatile List _replayList; private Cache _cache = new Cache(); private PlayerIdentity _playerIdentity; private BigMinimapCache _minimapCache; private MinimapReader _minimapReader; private CancellationTokenSource _loadReplaysToken; public MainWindow() { DataContext = _properties; InitializeComponent(); var handling = new bool[1] { false }; Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) => { if (handling == null || handling[0] == true) { return; } handling[0] = true; Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}")); }; Closing += ((sender, eventArgs) => _cache.Save()); _playerIdentity = new PlayerIdentity(_cache); _minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory); _minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory); LoadReplays(); _ = AutoSaveReplays(); } private async Task AutoSaveReplays() { const string ourPrefix = "自动保存"; var errorMessageCount = 0; // filename and last write time var previousFiles = new Dictionary(StringComparer.OrdinalIgnoreCase); // filename and file size var lastReplays = new Dictionary(StringComparer.OrdinalIgnoreCase); while (true) { try { var changed = (from fileName in Directory.GetFiles(_properties.RA3ReplayFolderPath, "*.RA3Replay") let info = new FileInfo(fileName) where !info.Name.StartsWith(ourPrefix) where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc select info).ToList(); foreach (var info in changed) { previousFiles[info.FullName] = info.LastWriteTimeUtc; } var replays = changed.Select(info => { Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n"; try { using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { return new Replay(info.FullName, stream); } } catch (Exception e) { Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n"; return null; } }).Where(replay => replay != null); var newLastReplays = from replay in replays let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds) let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero) let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds) where threshold < 40 || endThreshold < 40 select replay; var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase); foreach (var savedLastReplay in lastReplays.Keys) { if (!toBeChecked.ContainsKey(savedLastReplay)) { try { using (var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) { toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream)); } } catch (Exception e) { Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n"; } } } foreach (var kv in toBeChecked) { Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n"; var replay = kv.Value; if (lastReplays.TryGetValue(kv.Key, out var fileSize)) { if (fileSize == replay.Size) { // skip if size is not changed Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n"; continue; } } Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n"; lastReplays[kv.Key] = replay.Size; var date = replay.Date; var playerString = $"{replay.NumberOfPlayingPlayers}名玩家"; if (replay.NumberOfPlayingPlayers <= 2) { var playingPlayers = from player in replay.Players let faction = ModData.GetFaction(replay.Mod, player.FactionID) where faction.Kind != FactionKind.Observer select $"{player.PlayerName}({faction.Name})"; playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y); } var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}"; var destinationPath = Path.Combine(_properties.RA3ReplayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay"); try { File.Copy(replay.Path, destinationPath, true); } catch (Exception e) { throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e); } } } catch (Exception e) { var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n"; Debug.Instance.DebugMessage += errorString; _ = Dispatcher.InvokeAsync(() => { try { if (Interlocked.Increment(ref errorMessageCount) == 1) { MessageBox.Show(errorString); } } finally { Interlocked.Decrement(ref errorMessageCount); } }); } await Task.Delay(10 * 1000); } } private async void LoadReplays(string nextSelected = null) { const string loadingString = "正在加载录像列表,请稍候"; Dispatcher.Invoke(() => { if (_image != null) { _image.Source = null; } if (_dataGrid != null) { _dataGrid.Items.Clear(); } }); _loadReplaysToken?.Cancel(); _loadReplaysToken = new CancellationTokenSource(); var cancelToken = _loadReplaysToken.Token; var path = _properties.ReplayFolderPath; var task = Task.Run(async () => { var messages = ""; var replayList = new List(); if (!Directory.Exists(path)) { messages = "这个文件夹并不存在。"; } else { var replays = Directory.EnumerateFiles(path, "*.RA3Replay"); foreach (var replayPath in replays) { try { var replay = await Task.Run(() => new Replay(replayPath)); replayList.Add(replay); _properties.ReplayDetails = loadingString + $"\n已加载 {replayList.Count} 个录像"; } catch (Exception exception) { Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n"; } cancelToken.ThrowIfCancellationRequested(); } } return new { Replays = replayList, Messages = messages }; }); try { var result = await task; _replayList = result.Replays; cancelToken.ThrowIfCancellationRequested(); DisplayReplays(result.Messages, nextSelected); } catch (OperationCanceledException) { } } private void DisplayReplays(string message = null, string nextSelected = null) { var filtered = _replayList; Dispatcher.Invoke(() => { _properties.CurrentReplay = null; _dataGrid.Items.Clear(); filtered.ForEach(x => _dataGrid.Items.Add(x)); _properties.ReplayDetails = message; if (nextSelected != null) { for (var i = 0; i < _dataGrid.Items.Count; ++i) { var replay = _dataGrid.Items[i] as Replay; if (replay == null) { continue; } if (replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase)) { _dataGrid.SelectedIndex = i; OnReplaySelectionChanged(null, null); break; } } } }); } private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e) { LoadReplays(); } private void OnReplaySelectionChanged(object sender, EventArgs e) { _properties.CurrentReplay = _dataGrid.SelectedItem as Replay; if (_properties.CurrentReplay == null) { return; } Dispatcher.Invoke(() => { _image.Source = null; }); string GetSizeString(double size) { if (size > 1024 * 1024) { return $"{Math.Round(size / (1024 * 1024), 2)}MB"; } return $"{Math.Round(size / 1024)}KB"; } const string formatA = "文件名:{0}\n大小:{1}\n"; const string formatB = "地图:{0}\n日期:{1}\n长度:{2}\n"; const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n"; const string playerListTitle = "玩家列表:\n"; var replay = _properties.CurrentReplay; var sizeString = GetSizeString(replay.Size); var lengthString = "录像已损坏,请先修复录像"; if (replay.HasFooter) { lengthString = $"{replay.Length}"; } var replaySaver = "[无法获取保存录像的玩家]"; try { replaySaver = replay.ReplaySaver.PlayerName; } catch { } _properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString); _properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString); _properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver); _properties.ReplayDetails += playerListTitle; foreach (var player in replay.Players) { if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator")) { break; } var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name; var realName = replay.Type == ReplayType.Lan ? _playerIdentity.QueryRealName(player.PlayerIP) : string.Empty; _properties.ReplayDetails += $"{player.PlayerName + realName},{factionName}\n"; } try { var mapPath = replay.MapPath.TrimEnd('/'); var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1); var minimapPath = $"{mapPath}/{mapName}_art.tga"; Dispatcher.Invoke(() => { var source = _minimapReader.TryReadTarga(minimapPath, replay.Mod); _image.Source = source; /*_image.Width = source.Width; _image.Height = source.Height;*/ }); } catch (Exception exception) { Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{exception}\r\n"; } try { replay.ParseBody(); } catch (Exception exception) { Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{exception}\r\n"; } } private void OnAboutButton_Click(object sender, RoutedEventArgs e) { var aboutWindow = new About(); aboutWindow.ShowDialog(); } private void OnBrowseButton_Click(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; LoadReplays(fileName); } } catch (Exception exception) { Debug.Instance.DebugMessage += $"Cannot set replay folder: \r\n{exception}\r\n"; } } private void OnDetailsButton_Click(object sender, RoutedEventArgs e) { var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity); detailsWindow.ShowDialog(); } private void OnDebugButton_Click(object sender, RoutedEventArgs e) { var debug = new Debug(); debug.ShowDialog(); } private void OnPlayReplayButton_Click(object sender, RoutedEventArgs e) { Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" "); } private void OnFixReplayButton_Click(object sender, RoutedEventArgs e) { try { var replay = _properties.CurrentReplay; 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.WriteReplay(replay); } } } catch (Exception exception) { MessageBox.Show($"无法修复录像:\r\n{exception}"); } LoadReplays(); } } }