AnotherReplayReader/MainWindow.xaml.cs
2021-10-13 16:42:38 +02:00

531 lines
21 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 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;
using System.Windows.Threading;
namespace AnotherReplayReader
{
internal sealed class MainWindowProperties : INotifyPropertyChanged
{
public MainWindowProperties()
{
string userDataLeafName = null;
string replayFolderName = null;
try
{
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;
}
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"获取注册表项时出现错误:{e}\r\n";
}
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>(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 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 => _currentReplay;
set
{
_currentReplay = value;
NotifyPropertyChanged(_currentReplay);
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;
private string _replayDetails;
private Replay _currentReplay;
}
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private readonly MainWindowProperties _properties = new MainWindowProperties();
private readonly List<Replay> _replayList = new List<Replay>();
private readonly Cache _cache = new Cache();
private readonly PlayerIdentity _playerIdentity;
private readonly BigMinimapCache _minimapCache;
private readonly 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])
{
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();
Task.Run(() => AutoSaveReplays(Dispatcher, _properties.RA3ReplayFolderPath));
}
private static async Task AutoSaveReplays(Dispatcher dispatcher, string replayFolderPath)
{
const string ourPrefix = "自动保存";
var errorMessageCount = 0;
// filename and last write time
var previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
// filename and file size
var lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
while (true)
{
try
{
var changed = (from fileName in Directory.GetFiles(replayFolderPath, "*.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(replayFolderPath, $"{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;
if (Interlocked.Increment(ref errorMessageCount) == 1)
{
_ = dispatcher.InvokeAsync(() =>
{
try
{
MessageBox.Show(errorString);
}
finally
{
Interlocked.Decrement(ref errorMessageCount);
}
});
}
}
await Task.Delay(10 * 1000);
}
}
private async void LoadReplays(string nextSelected = null)
{
const string loadingString = "正在加载录像列表,请稍候… 已加载 {0} 个录像";
try
{
_loadReplaysToken?.Cancel();
}
catch (AggregateException e)
{
Debug.Instance.DebugMessage += $"Cancellation failed: {e}";
}
_loadReplaysToken?.Dispose();
_loadReplaysToken = new CancellationTokenSource();
var cancelToken = _loadReplaysToken.Token;
var path = _properties.ReplayFolderPath;
if (_image != null)
{
_image.Source = null;
}
_dataGrid?.Items.Clear();
_replayList.Clear();
if (!Directory.Exists(path))
{
DisplayReplays("这个文件夹并不存在。", nextSelected);
return;
}
var newList = await Task.Run(() =>
{
var list = new List<Replay>();
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
{
if (cancelToken.IsCancellationRequested)
{
break;
}
try
{
list.Add(new Replay(replayPath));
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
continue;
}
_ = Dispatcher.Invoke(() => _properties.ReplayDetails = string.Format(loadingString, _replayList.Count));
}
return list;
});
_replayList.AddRange(newList);
DisplayReplays(string.Empty, nextSelected);
}
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)
{
if (_dataGrid.Items[i] is Replay replay && 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();
}
private void ReplayFilterBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
}
}
}