Add project files.

This commit is contained in:
2021-04-23 00:56:08 +02:00
parent 97cf4333ea
commit 1c19badeb5
31 changed files with 3612 additions and 0 deletions

537
MainWindow.xaml.cs Normal file
View File

@@ -0,0 +1,537 @@
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>(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;
}
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
private MainWindowProperties _properties = new MainWindowProperties();
private volatile List<Replay> _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<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(_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<Replay>();
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();
}
}
}