diff --git a/About.xaml b/About.xaml index 38acf8e..823edef 100644 --- a/About.xaml +++ b/About.xaml @@ -14,20 +14,21 @@ - - - - + + + + - + - - - + + + + - + - + diff --git a/About.xaml.cs b/About.xaml.cs index b311812..4174e2a 100644 --- a/About.xaml.cs +++ b/About.xaml.cs @@ -1,17 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Diagnostics; using System.Windows; -using System.Windows.Controls; -using System.Windows.Data; -using System.Windows.Documents; -using System.Windows.Input; -using System.Windows.Media; -using System.Windows.Media.Imaging; -using System.Windows.Shapes; namespace AnotherReplayReader { diff --git a/AnotherReplayReader.csproj b/AnotherReplayReader.csproj index da12d7b..9fcf718 100644 --- a/AnotherReplayReader.csproj +++ b/AnotherReplayReader.csproj @@ -6,6 +6,7 @@ publish\ false true + true AnyCPU @@ -20,19 +21,34 @@ - - - - - - + - + + + TechnologyAssembler.Core.dll + true + + + + - - - + + + + + + + True + True + Settings.settings + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + \ No newline at end of file diff --git a/APM.xaml b/ApmWindow.xaml similarity index 96% rename from APM.xaml rename to ApmWindow.xaml index 4738814..e892a4f 100644 --- a/APM.xaml +++ b/ApmWindow.xaml @@ -1,4 +1,4 @@ - new DataValue(x)).ToArray(); } - } - internal static class DataTableFactory - { - static public List Get(Replay replay, PlayerIdentity identity) + public static List GetList(Replay replay, PlayerIdentity identity) { var list = new List { @@ -136,7 +134,7 @@ namespace AnotherReplayReader }; if (replay.Type == ReplayType.Lan && identity.IsUsable) { - dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIP)))); + dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIp)))); } var commandCounts = replay.GetCommandCounts(); @@ -155,65 +153,54 @@ namespace AnotherReplayReader return dataList; } } + /// /// APM.xaml 的交互逻辑 /// - internal partial class APM : Window + internal partial class ApmWindow : Window { - private PlayerIdentity _identity; + private readonly PlayerIdentity _identity; - public APM(Replay replay, PlayerIdentity identity) + public ApmWindow(Replay replay, PlayerIdentity identity) { - InitializeComponent(); - _identity = identity; + InitializeComponent(); + InitializeApmWindowData(replay); + } + private async void InitializeApmWindowData(Replay replay) + { if (_identity.IsUsable) { - Dispatcher.Invoke(() => - { - _setPlayerButton.IsEnabled = true; - _setPlayerButton.Visibility = Visibility.Visible; - }); + _setPlayerButton.IsEnabled = true; + _setPlayerButton.Visibility = Visibility.Visible; } else { - Dispatcher.Invoke(() => - { - _setPlayerButton.IsEnabled = false; - _setPlayerButton.Visibility = Visibility.Hidden; - }); + _setPlayerButton.IsEnabled = false; + _setPlayerButton.Visibility = Visibility.Hidden; } - Task.Run(() => + try { - try + var dataList = await Task.Run(() => DataRow.GetList(replay, _identity)); + _table.Items.Clear(); + foreach (var row in dataList) { - var dataList = DataTableFactory.Get(replay, _identity); - Dispatcher.Invoke(() => - { - _table.Items.Clear(); - foreach (var row in dataList) - { - _table.Items.Add(row); - } + _table.Items.Add(row); + } - _table.Columns[1].Header = dataList[0].Player1Value; - _table.Columns[2].Header = dataList[0].Player2Value; - _table.Columns[3].Header = dataList[0].Player3Value; - _table.Columns[4].Header = dataList[0].Player4Value; - _table.Columns[5].Header = dataList[0].Player5Value; - _table.Columns[6].Header = dataList[0].Player6Value; - }); - } - catch (Exception e) - { - Dispatcher.Invoke(() => - { - MessageBox.Show($"加载录像信息失败:\r\n{e}"); - }); - } - }); + _table.Columns[1].Header = dataList[0].Player1Value; + _table.Columns[2].Header = dataList[0].Player2Value; + _table.Columns[3].Header = dataList[0].Player3Value; + _table.Columns[4].Header = dataList[0].Player4Value; + _table.Columns[5].Header = dataList[0].Player5Value; + _table.Columns[6].Header = dataList[0].Player6Value; + } + catch (Exception e) + { + MessageBox.Show($"加载录像信息失败:\r\n{e}"); + } } private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e) @@ -222,7 +209,7 @@ namespace AnotherReplayReader window1.ShowDialog(); } - private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e) + private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e) { _table.SelectAll(); } diff --git a/App.xaml b/App.xaml index 75c9b3c..23908b7 100644 --- a/App.xaml +++ b/App.xaml @@ -2,7 +2,8 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:AnotherReplayReader" - StartupUri="MainWindow.xaml"> + StartupUri="MainWindow.xaml" + ShutdownMode="OnMainWindowClose"> diff --git a/App.xaml.cs b/App.xaml.cs index b45448e..efec509 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,8 +1,5 @@ using System; -using System.Collections.Generic; -using System.Configuration; -using System.Data; -using System.Linq; +using System.Threading; using System.Windows; namespace AnotherReplayReader @@ -12,5 +9,25 @@ namespace AnotherReplayReader /// public partial class App : Application { + private int _isInException = 0; + public App() + { + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => + { + if (Interlocked.Increment(ref _isInException) > 1) + { + return; + } + Dispatcher.Invoke(() => + { + const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?"; + var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", "自动录像机", MessageBoxButton.YesNo); + if (choice == MessageBoxResult.Yes) + { + Debug.Instance.RequestSave(); + } + }); + }; + } } } diff --git a/Auth.cs b/Auth.cs index 4faf608..1098838 100644 --- a/Auth.cs +++ b/Auth.cs @@ -1,37 +1,33 @@ -using System; +using Microsoft.Win32; +using System; using System.IO; -using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using System.Security.Cryptography; -using Microsoft.Win32; +using System.Text; namespace AnotherReplayReader { internal static class Auth { - public static string ID { get; private set; } + public static string? ID { get; } static Auth() { ID = null; - var windowsID = null as string; + string? windowsID; try { - using (var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64)) - using (var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false)) - { - windowsID = winNt?.GetValue("ProductId") as string; - } + using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + using var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false); + windowsID = winNt?.GetValue("ProductId") as string; } - catch(Exception) + catch { return; } - var randomKey = null as string; + string? randomKey; try { var folderPath = Cache.CacheDirectory; @@ -41,34 +37,31 @@ namespace AnotherReplayReader } var keyPath = Path.Combine(folderPath, "id"); - if(!File.Exists(keyPath)) + if (!File.Exists(keyPath)) { File.WriteAllText(keyPath, Guid.NewGuid().ToString()); } randomKey = File.ReadAllText(keyPath); } - catch(Exception) - { - return; - } - - if(string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey)) + catch { return; } - - using (var sha = SHA256.Create()) + if (string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey)) { - var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey)); - ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}")); + return; } + + using var sha = SHA256.Create(); + var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey)); + ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}")); } public static string GetKey() { - if(ID == null) + if (ID == null) { return string.Empty; } @@ -82,7 +75,7 @@ namespace AnotherReplayReader rng.GetNonZeroBytes(salt); } - for(var i = 0; i < bytes.Length; ++i) + for (var i = 0; i < bytes.Length; ++i) { bytes[i] = (byte)(bytes[i] ^ salt[i % salt.Length]); } diff --git a/BigMinimapCache.cs b/BigMinimapCache.cs index 294c2e2..b090dd5 100644 --- a/BigMinimapCache.cs +++ b/BigMinimapCache.cs @@ -1,172 +1,35 @@ -using System; -using System.Collections.Generic; +using AnotherReplayReader.Utils; +using Microsoft.Win32; +using System; using System.IO; using System.Linq; -using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; -using Microsoft.Win32; -using OpenSage.FileFormats.Big; +using TechnologyAssembler.Core.IO; namespace AnotherReplayReader { internal sealed class BigMinimapCache { - private sealed class CacheAdapter - { - public List Bigs { get; set; } - public Dictionary MapsToBigs { get; set; } + private readonly object _lock = new(); + private SkuDefFileSystemProvider? _skudefFileSystem = null; - public CacheAdapter() - { - Bigs = new List(); - MapsToBigs = new Dictionary(); - } + public BigMinimapCache(string? ra3Directory) + { + Task.Run(() => Initialize(ra3Directory)); } - //private Cache _cache; - private volatile IReadOnlyDictionary _mapsToBigs = null; - - public BigMinimapCache(Cache cache, string ra3Directory) + public bool TryGetEntry(string path, out Stream? bigEntry) { - //_cache = cache; + bigEntry = null; - Task.Run(() => - { - try - { - if (!Directory.Exists(ra3Directory)) - { - Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n"; - return; - } - - var bigSet = ParseSkudefs(Directory.EnumerateFiles(ra3Directory, "*.SkuDef")); - - //var cached = _cache.GetOrDefault("bigsCache", new CacheAdapter()); - var mapsToBigs = new Dictionary(); - - foreach (var bigPath in bigSet/*.Where(x => !cached.Bigs.Contains(x))*/) - { - if (!File.Exists(bigPath)) - { - Debug.Instance.DebugMessage += $"Big {bigPath} does not exist.\r\n"; - continue; - } - - Debug.Instance.DebugMessage += $"Trying to add Big {bigPath} to big minimap cache...\r\n"; - try - { - using (var big = new BigArchive(bigPath)) - { - foreach (var entry in big.Entries) - { - if (entry.FullName.EndsWith("_art.tga", StringComparison.OrdinalIgnoreCase)) - { - mapsToBigs[entry.FullName] = bigPath; - } - - } - } - } - catch(Exception exception) - { - Debug.Instance.DebugMessage += $"Exception when reading big:\r\n {exception}\r\n"; - } - } - //cached.Bigs = bigSet.ToList(); - - //_cache.Set("bigsCache", cached); - _mapsToBigs = mapsToBigs; //cached.MapsToBigs; - } - catch (Exception exception) - { - Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n"; - } - }); - } - - public static HashSet ParseSkudefs(IEnumerable skudefs) - { - var skudefSet = new HashSet(skudefs.Select(x => x.ToLowerInvariant())); - var unreadSkudefs = new HashSet(); - var bigSet = new HashSet(); - - void ReadSkudefLine(string baseDirectory, string line, string expectedCommand, Action action) - { - try - { - char[] separators = { ' ', '\t' }; - line = line.ToLowerInvariant(); - var splitted = line.Split(separators, 2, StringSplitOptions.RemoveEmptyEntries); - if (splitted[0].Equals(expectedCommand)) - { - var path = splitted[1]; - if (!Path.IsPathRooted(path)) - { - path = Path.Combine(baseDirectory, path); - } - action(path); - } - } - catch (Exception exception) - { - Debug.Instance.DebugMessage += $"Exception when parsing skudef line:\r\n {exception}\r\n"; - } - } - - void ReadSkudef(string fileName, Action onBaseDirectoryAndLine) - { - try - { - var baseDirectory = Path.GetDirectoryName(fileName).ToLowerInvariant(); - foreach (var line in File.ReadAllLines(fileName)) - { - onBaseDirectoryAndLine(baseDirectory, line); - } - } - catch (Exception exception) - { - Debug.Instance.DebugMessage += $"Exception when parsing skudef file:\r\n {exception}\r\n"; - } - } - - foreach (var skudef in skudefSet) - { - ReadSkudef(skudef, (baseDirectory, line) => - { - ReadSkudefLine(baseDirectory, line, "add-config", x => - { - if (!skudefSet.Contains(x)) - { - unreadSkudefs.Add(x); - } - }); - - ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x)); - }); - } - - foreach (var skudef in unreadSkudefs) - { - ReadSkudef(skudef, (baseDirectory, line) => - { - ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x)); - }); - } - - return bigSet; - } - - public bool TryGetBigByEntryPath(string path, out BigArchive big) - { - big = null; - - if (_mapsToBigs == null) + using var locker = new Lock(_lock); + if (_skudefFileSystem is not { } fs) { return false; } - if (!_mapsToBigs.ContainsKey(path)) + if (!fs.FileExists(path)) { Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n"; return false; @@ -174,58 +37,75 @@ namespace AnotherReplayReader try { - var bigPath = _mapsToBigs[path]; - big = new BigArchive(bigPath); - if(big.GetEntry(path) == null) - { - //_cache.Remove("bigsCache"); - big.Dispose(); - big = null; - return false; - } + bigEntry = fs.OpenStream(path, VirtualFileModeType.Open); + return true; } catch (Exception exception) { Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n"; - big = null; - return false; } - return true; + return false; } - public byte[] TryReadBytesFromBig(string path) + private void Initialize(string? ra3Directory) { - if(_mapsToBigs == null) - { - return null; - } - - if(!_mapsToBigs.ContainsKey(path)) - { - return null; - } - try { - var bigPath = _mapsToBigs[path]; - using (var big = new BigArchive(path)) + if (ra3Directory is null || !Directory.Exists(ra3Directory)) { - var entry = big.GetEntry(path); - using (var stream = entry.Open()) - using (var reader = new BinaryReader(stream)) - { - return reader.ReadBytes((int)entry.Length); - } + Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n"; + return; } + + var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language"); + var currentLanguage_ = $"{currentLanguage}_"; + double SkudefVersionSelector(string fullPath) + { + var value = -1.0; + try + { + const string skudefPrefix = "RA3"; + const int prefix = 1; + const int language = 2; + const int majorVersion = 3; + const int minorVersion = 4; + const int majorVersionMultiplier = 10000; + const int correctLanguageBonus = 1000_0000; + + value = 0; + var stem = Path.GetFileNameWithoutExtension(fullPath); + var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)"); + if (!match.Success || match.Groups.Cast().Any(g => !g.Success)) + { + return value; + } + + value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0; + value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0; + value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier; + value += int.Parse(match.Groups[minorVersion].Value); + return value; + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n"; + } + return value; + } + var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef") + orderby SkudefVersionSelector(p) descending + select p).First(); + Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n"; + + using var locker = new Lock(_lock); + DronePlatform.BuildTechnologyAssembler(); + _skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef); } catch (Exception exception) { - Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n"; - //_cache.Remove("bigsCache"); + Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n"; } - - return null; } } } diff --git a/Cache.cs b/Cache.cs index d70d352..1365cf2 100644 --- a/Cache.cs +++ b/Cache.cs @@ -2,11 +2,8 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Linq; -using System.Text; -using System.Web.Script.Serialization; using System.Threading.Tasks; - +using static System.Text.Json.JsonSerializer; namespace AnotherReplayReader { @@ -15,24 +12,32 @@ namespace AnotherReplayReader public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader"); public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache"); - private ConcurrentDictionary _storage; + private readonly ConcurrentDictionary _storage = new(); public Cache() { - try + Task.Run(async () => { - if (!Directory.Exists(CacheDirectory)) + try { - Directory.CreateDirectory(CacheDirectory); - } + if (!Directory.Exists(CacheDirectory)) + { + Directory.CreateDirectory(CacheDirectory); + } - var serializer = new JavaScriptSerializer(); - _storage = serializer.Deserialize>(File.ReadAllText(CacheFilePath)); - } - catch - { - _storage = new ConcurrentDictionary(); - } + using var cacheStream = File.OpenRead(CacheFilePath); + var futureCache = DeserializeAsync>(cacheStream).ConfigureAwait(false); + if (await futureCache is not { } cached) + { + return; + } + foreach (var kv in cached) + { + _storage.TryAdd(kv.Key, kv.Value); + } + } + catch { } + }); } public T GetOrDefault(string key, in T defaultValue) @@ -41,8 +46,7 @@ namespace AnotherReplayReader { if (_storage.TryGetValue(key, out var valueString)) { - var serializer = new JavaScriptSerializer(); - return serializer.Deserialize(valueString); + return Deserialize(valueString) ?? defaultValue; } } catch { } @@ -53,8 +57,20 @@ namespace AnotherReplayReader { try { - var serializer = new JavaScriptSerializer(); - _storage[key] = serializer.Serialize(value); + _storage[key] = Serialize(value); + Save(); + } + catch { } + } + + public void SetValues(params (string Key, object Value)[] values) + { + try + { + foreach (var (key, value) in values) + { + _storage[key] = Serialize(value); + } Save(); } catch { } @@ -66,12 +82,12 @@ namespace AnotherReplayReader Save(); } - public void Save() + public async void Save() { try { - var serializer = new JavaScriptSerializer(); - File.WriteAllText(CacheFilePath, serializer.Serialize(_storage)); + using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write); + await SerializeAsync(cacheStream, _storage).ConfigureAwait(false); } catch { } } diff --git a/Debug.xaml.cs b/Debug.xaml.cs index 25d2917..017c47d 100644 --- a/Debug.xaml.cs +++ b/Debug.xaml.cs @@ -1,7 +1,9 @@ -using Microsoft.Win32; +using AnotherReplayReader.Utils; +using Microsoft.Win32; using System; +using System.Collections.Generic; +using System.ComponentModel; using System.IO; -using System.Threading; using System.Windows; namespace AnotherReplayReader @@ -21,11 +23,33 @@ namespace AnotherReplayReader } } + private readonly object _lock = new(); + private readonly List _list = new(); + public event Action? NewText; public Proxy DebugMessage { - get => new Proxy(); - set => NewText?.Invoke(value.Payload); + get => new(); + set + { + var text = value.Payload; + using var locker = new Lock(_lock); + if (NewText is null || _list.Count > 0) + { + _list.Add(text); + if (NewText is not null) + { + text = string.Join(string.Empty, _list); + _list.Clear(); + _list.Capacity = 0; + } + else + { + return; + } + } + NewText(text); + } } public event Action? RequestedSave; @@ -36,18 +60,31 @@ namespace AnotherReplayReader /// public sealed partial class Debug : Window { - public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper(); + public static readonly DebugMessageWrapper Instance = new(); + private static readonly object _lock = new(); private static Debug? _window = null; public static void Initialize() { - Interlocked.CompareExchange(ref _window, new(), null); - _window.InitializeComponent(); + using var locker = new Lock(_lock); + if (_window is not null) + { + return; + } + var window = new Debug(); + window.InitializeComponent(); + _window = window; Instance.NewText += _window.AppendText; Instance.RequestedSave += _window.ExportInMainWindow; } - public static new void ShowDialog() => (_window as Window)?.ShowDialog(); + public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog()); + + protected override void OnClosing(CancelEventArgs e) + { + e.Cancel = true; + Hide(); + } private Debug() { } @@ -55,7 +92,7 @@ namespace AnotherReplayReader private void OnClear_Click(object sender, RoutedEventArgs e) => _textBox.Clear(); - private void AppendText(string s) => Dispatcher.Invoke(() => _textBox.AppendText(s)); + private void AppendText(string s) => Dispatcher.InvokeAsync(() => _textBox.AppendText(s)); private void ExportInMainWindow() => Export(Application.Current.MainWindow); @@ -72,11 +109,9 @@ namespace AnotherReplayReader var result = saveFileDialog.ShowDialog(owner); if (result == true) { - using (var file = saveFileDialog.OpenFile()) - using (var writer = new StreamWriter(file)) - { - writer.Write(_textBox.Text); - } + using var file = saveFileDialog.OpenFile(); + using var writer = new StreamWriter(file); + writer.Write(_textBox.Text); } }); } diff --git a/DronePlatform.cs b/DronePlatform.cs new file mode 100644 index 0000000..2332eb6 --- /dev/null +++ b/DronePlatform.cs @@ -0,0 +1,25 @@ +using AnotherReplayReader.Utils; +using TechnologyAssembler; +using TechnologyAssembler.Core.Diagnostics; + +namespace AnotherReplayReader +{ + public static class DronePlatform + { + private static readonly object _lock = new(); + private static bool _built = false; + + public static void BuildTechnologyAssembler() + { + using var locker = new Lock(_lock); + if (_built) + { + return; + } + new TechnologyAssemblerCoreModule().Initialize(); + Tracer.SetTraceLevel(7); + Tracer.TraceWrite += (source, type, message) => Debug.Instance.DebugMessage += $"[{source}][{type}] {message}\r\n"; + _built = true; + } + } +} diff --git a/Excludes.txt b/Excludes.txt deleted file mode 100644 index 7047d78..0000000 --- a/Excludes.txt +++ /dev/null @@ -1 +0,0 @@ -Veldrid\.SDL2\.dll \ No newline at end of file diff --git a/ILMergeConfig.json b/ILMergeConfig.json index 4220560..8ad6429 100644 --- a/ILMergeConfig.json +++ b/ILMergeConfig.json @@ -1,121 +1,36 @@ -{ +{ "General": { + "OutputFile": null, + "TargetPlatform": null, + "KeyFile": null, + "AlternativeILMergePath": null, "InputAssemblies": [ - "$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll", - "$(TargetDir)Microsoft.Extensions.DependencyModel.dll", - "$(TargetDir)Microsoft.Win32.Primitives.dll", - "$(TargetDir)NativeLibraryLoader.dll", - "$(TargetDir)netstandard.dll", - "$(TargetDir)Newtonsoft.Json.dll", - "$(TargetDir)NLog.dll", - "$(TargetDir)OpenSage.Core.dll", - "$(TargetDir)OpenSage.FileFormats.Big.dll", - "$(TargetDir)OpenSage.FileFormats.dll", - "$(TargetDir)OpenSage.FileFormats.RefPack.dll", - "$(TargetDir)OpenSage.Mathematics.dll", - "$(TargetDir)Pfim.dll", - "$(TargetDir)System.AppContext.dll", - "$(TargetDir)System.Collections.Concurrent.dll", - "$(TargetDir)System.Collections.dll", - "$(TargetDir)System.Collections.NonGeneric.dll", - "$(TargetDir)System.Collections.Specialized.dll", - "$(TargetDir)System.ComponentModel.dll", - "$(TargetDir)System.ComponentModel.EventBasedAsync.dll", - "$(TargetDir)System.ComponentModel.Primitives.dll", - "$(TargetDir)System.ComponentModel.TypeConverter.dll", - "$(TargetDir)System.Console.dll", - "$(TargetDir)System.Data.Common.dll", - "$(TargetDir)System.Diagnostics.Contracts.dll", - "$(TargetDir)System.Diagnostics.Debug.dll", - "$(TargetDir)System.Diagnostics.DiagnosticSource.dll", - "$(TargetDir)System.Diagnostics.FileVersionInfo.dll", - "$(TargetDir)System.Diagnostics.Process.dll", - "$(TargetDir)System.Diagnostics.StackTrace.dll", - "$(TargetDir)System.Diagnostics.TextWriterTraceListener.dll", - "$(TargetDir)System.Diagnostics.Tools.dll", - "$(TargetDir)System.Diagnostics.TraceSource.dll", - "$(TargetDir)System.Diagnostics.Tracing.dll", - "$(TargetDir)System.Drawing.Primitives.dll", - "$(TargetDir)System.Dynamic.Runtime.dll", - "$(TargetDir)System.Globalization.Calendars.dll", - "$(TargetDir)System.Globalization.dll", - "$(TargetDir)System.Globalization.Extensions.dll", - "$(TargetDir)System.IO.Compression.dll", - "$(TargetDir)System.IO.Compression.ZipFile.dll", - "$(TargetDir)System.IO.dll", - "$(TargetDir)System.IO.FileSystem.dll", - "$(TargetDir)System.IO.FileSystem.DriveInfo.dll", - "$(TargetDir)System.IO.FileSystem.Primitives.dll", - "$(TargetDir)System.IO.FileSystem.Watcher.dll", - "$(TargetDir)System.IO.IsolatedStorage.dll", - "$(TargetDir)System.IO.MemoryMappedFiles.dll", - "$(TargetDir)System.IO.Pipes.dll", - "$(TargetDir)System.IO.UnmanagedMemoryStream.dll", - "$(TargetDir)System.Linq.dll", - "$(TargetDir)System.Linq.Expressions.dll", - "$(TargetDir)System.Linq.Parallel.dll", - "$(TargetDir)System.Linq.Queryable.dll", - "$(TargetDir)System.Net.Http.dll", - "$(TargetDir)System.Net.NameResolution.dll", - "$(TargetDir)System.Net.NetworkInformation.dll", - "$(TargetDir)System.Net.Ping.dll", - "$(TargetDir)System.Net.Primitives.dll", - "$(TargetDir)System.Net.Requests.dll", - "$(TargetDir)System.Net.Security.dll", - "$(TargetDir)System.Net.Sockets.dll", - "$(TargetDir)System.Net.WebHeaderCollection.dll", - "$(TargetDir)System.Net.WebSockets.Client.dll", - "$(TargetDir)System.Net.WebSockets.dll", - "$(TargetDir)System.Numerics.Vectors.dll", - "$(TargetDir)System.ObjectModel.dll", - "$(TargetDir)System.Reflection.dll", - "$(TargetDir)System.Reflection.Extensions.dll", - "$(TargetDir)System.Reflection.Primitives.dll", - "$(TargetDir)System.Resources.Reader.dll", - "$(TargetDir)System.Resources.ResourceManager.dll", - "$(TargetDir)System.Resources.Writer.dll", - "$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll", - "$(TargetDir)System.Runtime.CompilerServices.VisualC.dll", - "$(TargetDir)System.Runtime.dll", - "$(TargetDir)System.Runtime.Extensions.dll", - "$(TargetDir)System.Runtime.Handles.dll", - "$(TargetDir)System.Runtime.InteropServices.dll", - "$(TargetDir)System.Runtime.InteropServices.RuntimeInformation.dll", - "$(TargetDir)System.Runtime.Numerics.dll", - "$(TargetDir)System.Runtime.Serialization.Formatters.dll", - "$(TargetDir)System.Runtime.Serialization.Json.dll", - "$(TargetDir)System.Runtime.Serialization.Primitives.dll", - "$(TargetDir)System.Runtime.Serialization.Xml.dll", - "$(TargetDir)System.Security.Claims.dll", - "$(TargetDir)System.Security.Cryptography.Algorithms.dll", - "$(TargetDir)System.Security.Cryptography.Csp.dll", - "$(TargetDir)System.Security.Cryptography.Encoding.dll", - "$(TargetDir)System.Security.Cryptography.Primitives.dll", - "$(TargetDir)System.Security.Cryptography.X509Certificates.dll", - "$(TargetDir)System.Security.Principal.dll", - "$(TargetDir)System.Security.SecureString.dll", - "$(TargetDir)System.Text.Encoding.CodePages.dll", - "$(TargetDir)System.Text.Encoding.dll", - "$(TargetDir)System.Text.Encoding.Extensions.dll", - "$(TargetDir)System.Text.RegularExpressions.dll", - "$(TargetDir)System.Threading.dll", - "$(TargetDir)System.Threading.Overlapped.dll", - "$(TargetDir)System.Threading.Tasks.dll", - "$(TargetDir)System.Threading.Tasks.Parallel.dll", - "$(TargetDir)System.Threading.Thread.dll", - "$(TargetDir)System.Threading.ThreadPool.dll", - "$(TargetDir)System.Threading.Timer.dll", - "$(TargetDir)System.ValueTuple.dll", - "$(TargetDir)System.Xml.ReaderWriter.dll", - "$(TargetDir)System.Xml.XDocument.dll", - "$(TargetDir)System.Xml.XmlDocument.dll", - "$(TargetDir)System.Xml.XmlSerializer.dll", - "$(TargetDir)System.Xml.XPath.dll", - "$(TargetDir)System.Xml.XPath.XDocument.dll" + "NPinyin.Core.dll", + "Pfim.dll", + "TechnologyAssembler.Core.dll" ] }, "Advanced": { - "AllowWildCards": true + "AllowDuplicateType": null, + "AllowMultipleAssemblyLevelAttributes": false, + "AllowWildCards": false, + "AllowZeroPeKind": false, + "AttributeFile": null, + "Closed": false, + "CopyAttributes": false, + "DebugInfo": true, + "DelaySign": false, + "DeleteCopiesOverwriteTarget": false, + "ExcludeFile": "", + "FileAlignment": 512, + "Internalize": false, + "Log": false, + "LogFile": null, + "PublicKeyTokens": true, + "SearchDirectories": [], + "TargetKind": null, + "UnionMerge": false, + "Version": null, + "XmlDocumentation": false } -} - +} \ No newline at end of file diff --git a/MainWindow.xaml b/MainWindow.xaml index 4051df8..b3204a8 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -5,7 +5,8 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:AnotherReplayReader" mc:Ignorable="d" - Title="MainWindow" Width="800" Height="600"> + Title="MainWindow" Width="800" Height="600" + Loaded="OnMainWindowLoaded"> @@ -71,7 +72,8 @@ - + diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 2be3209..4811415 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -1,6 +1,9 @@ -using Microsoft.Win32; +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; @@ -17,28 +20,14 @@ namespace AnotherReplayReader { 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"; - } - + 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"; @@ -50,23 +39,21 @@ namespace AnotherReplayReader ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods"); } - public event PropertyChangedEventHandler PropertyChanged; + 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 = "") { - var equals = Equals(target, newValue); - target = newValue; - if (!equals) + if (!Equals(target, newValue)) { + target = newValue; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } - } - public string RA3Directory { get; } + public string? RA3Directory { get; } public string RA3ReplayFolderPath { get; } public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe"); public string CustomMapsDirectory { get; } @@ -78,19 +65,19 @@ namespace AnotherReplayReader set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value); } - public string ReplayDetails + public string? ReplayDetails { get => _replayDetails; set => SetAndNotifyPropertyChanged(ref _replayDetails, value); } - public bool ReplaySelected => _currentReplay != null; + public bool ReplaySelected => CurrentReplay != null; - public bool ReplayPlayable => _currentReplay?.HasFooter == true && _currentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe); + public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe); - public bool ReplayDamaged => _currentReplay?.HasFooter == false; + public bool ReplayDamaged => CurrentReplay?.HasFooter == false; - public Replay CurrentReplay + public Replay? CurrentReplay { get => _currentReplay; set @@ -103,9 +90,9 @@ namespace AnotherReplayReader } } - private string _replayFolderPath; - private string _replayDetails; - private Replay _currentReplay; + private string _replayFolderPath = null!; + private string? _replayDetails; + private Replay? _currentReplay; } /// @@ -113,346 +100,236 @@ namespace AnotherReplayReader /// public partial class MainWindow : Window { - private readonly MainWindowProperties _properties = new MainWindowProperties(); - private readonly Cache _cache = new Cache(); + 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 List _replayList = new List(); - private List _pinyinList = new List(); - private CancellationTokenSource _loadReplaysToken; + 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() { - DataContext = _properties; - - 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}")); - }; - + _taskQueue = new(Dispatcher); _playerIdentity = new PlayerIdentity(_cache); - _minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory); - _minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory); + _minimapCache = new BigMinimapCache(_properties.RA3Directory); + _minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory); + _replayList = new(_playerIdentity); + + DataContext = _properties; InitializeComponent(); - Closing += (sender, eventArgs) => _cache.Save(); - - LoadReplays(); - Task.Run(() => AutoSaveReplays(Dispatcher, _properties.RA3ReplayFolderPath)); + Closing += (sender, eventArgs) => + { + _cache.Save(); + Application.Current.Shutdown(); + }; } - private static async Task AutoSaveReplays(Dispatcher dispatcher, string replayFolderPath) + private async void OnMainWindowLoaded(object sender, EventArgs e) { - 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(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); - } + Debug.Initialize(); + ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath); + var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); + await _taskQueue.Enqueue(() => LoadReplays(null, token), token); } - private async void LoadReplays(string nextSelected = null) + private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken) { - const string loadingString = "正在加载录像列表,请稍候… 已加载 {0} 个录像"; + if (!IsLoaded) + { + return; + } + cancelToken.ThrowIfCancellationRequested(); + var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken); + _cancelDisplayReplays.Reset(_cancelFilterReplays.Token); - 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; - } - if (_dataGrid != null) + _properties.CurrentReplay = null; + _image.Source = null; + if (_dataGrid.Items.Count > 0) { _dataGrid.ItemsSource = Array.Empty(); _dataGrid.Items.Refresh(); } - _replayList.Clear(); - _pinyinList.Clear(); + cancelToken.ThrowIfCancellationRequested(); + _replayList = new(_playerIdentity); + + var path = _properties.ReplayFolderPath; if (!Directory.Exists(path)) { - DisplayReplays("这个文件夹并不存在。", nextSelected); + await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken); return; } - var (newList, newPinyinList) = await Task.Run(() => + var result = await Task.Run(() => { var list = new List(); - var pinyinList = new List(); + var clock = new Stopwatch(); + clock.Start(); foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay")) { - if (cancelToken.IsCancellationRequested) - { - break; - } + cancelToken.ThrowIfCancellationRequested(); try { var replay = new Replay(replayPath); list.Add(replay); - pinyinList.Add(replay.ToPinyin(_playerIdentity)); } 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, pinyinList); - }); - - _replayList = newList; - _pinyinList = newPinyinList; - DisplayReplays(string.Empty, nextSelected); - } - - private void DisplayReplays(string message = null, string nextSelected = null, Replay[] filtered = null) - { - Dispatcher.Invoke(() => - { - filtered = filtered ?? _replayList.ToArray(); - _properties.CurrentReplay = null; - _dataGrid.ItemsSource = filtered; - _dataGrid.Items.Refresh(); - _properties.ReplayDetails = message; - - if (nextSelected != null) - { - for (var i = 0; i < _dataGrid.Items.Count; ++i) + if (clock.ElapsedMilliseconds > 300) { - if (_dataGrid.Items[i] is Replay replay && replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase)) - { - _dataGrid.SelectedIndex = i; - OnReplaySelectionChanged(null, null); - break; - } + 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 void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e) + private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken) { - LoadReplays(); + 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 void OnReplaySelectionChanged(object sender, EventArgs e) + private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken) { - _properties.CurrentReplay = _dataGrid.SelectedItem as Replay; - if (_properties.CurrentReplay == null) + 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; } - 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.FormatRealName(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"; - } + var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token); + _properties.ReplayDetails = replay.GetDetails(_playerIdentity); + await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token); } private void OnAboutButton_Click(object sender, RoutedEventArgs e) @@ -477,7 +354,6 @@ namespace AnotherReplayReader var fileName = openFileDialog.FileName; var directoryName = Path.GetDirectoryName(fileName); _properties.ReplayFolderPath = directoryName; - LoadReplays(fileName); } } catch (Exception exception) @@ -488,7 +364,7 @@ namespace AnotherReplayReader private void OnDetailsButton_Click(object sender, RoutedEventArgs e) { - var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity); + var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity); detailsWindow.ShowDialog(); } @@ -499,12 +375,12 @@ namespace AnotherReplayReader Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" "); } - private void OnFixReplayButton_Click(object sender, RoutedEventArgs e) + private async void OnFixReplayButton_Click(object sender, RoutedEventArgs e) { + var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay"); + try { - var replay = _properties.CurrentReplay; - var saveFileDialog = new SaveFileDialog { Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*", @@ -516,11 +392,9 @@ namespace AnotherReplayReader var result = saveFileDialog.ShowDialog(this); if (result == true) { - using (var file = saveFileDialog.OpenFile()) - using (var writer = new BinaryWriter(file)) - { - writer.WriteReplay(replay); - } + using var file = saveFileDialog.OpenFile(); + using var writer = new BinaryWriter(file); + writer.Write(replay); } } catch (Exception exception) @@ -528,42 +402,21 @@ namespace AnotherReplayReader MessageBox.Show($"无法修复录像:\r\n{exception}"); } - LoadReplays(); + var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); + await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token); } private async void ReplayFilterBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { - try - { - var pinyins = _replayFilterBox.Text.Split(',', ' ', ',') - .Select(x => x.ToPinyin()) - .Where(x => !string.IsNullOrEmpty(x)) - .ToArray(); - if (!pinyins.Any()) - { - DisplayReplays(); - return; - } - var tokenSource = _loadReplaysToken; - var token = tokenSource.Token; - var list = _pinyinList.ToArray(); - var result = await Task.Run(() => - { - var query = from replay in list.AsParallel() - where pinyins.Any(pinyin => replay.MatchPinyin(pinyin)) - select replay.Replay; - return query.ToArray(); - }); - if (token.IsCancellationRequested || _loadReplaysToken != tokenSource) - { - return; - } - DisplayReplays(null, null, result); - } - catch (Exception ex) - { - Debug.Instance.DebugMessage += $"Exception when filtering replays: {ex}"; - } + _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); } } } diff --git a/MinimapReader.cs b/MinimapReader.cs index 1adef12..b506334 100644 --- a/MinimapReader.cs +++ b/MinimapReader.cs @@ -1,13 +1,13 @@ -using System; +using AnotherReplayReader.ReplayFile; +using Pfim; +using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; using System.Threading.Tasks; using System.Windows.Media; using System.Windows.Media.Imaging; -using OpenSage.FileFormats.Big; -using Pfim; +using TechnologyAssembler.Core.IO; namespace AnotherReplayReader { @@ -24,104 +24,93 @@ namespace AnotherReplayReader }; private readonly BigMinimapCache _cache; - private readonly string _ra3InstallPath; private readonly string _mapFolderPath; private readonly string _modFolderPath; - public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath) + public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath) { _cache = cache; - _ra3InstallPath = ra3InstallPath; _mapFolderPath = mapFolderPath; _modFolderPath = modFolderPath; } - public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0) + public Task TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0) { - using (var targa = TryGetTarga(path, mod)) - { - if(targa == null) - { - return null; - } + var mapPath = replay.MapPath.TrimEnd('/'); + var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1); + var minimapPath = $"{mapPath}/{mapName}_art.tga"; + return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY)); + } - try - { - return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride); - } - catch (Exception exception) - { - Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n"; - return null; - } + public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0) + { + using var targa = TryGetTarga(path, mod); + if (targa == null) + { + return null; + } + + try + { + var bitmap = BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride); + bitmap.Freeze(); + return bitmap; + } + catch (Exception exception) + { + Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n"; + return null; } } - private Targa TryGetTarga(string path, Mod mod) + private Targa? TryGetTarga(string path, Mod mod) { - var tga = null as Targa; const string customMapPrefix = "data/maps/internal/"; if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix)) { var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length)); - if(File.Exists(minimapPath)) - { - return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig()); - } - } - - // now, normalize paths - path = path.Replace('/', '\\'); - - if(!mod.IsRA3) - { - try - { - var modSkudefPaths = Enumerable.Empty(); - foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath)) - { - modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef")); - } - - foreach (var modSkudefPath in modSkudefPaths) - { - var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath }); - foreach (var modBigPath in modBigPaths) - { - using (var modBig = new BigArchive(modBigPath)) - { - var entry = modBig.GetEntry(path); - if (entry != null) - { - return Targa.Create(entry.Open(), new PfimConfig()); - } - } - } - } - } - catch (Exception exception) - { - Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n"; - } - } - - if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big)) - { - using (big) - { - return Targa.Create(big.GetEntry(path).Open(), new PfimConfig()); - } - } - - if (Directory.Exists(_ra3InstallPath)) - { - var minimapPath = Path.Combine(_mapFolderPath, path); if (File.Exists(minimapPath)) { return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig()); } } + if (!mod.IsRA3) + { + var modSkudefPaths = Enumerable.Empty(); + foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath)) + { + modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef")); + } + + DronePlatform.BuildTechnologyAssembler(); + foreach (var modSkudefPath in modSkudefPaths) + { + try + { + using var fs = new SkuDefFileSystemProvider("modConfig", modSkudefPath); + if (!fs.FileExists(path)) + { + continue; + } + using var stream = fs.OpenStream(path, VirtualFileModeType.Open); + return Targa.Create(stream, new PfimConfig()); + } + catch (Exception exception) + { + Debug.Instance.DebugMessage += $"Exception when reading minimap from mod bigs:\r\n {exception}\r\n"; + } + } + } + + if (_cache != null && _cache.TryGetEntry(path, out var big)) + { + using (big) + { + return Targa.Create(big, new PfimConfig()); + } + } + return null; } } diff --git a/ModData.cs b/ModData.cs index 8762b4b..9ce0e0e 100644 --- a/ModData.cs +++ b/ModData.cs @@ -1,8 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; namespace AnotherReplayReader { @@ -42,13 +39,13 @@ namespace AnotherReplayReader public int CompareTo(object other) { - if(!(other is Mod)) + if (!(other is Mod)) { return GetType().FullName.CompareTo(other.GetType().FullName); } var otherMod = (Mod)other; - if(IsRA3 != otherMod.IsRA3) + if (IsRA3 != otherMod.IsRA3) { if (IsRA3) { @@ -84,20 +81,13 @@ namespace AnotherReplayReader internal static class ModData { - private static readonly IReadOnlyDictionary _ra3Factions; - private static readonly IReadOnlyDictionary _ra3ARFactions; - private static readonly IReadOnlyDictionary _ra3CoronaFactions; - private static readonly IReadOnlyDictionary _ra3DawnFactions; - private static readonly IReadOnlyDictionary _ra3INSFactions; - private static readonly IReadOnlyDictionary _ra3FSFactions; - private static readonly IReadOnlyDictionary _ra3EisenreichFactions; - private static readonly IReadOnlyDictionary _ra3TNWFactions; - private static readonly IReadOnlyDictionary _ra3WOPFactions; - private static readonly Faction _unknown; + private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营"); + private static readonly IReadOnlyDictionary> _factions; + static ModData() { - _ra3Factions = new Dictionary + var ra3Factions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -106,8 +96,8 @@ namespace AnotherReplayReader { 4, new Faction(FactionKind.Player, "盟军") }, { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, - }; - _ra3ARFactions = new Dictionary + }; + var arFactions = new Dictionary { { 1, new Faction(FactionKind.Player, "阳炎") }, { 2, new Faction(FactionKind.Player, "天琼") }, @@ -123,7 +113,7 @@ namespace AnotherReplayReader { 14, new Faction(FactionKind.Player, "涅墨西斯") }, { 0, new Faction(FactionKind.Player, "AI") }, }; - _ra3CoronaFactions = new Dictionary + var coronaFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -134,7 +124,7 @@ namespace AnotherReplayReader { 8, new Faction(FactionKind.Player, "苏联") }, { 9, new Faction(FactionKind.Player, "神州") }, }; - _ra3DawnFactions = new Dictionary + var dawnFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -147,7 +137,7 @@ namespace AnotherReplayReader { 11, new Faction(FactionKind.Player, "随机") }, { 12, new Faction(FactionKind.Player, "苏联") }, }; - _ra3INSFactions = new Dictionary + var insFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -157,7 +147,7 @@ namespace AnotherReplayReader { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, }; - _ra3FSFactions = new Dictionary + var fsFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -167,7 +157,7 @@ namespace AnotherReplayReader { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, }; - _ra3EisenreichFactions = new Dictionary + var eisenreichFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -177,7 +167,7 @@ namespace AnotherReplayReader { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, }; - _ra3TNWFactions = new Dictionary + var tnwFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -187,7 +177,7 @@ namespace AnotherReplayReader { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, }; - _ra3WOPFactions = new Dictionary + var wopFactions = new Dictionary { { 0, new Faction(FactionKind.Player, "AI") }, { 1, new Faction(FactionKind.Observer, "观察员") }, @@ -197,61 +187,31 @@ namespace AnotherReplayReader { 7, new Faction(FactionKind.Player, "随机") }, { 8, new Faction(FactionKind.Player, "苏联") }, }; - _unknown = new Faction(FactionKind.Unknown, "未知阵营"); + _factions = new Dictionary>(StringComparer.CurrentCultureIgnoreCase) + { + ["RA3"] = ra3Factions, + ["Armor Rush"] = arFactions, + ["ART"] = arFactions, + ["corona"] = coronaFactions, + ["Dawn"] = dawnFactions, + ["Insurrection"] = insFactions, + ["1.12+FS"] = fsFactions, + ["Eisenreich"] = eisenreichFactions, + ["The New World"] = tnwFactions, + ["War Of Powers"] = wopFactions + }; } - public static Faction GetFaction(Mod mod, int factionID) + public static Faction GetFaction(Mod mod, int factionId) { - new Faction(FactionKind.Player, mod.ModName + "-" + factionID); - - if - (mod.ModName.Equals("RA3")) + if (_factions.TryGetValue(mod.ModName, out var table)) { - return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown; + if (table.TryGetValue(factionId, out var result)) + { + return result; + } } - if (mod.ModName.Equals("Armor Rush")) - { - return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("ART")) - { - return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("corona")) - { - return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("Dawn")) - { - return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("Insurrection")) - { - return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("1.12+FS")) - { - return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("Eisenreich")) - { - return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("The New World")) - { - return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - if (mod.ModName.Equals("War Of Powers")) - { - return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown; - } - - - - - return _unknown; - } } diff --git a/PinyinExtensions.cs b/PinyinExtensions.cs deleted file mode 100644 index cc9a6af..0000000 --- a/PinyinExtensions.cs +++ /dev/null @@ -1,76 +0,0 @@ -using NPinyin; -using System; -using System.Collections.Generic; - -namespace AnotherReplayReader -{ - class PinyinReplayData - { - public Replay Replay { get; } - public string Name { get; } - public string Map { get; } - public string Mod { get; } - public List Players { get; } = new List(); - public List RealNames { get; } = new List(); - public List Factions { get; } = new List(); - - public PinyinReplayData(Replay replay, PlayerIdentity playerIdentity) - { - Replay = replay; - Name = replay.FileName.ToPinyin(); - Map = replay.MapName.ToPinyin(); - Mod = replay.Mod.ModName.ToPinyin(); - foreach (var player in replay.Players) - { - void AddIfNotEmpty(List target, string s) - { - s = s.ToPinyin(); - if (!string.IsNullOrEmpty(s)) - { - target.Add(s); - } - } - AddIfNotEmpty(Players, player.PlayerName); - AddIfNotEmpty(RealNames, playerIdentity.GetRealName(player.PlayerIP)); - AddIfNotEmpty(Factions, ModData.GetFaction(replay.Mod, player.FactionID).Name); - } - } - - public bool MatchPinyin(string pinyin) - { - return Name.ContainsIgnoreCase(pinyin) - || Players.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1 - || RealNames.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1 - || Map.ContainsIgnoreCase(pinyin) - || Mod.ContainsIgnoreCase(pinyin) - || Factions.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1; - } - } - - static class PinyinExtensions - { - public static bool ContainsIgnoreCase(this string self, string s) - { - return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1; - } - - public static string ToPinyin(this string self) - { - string pinyin; - try - { - pinyin = Pinyin.GetPinyin(self); - } - catch - { - return null; - } - return pinyin.Replace(" ", ""); - } - - public static PinyinReplayData ToPinyin(this Replay replay, PlayerIdentity playerIdentity) - { - return new PinyinReplayData(replay, playerIdentity); - } - } -} diff --git a/PlayerIdentity.cs b/PlayerIdentity.cs index d368fd3..b5cfba1 100644 --- a/PlayerIdentity.cs +++ b/PlayerIdentity.cs @@ -1,33 +1,38 @@ -using System; +using AnotherReplayReader.Utils; +using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; +using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using System.Web; -using System.Web.Script.Serialization; +using static System.Text.Json.JsonSerializer; namespace AnotherReplayReader { - internal sealed class IPAndPlayer + internal sealed class IpAndPlayer { public static string SimpleIPToString(uint ip) { return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}"; } - public uint IP + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public uint Ip { get => _ip; set { _ip = value; - IPString = SimpleIPToString(_ip); + IpString = SimpleIPToString(_ip); } } - public string IPString { get; private set; } - public string ID + public string IpString { get; private set; } = "0.0.0.0"; + public string Id { get => _id; set @@ -36,116 +41,88 @@ namespace AnotherReplayReader _pinyin = _id.ToPinyin(); } } - public string PinyinID => _pinyin; + public string? PinyinId => _pinyin; private uint _ip; - private string _id; - private string _pinyin; + private string _id = string.Empty; + private string? _pinyin; } internal class PlayerIdentity { - public bool IsUsable => IsListUsable(_list); + private const string StoredKey = "pt"; + private const string IvKey = "jw"; + private static readonly JsonSerializerOptions _jsonOptions = new() + { + PropertyNameCaseInsensitive = true + }; + private readonly object _lock = new(); + private readonly Cache _cache; + private IReadOnlyDictionary? _list; - private Cache _cache; - private volatile IReadOnlyDictionary _list; + public bool IsUsable => Lock.Run(_lock, () => _list is not null); public PlayerIdentity(Cache cache) { _cache = cache; + Task.Run(FetchLocal); Fetch(); - - try - { - var stored = _cache.GetOrDefault("pt", string.Empty); - if (string.IsNullOrWhiteSpace(stored)) - { - return; - } - - var bytes = Convert.FromBase64String(stored); - var id = Encoding.UTF8.GetBytes(Auth.ID); - var data = Encoding.UTF8.GetString(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray()); - var serializer = new JavaScriptSerializer(); - var cachedTable = serializer.Deserialize>(data); - if (cachedTable == null) - { - _list = null; - return; - } - var converted = cachedTable.ToDictionary(x => x.IP, x => x.ID); - converted[0] = "【没有网络连接,正在使用上次保存的数据】"; - - _list = converted; - } - catch { } - } - - public static bool IsListUsable(IReadOnlyDictionary list) - { - return list != null && list.Count != 0; } public Task Fetch() { - return Task.Run(() => + return Task.Run(async () => { try { var key = HttpUtility.UrlEncode(Auth.GetKey()); var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}"); - - using (var stream = request.GetResponse().GetResponseStream()) - using (var reader = new StreamReader(stream)) + using var response = await request.GetResponseAsync().ConfigureAwait(false); + using var stream = response.GetResponseStream(); + var list = await DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false); + var converted = list?.ToDictionary(x => x.Ip, x => x.Id); + lock (_lock) { - var response = reader.ReadToEnd(); - var serializer = new JavaScriptSerializer(); - var temp = serializer.Deserialize>(response); - if (temp == null) - { - _list = null; - return; - } - var converted = temp.ToDictionary(x => x.IP, x => x.ID); _list = converted; - - var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp)); - var id = Encoding.UTF8.GetBytes(Auth.ID); - var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray()); - _cache.Set("pt", base64); } + + if (list is null) + { + _cache.Set(StoredKey, null); + return; + } + + using var aes = Aes.Create(); + using var encryptor = aes.CreateEncryptor(Encoding.UTF8.GetBytes(Auth.ID), aes.IV); + using var memory = new MemoryStream(); + using var decryptorStream = new CryptoStream(memory, encryptor, CryptoStreamMode.Write); + await SerializeAsync(decryptorStream, list, _jsonOptions).ConfigureAwait(false); + memory.Flush(); + _cache.SetValues((StoredKey, Convert.ToBase64String(memory.ToArray())), (IvKey, Convert.ToBase64String(aes.IV))); } catch { } }); } - public List AsSortedList() + public List AsSortedList() { - var list = _list; + using var locker = new Lock(_lock); - if (!IsListUsable(list)) - { - return new List(); - } - - return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList(); + return _list? + .Select((kv) => new IpAndPlayer { Ip = kv.Key, Id = kv.Value }) + .OrderBy(x => x.Ip) + .ToList() ?? new(0); } public string GetRealName(uint ip) { - var list = _list; - - if (!IsListUsable(list)) - { - return string.Empty; - } - if (ip == 0) { return string.Empty; } - if (!list.TryGetValue(ip, out var name)) + using var locker = new Lock(_lock); + if (_list is null || !_list.TryGetValue(ip, out var name)) { return string.Empty; } @@ -158,27 +135,50 @@ namespace AnotherReplayReader var name = GetRealName(ip); if (string.IsNullOrEmpty(name)) { - name = IPAndPlayer.SimpleIPToString(ip); + name = IpAndPlayer.SimpleIPToString(ip); } return $"({name})"; } public string QueryRealNameAndIP(uint ip) { - var list = _list; - - if (!IsListUsable(list)) + var ipText = IpAndPlayer.SimpleIPToString(ip); + var name = GetRealName(ip); + if (string.IsNullOrEmpty(name)) { - return string.Empty; + return ipText; } + return $"{name},{ipText}"; + } - if (ip == 0) + private async Task FetchLocal() + { + try { - return string.Empty; - } + var stored = _cache.GetOrDefault(StoredKey, string.Empty); + var iv = Convert.FromBase64String(_cache.GetOrDefault(IvKey, string.Empty)); + if (string.IsNullOrWhiteSpace(stored)) + { + return; + } - var name = list.TryGetValue(ip, out var realName) ? realName + "," : string.Empty; - return name + IPAndPlayer.SimpleIPToString(ip); + using var aes = Aes.Create(); + using var decryptor = aes.CreateDecryptor(Encoding.UTF8.GetBytes(Auth.ID), iv); + using var memory = new MemoryStream(Convert.FromBase64String(stored)); + using var decryptorStream = new CryptoStream(memory, decryptor, CryptoStreamMode.Read); + var cachedTable = await DeserializeAsync>(decryptorStream, _jsonOptions).ConfigureAwait(false); + if (cachedTable is null) + { + return; + } + var converted = cachedTable.ToDictionary(x => x.Ip, x => x.Id); + converted[0] = "【没有网络连接,正在使用上次保存的数据】"; + lock (_lock) + { + _list ??= converted; + } + } + catch { } } } } diff --git a/Replay.cs b/Replay.cs deleted file mode 100644 index 4a04cee..0000000 --- a/Replay.cs +++ /dev/null @@ -1,433 +0,0 @@ -using System; -using System.IO; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace AnotherReplayReader -{ - internal sealed class Player - { - public static readonly IReadOnlyDictionary ComputerNames = new Dictionary - { - { "E", "简单" }, - { "M", "中等" }, - { "H", "困难" }, - { "B", "凶残" }, - }; - - public string PlayerName { get; private set; } - public uint PlayerIP { get; private set; } - public int FactionID { get; private set; } - public int Team { get; private set; } - - public Player(string[] playerEntry) - { - var isComputer = playerEntry[0][0] == 'C'; - - PlayerName = playerEntry[0].Substring(1); - - if (isComputer) - { - PlayerName = ComputerNames[PlayerName]; - PlayerIP = 0; - FactionID = int.Parse(playerEntry[2]); - Team = int.Parse(playerEntry[4]); - } - else - { - PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber); - FactionID = int.Parse(playerEntry[5]); - Team = int.Parse(playerEntry[7]); - } - - - } - } - - internal enum ReplayType - { - Skirmish, - Lan, - Online - } - - internal sealed class ReplayChunk - { - public uint TimeCode { get; private set; } - public byte Type { get; private set; } - public byte[] Data { get; private set; } - - public ReplayChunk(uint timeCode, BinaryReader reader) - { - TimeCode = timeCode; // reader.ReadUInt32(); - Type = reader.ReadByte(); - var chunkSize = reader.ReadInt32(); - Data = reader.ReadBytes(chunkSize); - if(reader.ReadInt32() != 0) - { - throw new InvalidDataException("Replay Chunk not ended with zero"); - } - } - } - - internal enum ReplayFooterOption - { - SeekToFooter, - CurrentlyAtFooter, - } - - internal sealed class ReplayFooter - { - public const uint Terminator = 0x7FFFFFFF; - public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER"); - public uint FinalTimeCode { get; private set; } - public byte[] Data { get; private set; } - - public ReplayFooter(BinaryReader reader, ReplayFooterOption option) - { - var currentPosition = reader.BaseStream.Position; - reader.BaseStream.Seek(-4, SeekOrigin.End); - var footerLength = reader.ReadInt32(); - - if (option == ReplayFooterOption.SeekToFooter) - { - currentPosition = reader.BaseStream.Length - footerLength; - } - - reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin); - var footer = reader.ReadBytes(footerLength); - - if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length) - { - throw new InvalidDataException("Invalid footer"); - } - - using (var footerStream = new MemoryStream(footer)) - using (var footerReader = new BinaryReader(footerStream)) - { - var footerString = footerReader.ReadBytes(17); - if(!footerString.SequenceEqual(FooterString)) - { - throw new InvalidDataException("Invalid footer, no footer string"); - } - FinalTimeCode = footerReader.ReadUInt32(); - Data = footerReader.ReadBytes(footer.Length - 25); - if(footerReader.ReadInt32() != footerLength) - { - throw new InvalidDataException(); - } - } - } - - public ReplayFooter(uint finalTimeCode) - { - FinalTimeCode = finalTimeCode; - Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 }; - } - - List TryGetKillDeathRatio() - { - if(Data.Length < 24) - { - return null; - } - - var ratios = new List(); - using (var stream = new MemoryStream(Data, Data.Length - 24, 24)) - using (var reader = new BinaryReader(stream)) - { - ratios.Add(reader.ReadSingle()); - } - - return ratios; - } - } - - internal static class ReplayExtensions - { - public static string ReadUTF16String(this BinaryReader reader) - { - var currentBytes = new List(); - byte[] lastTwoBytes = null; - while(true) - { - lastTwoBytes = reader.ReadBytes(2); - if (lastTwoBytes.Length != 2) - { - throw new InvalidDataException(); - } - if(lastTwoBytes.All(x => x == 0)) - { - break; - } - currentBytes.AddRange(lastTwoBytes); - } - - return Encoding.Unicode.GetString(currentBytes.ToArray()); - } - - public static void WriteChunk(this BinaryWriter writer, ReplayChunk chunk) - { - writer.Write(chunk.TimeCode); - writer.Write(chunk.Type); - writer.Write(chunk.Data.Length); - writer.Write(chunk.Data); - writer.Write(0); - } - - public static void WriteFooter(this BinaryWriter writer, ReplayFooter footer) - { - writer.Write(ReplayFooter.Terminator); - writer.Write(ReplayFooter.FooterString); - writer.Write(footer.FinalTimeCode); - writer.Write(footer.Data); - writer.Write(ReplayFooter.FooterString.Length + footer.Data.Length + 8); - } - - public static void WriteReplay(this BinaryWriter writer, Replay replay) - { - writer.Write(replay.RawHeader); - - var lastTimeCode = (uint)0; - foreach(var chunk in replay.Body) - { - lastTimeCode = chunk.TimeCode; - writer.WriteChunk(chunk); - } - - if (replay.HasFooter) - { - writer.WriteFooter(replay.Footer); - } - else - { - writer.WriteFooter(new ReplayFooter(lastTimeCode)); - } - } - } - - internal sealed class Replay - { - public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER"); - public static readonly Dictionary TypeStrings = new Dictionary() - { - { ReplayType.Skirmish, "遭遇战录像" }, - { ReplayType.Lan, "局域网录像" }, - { ReplayType.Online, "官网录像" }, - }; - - public string Path { get; private set; } - public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path); - public DateTime Date { get; private set; } - public bool HasFooter => Footer != null; - public TimeSpan? Length { get; private set; } - public string MapName { get; private set; } - public string MapPath { get; private set; } - public IReadOnlyList Players => _players; - public int NumberOfPlayingPlayers { get; private set; } - public long Size { get; private set; } - public Mod Mod { get; private set; } - public ReplayType Type { get; private set; } - public string TypeString => TypeStrings[Type]; - public bool HasCommentator { get; private set; } - public Player ReplaySaver => Players[_replaySaverIndex]; - - public byte[] RawHeader { get; private set; } - public IReadOnlyList Body => _body; - public ReplayFooter Footer { get; private set; } - - private List _players; - private byte _replaySaverIndex; - private long _headerSize; - private List _body; - - public Replay(string path) - { - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read)) - { - Parse(path, stream); - } - } - - public Replay(string path, Stream stream) - { - Parse(path, stream); - } - - private void Parse(string path, Stream stream) - { - Path = path; - - using (var reader = new BinaryReader(stream)) - { - Size = reader.BaseStream.Length; - var headerMagic = reader.ReadBytes(HeaderMagic.Length); - if(!headerMagic.SequenceEqual(HeaderMagic)) - { - throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}"); - } - - var isSkirmish = reader.ReadByte() == 0x04; - reader.ReadBytes(4 * 4); // version and builds - reader.ReadBytes(2); // commentary flag, and padding zero byte - - reader.ReadUTF16String(); // title - reader.ReadUTF16String(); // description - MapName = reader.ReadUTF16String(); // map name - reader.ReadUTF16String(); // map id - - NumberOfPlayingPlayers = reader.ReadByte(); - - for(var i = 0; i <= NumberOfPlayingPlayers; ++i) - { - reader.ReadUInt32(); - reader.ReadUTF16String(); // utf16 player name - if(!isSkirmish) - { - reader.ReadByte(); // team - } - } - - var offset = reader.ReadInt32(); - var cnc3MagicLength = reader.ReadInt32(); - _headerSize = reader.BaseStream.Position + offset; - reader.ReadBytes(cnc3MagicLength); - - var modInfo = reader.ReadBytes(22); - Mod = new Mod(Encoding.UTF8.GetString(modInfo)); - - var timeStamp = reader.ReadUInt32(); - Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime; - - reader.ReadBytes(31); - var descriptionsLength = reader.ReadInt32(); - var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength)); - - var entries = null as Dictionary; - try - { - var query = from splitted in description.Split(';') - where !string.IsNullOrWhiteSpace(splitted) - select splitted.Split(new[] { '=' }, 2); - entries = query.ToDictionary(x => x[0], x => x[1]); - } - catch(Exception exception) - { - throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}"); - } - - try - { - _players = entries["S"].Split(':') - .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X') - .Select(x => new Player(x.Split(','))) - .ToList(); - } - catch (Exception exception) - { - throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}"); - } - - MapPath = entries["M"].Substring(3); - - HasCommentator = !entries["PC"].Equals("-1"); - - var lanFlag = int.Parse(entries["GT"]) == 0; - if (lanFlag) - { - if(_players.First().PlayerIP == 0) - { - Type = ReplayType.Skirmish; - } - else - { - Type = ReplayType.Lan; - } - } - else - { - Type = ReplayType.Online; - } - - _replaySaverIndex = reader.ReadByte(); - - reader.ReadBytes(8); // 8 bit paddings - var fileNameLength = reader.ReadInt32(); - reader.ReadBytes(fileNameLength * 2); - reader.ReadBytes(16); - var verMagicLength = reader.ReadInt32(); - reader.ReadBytes(verMagicLength); - reader.ReadBytes(85); - - if(reader.BaseStream.Position != _headerSize) - { - throw new InvalidDataException(); - } - - reader.BaseStream.Seek(-4, SeekOrigin.End); - try - { - Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter); - Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0)); - } - catch(Exception) - { - Length = null; - Footer = null; - } - } - } - - public void ParseBody() - { - _body = new List(); - - using (var stream = new FileStream(Path, FileMode.Open)) - using (var reader = new BinaryReader(stream)) - { - RawHeader = reader.ReadBytes((int)_headerSize); - while (true) - { - var timeCode = reader.ReadUInt32(); - if (timeCode == ReplayFooter.Terminator) - { - Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter); - break; - } - - _body.Add(new ReplayChunk(timeCode, reader)); - } - } - } - - public Dictionary GetCommandCounts() - { - var playerCommands = new Dictionary(); - - foreach(var chunk in _body) - { - if(chunk.Type != 1) - { - continue; - } - - foreach(var command in CommandChunk.Parse(chunk)) - { - var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count]; - if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command? - { - commandCount = commandCount - .Concat(new int[command.PlayerIndex - commandCount.Length + 1]) - .ToArray(); - } - commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1; - playerCommands[command.CommandID] = commandCount; - } - } - - return playerCommands; - } - } -} diff --git a/ReplayAutoSaver.cs b/ReplayAutoSaver.cs new file mode 100644 index 0000000..5f4d4bf --- /dev/null +++ b/ReplayAutoSaver.cs @@ -0,0 +1,148 @@ +using AnotherReplayReader.ReplayFile; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace AnotherReplayReader +{ + public static class ReplayAutoSaver + { + private static int _errorMessageCount = 0; + + public static void SpawnAutoSaveReplaysTask(string replayFolderPath) + { + Task.Run(() => AutoSaveReplays(replayFolderPath)); + } + + private static async Task AutoSaveReplays(string replayFolderPath) + { + const string ourPrefix = "自动保存"; + + // 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(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) + { + _ = Application.Current.Dispatcher.InvokeAsync(() => + { + try + { + MessageBox.Show(errorString); + } + finally + { + Interlocked.Decrement(ref _errorMessageCount); + } + }); + } + } + + await Task.Delay(10 * 1000).ConfigureAwait(false); + } + } + } +} diff --git a/CommandChunk.cs b/ReplayFile/CommandChunk.cs similarity index 77% rename from CommandChunk.cs rename to ReplayFile/CommandChunk.cs index 8a58dee..2a6d0e5 100644 --- a/CommandChunk.cs +++ b/ReplayFile/CommandChunk.cs @@ -2,20 +2,13 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace AnotherReplayReader +namespace AnotherReplayReader.ReplayFile { internal static class RA3Commands { - //public static IReadOnlyDictionary> CommandParser { get; private set; } - - public static IReadOnlyDictionary> CommandParser => _commandParser; - public static IReadOnlyDictionary CommandNames => _commandNames; - - private static Dictionary> _commandParser; - private static Dictionary _commandNames; + private static readonly Dictionary> _commandParser; + private static readonly Dictionary _commandNames; static RA3Commands() { @@ -31,13 +24,13 @@ namespace AnotherReplayReader }; } - Action variableSizeParser (byte command, int offset) + Action variableSizeParser(byte command, int offset) { return (BinaryReader current) => { var totalBytes = 2; totalBytes += current.ReadBytes(offset - 2).Length; - for(var x = current.ReadByte(); x != 0xFF; x = current.ReadByte()) + for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte()) { totalBytes += 1; @@ -107,7 +100,7 @@ namespace AnotherReplayReader (0x02, ParseSetRallyPoint0x02, "设计集结点"), (0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"), (0x10, ParseGarrison0x10, "进驻建筑"), - (0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"), + (0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"), (0x4B, ParsePlaceBeacon0x4B, "信标") }; @@ -129,9 +122,26 @@ namespace AnotherReplayReader _commandNames.Add(0x4D, "在信标里输入文字"); } - public static void UnknownCommandParser(BinaryReader current, byte commandID) + public static void ReadCommandData(this BinaryReader reader, byte commandId) { - while(true) + if (_commandParser.TryGetValue(commandId, out var parser)) + { + _commandParser[commandId](reader); + } + else + { + UnknownCommandParser(reader, commandId); + } + } + + public static string GetCommandName(byte commandId) + { + return _commandNames.TryGetValue(commandId, out var storedName) ? storedName : $"(未知指令 0x{commandId:2X})"; + } + + public static void UnknownCommandParser(BinaryReader current, byte commandId) + { + while (true) { var value = current.ReadByte(); if (value == 0xFF) @@ -143,11 +153,6 @@ namespace AnotherReplayReader //return $"(未知指令 0x{commandID:2X})"; } - public static string GetCommandName(byte commandID) - { - return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 0x{commandID:2X})"; - } - private static void ParseSpecialChunk0x01(BinaryReader current) { var firstByte = current.ReadByte(); @@ -157,7 +162,7 @@ namespace AnotherReplayReader } var sixthByte = current.ReadBytes(5).Last(); - if(sixthByte == 0xFF) + if (sixthByte == 0xFF) { return; } @@ -165,7 +170,7 @@ namespace AnotherReplayReader var sixteenthByte = current.ReadBytes(10).Last(); var size = (int)(sixteenthByte + 1) * 4 + 14; var lastByte = current.ReadBytes(size).Last(); - if(lastByte != 0xFF) + if (lastByte != 0xFF) { throw new InvalidDataException(); } @@ -198,23 +203,23 @@ namespace AnotherReplayReader { var type = current.ReadByte(); var size = -1; - if(type == 0x14) + if (type == 0x14) { size = 9; } - else if(type == 0x04) + else if (type == 0x04) { size = 10; } var lastByte = current.ReadBytes(size).Last(); - if(lastByte != 0xFF) + if (lastByte != 0xFF) { throw new InvalidDataException(); } } - private static void ParseUUID0x33(BinaryReader current) + private static void ParseUuid0x33(BinaryReader current) { current.ReadByte(); var firstStringLength = (int)current.ReadByte(); @@ -231,11 +236,11 @@ namespace AnotherReplayReader { var type = current.ReadByte(); var size = -1; - if(type == 0x04) + if (type == 0x04) { size = 5; } - else if(type == 0x07) + else if (type == 0x07) { size = 13; } @@ -250,7 +255,7 @@ namespace AnotherReplayReader internal sealed class CommandChunk { - public byte CommandID { get; private set; } + public byte CommandId { get; private set; } public int PlayerIndex { get; private set; } public static List Parse(in ReplayChunk chunk) @@ -260,44 +265,37 @@ namespace AnotherReplayReader throw new NotImplementedException(); } - int ManglePlayerID(byte ID) + static int ManglePlayerId(byte id) { - return ID / 8 - 2; + return id / 8 - 2; } - using (var stream = new MemoryStream(chunk.Data)) - using (var reader = new BinaryReader(stream)) + using var reader = chunk.GetReader(); + if (reader.ReadByte() != 1) { - if(reader.ReadByte() != 1) - { - throw new InvalidDataException("Payload first byte not 1"); - } - - var list = new List(); - var numberOfCommands = reader.ReadInt32(); - for(var i = 0; i < numberOfCommands; ++i) - { - var commandID = reader.ReadByte(); - var playerID = reader.ReadByte(); - if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser)) - { - RA3Commands.CommandParser[commandID](reader); - } - else - { - RA3Commands.UnknownCommandParser(reader, commandID); - } - - list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) }); - } - - if(reader.BaseStream.Position != reader.BaseStream.Length) - { - throw new InvalidDataException("Payload not fully parsed"); - } - - return list; + throw new InvalidDataException("Payload first byte not 1"); } + + var list = new List(); + var numberOfCommands = reader.ReadInt32(); + for (var i = 0; i < numberOfCommands; ++i) + { + var commandId = reader.ReadByte(); + var playerId = reader.ReadByte(); + reader.ReadCommandData(commandId); + list.Add(new CommandChunk + { + CommandId = commandId, + PlayerIndex = ManglePlayerId(playerId) + }); + } + + if (reader.BaseStream.Position != reader.BaseStream.Length) + { + throw new InvalidDataException("Payload not fully parsed"); + } + + return list; } } } diff --git a/ReplayFile/Player.cs b/ReplayFile/Player.cs new file mode 100644 index 0000000..a4f0dc5 --- /dev/null +++ b/ReplayFile/Player.cs @@ -0,0 +1,41 @@ +using System.Collections.Generic; + +namespace AnotherReplayReader.ReplayFile +{ + internal sealed class Player + { + public static readonly IReadOnlyDictionary ComputerNames = new Dictionary + { + { "E", "简单" }, + { "M", "中等" }, + { "H", "困难" }, + { "B", "凶残" }, + }; + + public string PlayerName { get; } + public uint PlayerIp { get; } + public int FactionId { get; } + public int Team { get; } + + public Player(string[] playerEntry) + { + var isComputer = playerEntry[0][0] == 'C'; + + PlayerName = playerEntry[0].Substring(1); + + if (isComputer) + { + PlayerName = ComputerNames[PlayerName]; + PlayerIp = 0; + FactionId = int.Parse(playerEntry[2]); + Team = int.Parse(playerEntry[4]); + } + else + { + PlayerIp = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber); + FactionId = int.Parse(playerEntry[5]); + Team = int.Parse(playerEntry[7]); + } + } + } +} diff --git a/ReplayFile/Replay.cs b/ReplayFile/Replay.cs new file mode 100644 index 0000000..b360ae3 --- /dev/null +++ b/ReplayFile/Replay.cs @@ -0,0 +1,346 @@ +using AnotherReplayReader.Utils; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AnotherReplayReader.ReplayFile +{ + internal enum ReplayType + { + Skirmish, + Lan, + Online + } + + internal sealed class Replay + { + public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER"); + public static readonly Dictionary TypeStrings = new() + { + { ReplayType.Skirmish, "遭遇战录像" }, + { ReplayType.Lan, "局域网录像" }, + { ReplayType.Online, "官网录像" }, + }; + + private readonly byte _replaySaverIndex; + private readonly byte[]? _rawHeader; + + public string Path { get; } + public DateTime Date { get; } + public string MapName { get; } + public string MapPath { get; } + public IReadOnlyList Players { get; } + public int NumberOfPlayingPlayers { get; } + public Mod Mod { get; } + public ReplayType Type { get; } + public bool HasCommentator { get; } + + public long Size { get; } + public ReplayFooter? Footer { get; } + public IReadOnlyList? Body { get; } + + public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path); + public Player ReplaySaver => Players[_replaySaverIndex]; + public string TypeString => TypeStrings[Type]; + public bool HasFooter => Footer != null; + public ShortTimeSpan? Length => Footer?.ReplayLength; + + public Replay(string path, bool parseBody = false) : + this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody) + { + } + + public Replay(string path, Stream stream, bool parseBody = false) + { + Path = path; + + using var reader = new BinaryReader(stream); + Size = reader.BaseStream.Length; + + var headerMagic = reader.ReadBytes(HeaderMagic.Length); + if (!headerMagic.SequenceEqual(HeaderMagic)) + { + throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}"); + } + + var isSkirmish = reader.ReadByte() == 0x04; + reader.ReadBytes(4 * 4); // version and builds + reader.ReadBytes(2); // commentary flag, and padding zero byte + + reader.ReadUTF16String(); // title + reader.ReadUTF16String(); // description + MapName = reader.ReadUTF16String(); // map name + reader.ReadUTF16String(); // map id + + NumberOfPlayingPlayers = reader.ReadByte(); + + for (var i = 0; i <= NumberOfPlayingPlayers; ++i) + { + reader.ReadUInt32(); + reader.ReadUTF16String(); // utf16 player name + if (!isSkirmish) + { + reader.ReadByte(); // team + } + } + + var offset = reader.ReadInt32(); + var cnc3MagicLength = reader.ReadInt32(); + var headerSize = checked((int)(reader.BaseStream.Position + offset)); + reader.ReadBytes(cnc3MagicLength); + + var modInfo = reader.ReadBytes(22); + Mod = new Mod(Encoding.UTF8.GetString(modInfo)); + + var timeStamp = reader.ReadUInt32(); + Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime; + + reader.ReadBytes(31); + var descriptionsLength = reader.ReadInt32(); + var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength)); + + var entries = null as Dictionary; + try + { + var query = from splitted in description.Split(';') + where !string.IsNullOrWhiteSpace(splitted) + select splitted.Split(new[] { '=' }, 2); + entries = query.ToDictionary(x => x[0], x => x[1]); + } + catch (Exception e) + { + throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}"); + } + + try + { + Players = entries["S"].Split(':') + .TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X') + .Select(x => new Player(x.Split(','))) + .ToList(); + } + catch (Exception exception) + { + throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}"); + } + + MapPath = entries["M"].Substring(3); + + HasCommentator = !entries["PC"].Equals("-1"); + + var lanFlag = int.Parse(entries["GT"]) == 0; + if (lanFlag) + { + if (Players.First().PlayerIp == 0) + { + Type = ReplayType.Skirmish; + } + else + { + Type = ReplayType.Lan; + } + } + else + { + Type = ReplayType.Online; + } + + _replaySaverIndex = reader.ReadByte(); + + reader.ReadBytes(8); // 8 bit paddings + var fileNameLength = reader.ReadInt32(); + reader.ReadBytes(fileNameLength * 2); + reader.ReadBytes(16); + var verMagicLength = reader.ReadInt32(); + reader.ReadBytes(verMagicLength); + reader.ReadBytes(85); + + if (reader.BaseStream.Position != headerSize) + { + Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n"; + return; + } + + if (!parseBody) + { + // jump to footer directly + reader.BaseStream.Seek(-4, SeekOrigin.End); + try + { + Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter); + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n"; + Footer = null; + } + + return; + } + + var body = new List(); + try + { + while (true) + { + var timeCode = reader.ReadUInt32(); + if (timeCode == ReplayFooter.Terminator) + { + Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter); + break; + } + + body.Add(new ReplayChunk(timeCode, reader)); + } + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n"; + } + + byte[]? rawHeader = null; + try + { + // 重新读取原来的整个录像头 + reader.BaseStream.Seek(0, SeekOrigin.Begin); + rawHeader = reader.ReadBytes(headerSize); + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n"; + return; + } + if (rawHeader.Length != headerSize) + { + Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n"; + return; + } + + _rawHeader = rawHeader; + Body = body; + } + + public Dictionary GetCommandCounts() + { + if (Body is null) + { + throw new InvalidOperationException("Replay body must be parsed before retrieving command chunks"); + } + + var playerCommands = new Dictionary(); + + foreach (var chunk in Body) + { + if (chunk.Type != 1) + { + continue; + } + + foreach (var command in CommandChunk.Parse(chunk)) + { + var commandCount = playerCommands.TryGetValue(command.CommandId, out var current) ? current : new int[Players.Count]; + if (command.PlayerIndex >= commandCount.Length) // unknown or unparsable command? + { + commandCount = commandCount + .Concat(new int[command.PlayerIndex - commandCount.Length + 1]) + .ToArray(); + } + commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1; + playerCommands[command.CommandId] = commandCount; + } + } + + return playerCommands; + } + + public Replay CloneHeader() + { + using var stream = new MemoryStream(); + using (var writer = new BinaryWriter(stream, Encoding.UTF8, true)) + { + WriteTo(writer); + } + stream.Position = 0; + return new Replay(Path, stream); + } + + public bool PathEquals(Replay replay) => PathEquals(replay.Path); + public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase); + + public string GetDetails(PlayerIdentity playerIdentity) + { + static string GetSizeString(double size) + { + if (size > 1024 * 1024) + { + return $"{Math.Round(size / (1024 * 1024), 2)}MB"; + } + return $"{Math.Round(size / 1024)}KB"; + } + + string size = GetSizeString(Size); + string length = Length?.ToString() ?? "录像已损坏,请先修复录像"; + + var replaySaver = _replaySaverIndex < Players.Count + ? ReplaySaver.PlayerName + : "[无法获取保存录像的玩家]"; + + using var writer = new StringWriter(); + writer.WriteLine("文件名:{0}", FileName); + writer.WriteLine("大小:{0}", size); + writer.WriteLine("地图:{0}", MapName); + writer.WriteLine("日期:{0}", Date); + writer.WriteLine("长度:{0}", length); + writer.WriteLine("录像类别:{0}", TypeString); + writer.WriteLine("这个文件是{0}保存的", replaySaver); + writer.WriteLine("玩家列表:"); + foreach (var player in Players) + { + if (player == Players.Last() && player.PlayerName.Equals("post Commentator")) + { + break; + } + var factionName = ModData.GetFaction(Mod, player.FactionId).Name; + var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty; + writer.WriteLine($"{player.PlayerName + realName},{factionName}"); + } + + return writer.ToString(); + } + + public void WriteTo(BinaryWriter writer) => WriteTo(writer, false); + + private void WriteTo(BinaryWriter writer, bool skipBody) + { + if ((_rawHeader is null || Body is null) && !skipBody) + { + throw new InvalidOperationException("Replay body must be parsed before writing replay"); + } + + writer.Write(_rawHeader); + + var lastTimeCode = Footer?.FinalTimeCode; + if (Body is not null) + { + foreach (var chunk in Body) + { + lastTimeCode = chunk.TimeCode; + writer.Write(chunk); + } + } + + if (Footer is not null) + { + writer.Write(Footer); + } + else if (lastTimeCode is uint lastTimeCodeValue) + { + writer.Write(new ReplayFooter(lastTimeCodeValue)); + } + } + } + + +} diff --git a/ReplayFile/ReplayChunk.cs b/ReplayFile/ReplayChunk.cs new file mode 100644 index 0000000..b4943f1 --- /dev/null +++ b/ReplayFile/ReplayChunk.cs @@ -0,0 +1,37 @@ +using System.IO; + +namespace AnotherReplayReader.ReplayFile +{ + internal sealed class ReplayChunk + { + private readonly byte[] _data; + + public uint TimeCode { get; } + public byte Type { get; } + + public ReplayChunk(uint timeCode, BinaryReader reader) + { + TimeCode = timeCode; // reader.ReadUInt32(); + Type = reader.ReadByte(); + var chunkSize = reader.ReadInt32(); + _data = reader.ReadBytes(chunkSize); + if (reader.ReadInt32() != 0) + { + throw new InvalidDataException("Replay Chunk not ended with zero"); + } + } + + public BinaryReader GetReader() => new(new MemoryStream(_data, false)); + + public void WriteTo(BinaryWriter writer) + { + writer.Write(TimeCode); + writer.Write(Type); + writer.Write(_data.Length); + writer.Write(_data); + writer.Write(0); + } + } + + +} diff --git a/ReplayFile/ReplayExtensions.cs b/ReplayFile/ReplayExtensions.cs new file mode 100644 index 0000000..38b0589 --- /dev/null +++ b/ReplayFile/ReplayExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AnotherReplayReader.ReplayFile +{ + internal static class ReplayExtensions + { + public static string ReadUTF16String(this BinaryReader reader) + { + var currentBytes = new List(); + var lastTwoBytes = Array.Empty(); + while (true) + { + lastTwoBytes = reader.ReadBytes(2); + if (lastTwoBytes.Length != 2) + { + throw new InvalidDataException(); + } + if (lastTwoBytes.All(x => x == 0)) + { + break; + } + currentBytes.AddRange(lastTwoBytes); + } + + return Encoding.Unicode.GetString(currentBytes.ToArray()); + } + + public static void Write(this BinaryWriter writer, ReplayChunk chunk) => chunk.WriteTo(writer); + + public static void Write(this BinaryWriter writer, ReplayFooter footer) => footer.WriteTo(writer); + + public static void Write(this BinaryWriter writer, Replay replay) => replay.WriteTo(writer); + } + + +} diff --git a/ReplayFile/ReplayFooter.cs b/ReplayFile/ReplayFooter.cs new file mode 100644 index 0000000..1b3a637 --- /dev/null +++ b/ReplayFile/ReplayFooter.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace AnotherReplayReader.ReplayFile +{ + internal enum ReplayFooterOption + { + SeekToFooter, + CurrentlyAtFooter, + } + + internal sealed class ReplayFooter + { + public const uint Terminator = 0x7FFFFFFF; + public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER"); + + private readonly byte[] _data; + + public uint FinalTimeCode { get; } + public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / 15.0); + + public ReplayFooter(BinaryReader reader, ReplayFooterOption option) + { + var currentPosition = reader.BaseStream.Position; + reader.BaseStream.Seek(-4, SeekOrigin.End); + var footerLength = reader.ReadInt32(); + + if (option == ReplayFooterOption.SeekToFooter) + { + currentPosition = reader.BaseStream.Length - footerLength; + } + + reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin); + var footer = reader.ReadBytes(footerLength); + + if (footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length) + { + throw new InvalidDataException("Invalid footer"); + } + + using var footerStream = new MemoryStream(footer); + using var footerReader = new BinaryReader(footerStream); + var footerString = footerReader.ReadBytes(17); + if (!footerString.SequenceEqual(FooterString)) + { + throw new InvalidDataException("Invalid footer, no footer string"); + } + FinalTimeCode = footerReader.ReadUInt32(); + _data = footerReader.ReadBytes(footer.Length - 25); + if (footerReader.ReadInt32() != footerLength) + { + throw new InvalidDataException(); + } + } + + public ReplayFooter(uint finalTimeCode) + { + FinalTimeCode = finalTimeCode; + _data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 }; + } + + List? TryGetKillDeathRatio() + { + if (_data.Length < 24) + { + return null; + } + + var ratios = new List(); + using var stream = new MemoryStream(_data, _data.Length - 24, 24); + using var reader = new BinaryReader(stream); + ratios.Add(reader.ReadSingle()); + throw new NotImplementedException(); + } + + public void WriteTo(BinaryWriter writer) + { + writer.Write(Terminator); + writer.Write(FooterString); + writer.Write(FinalTimeCode); + writer.Write(_data); + writer.Write(FooterString.Length + _data.Length + 8); + } + } + + +} diff --git a/TechnologyAssembler.Core.dll b/TechnologyAssembler.Core.dll new file mode 100644 index 0000000..2c15d9e Binary files /dev/null and b/TechnologyAssembler.Core.dll differ diff --git a/Utils/CancelManager.cs b/Utils/CancelManager.cs new file mode 100644 index 0000000..af3f9c0 --- /dev/null +++ b/Utils/CancelManager.cs @@ -0,0 +1,48 @@ +using System; +using System.Threading; + +namespace AnotherReplayReader.Utils +{ + internal class CancelManager : IDisposable + { + private CancellationToken _linkedToken; + private CancellationTokenSource? _source; + + public CancellationToken Token => Materialize().Token; + + public void Reset(CancellationToken linked) + { + _linkedToken = linked; + if (_source is { } source) + { + _source = null; + try + { + source.Cancel(); + } + catch (AggregateException e) + { + Debug.Instance.DebugMessage += $"Cancellation failed: {e}"; + } + source.Dispose(); + } + } + + public CancellationToken ResetAndGetToken(CancellationToken linked) + { + Reset(linked); + return Token; + } + + public void Dispose() => Reset(default); + + private CancellationTokenSource Materialize() + { + if (_source is null) + { + _source = CancellationTokenSource.CreateLinkedTokenSource(_linkedToken); + } + return _source; + } + } +} diff --git a/Utils/CancellableTaskExtensions.cs b/Utils/CancellableTaskExtensions.cs new file mode 100644 index 0000000..832f8b0 --- /dev/null +++ b/Utils/CancellableTaskExtensions.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading.Tasks; + +namespace AnotherReplayReader.Utils +{ + internal static class CancellableTaskExtensions + { + public static async Task IgnoreCancel(this Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } + + public static void Forget(this Task task) + { + const TaskContinuationOptions flags = TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously; + task.ContinueWith(t => t.Exception?.Handle(_ => true), flags); + } + } +} diff --git a/Utils/ImmutableArrayExtensions.cs b/Utils/ImmutableArrayExtensions.cs new file mode 100644 index 0000000..f7458ac --- /dev/null +++ b/Utils/ImmutableArrayExtensions.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Immutable; + +namespace AnotherReplayReader.Utils +{ + static class ImmutableArrayExtensions + { + public static int? FindIndex(this in ImmutableArray a, Predicate p) + { + for (var i = 0; i < a.Length; ++i) + { + if (p(a[i])) + { + return i; + } + } + return null; + } + } +} diff --git a/Utils/Lock.cs b/Utils/Lock.cs new file mode 100644 index 0000000..f17268d --- /dev/null +++ b/Utils/Lock.cs @@ -0,0 +1,32 @@ +using System; +using System.Threading; + +namespace AnotherReplayReader.Utils +{ + internal class Lock : IDisposable + { + public readonly object LockObject; + private bool _disposed = false; + + public Lock(object lockObject) + { + LockObject = lockObject; + Monitor.Enter(LockObject); + } + + public void Dispose() + { + if (!_disposed) + { + Monitor.Exit(LockObject); + _disposed = true; + } + } + + public static T Run(object @lock, Func action) + { + using var locker = new Lock(@lock); + return action(); + } + } +} diff --git a/Utils/PinyinExtensions.cs b/Utils/PinyinExtensions.cs new file mode 100644 index 0000000..d901680 --- /dev/null +++ b/Utils/PinyinExtensions.cs @@ -0,0 +1,27 @@ +using NPinyin; +using System; + +namespace AnotherReplayReader.Utils +{ + static class PinyinExtensions + { + public static bool ContainsIgnoreCase(this string self, string? s) + { + return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1; + } + + public static string? ToPinyin(this string self) + { + string pinyin; + try + { + pinyin = Pinyin.GetPinyin(self); + } + catch + { + return null; + } + return pinyin.Replace(" ", ""); + } + } +} diff --git a/Utils/RegistryUtils.cs b/Utils/RegistryUtils.cs new file mode 100644 index 0000000..8e58b84 --- /dev/null +++ b/Utils/RegistryUtils.cs @@ -0,0 +1,28 @@ +using Microsoft.Win32; +using System; + +namespace AnotherReplayReader.Utils +{ + internal static class RegistryUtils + { + public static string? Retrieve(RegistryHive hive, string path, string value) + { + try + { + using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32); + using var ra3Key = view32.OpenSubKey(path, false); + return ra3Key?.GetValue(value) as string; + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to retrieve registy {hive}:{path}:{value}: {e}"; + return null; + } + } + + public static string? RetrieveInRa3(RegistryHive hive, string value) + { + return Retrieve(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value); + } + } +} diff --git a/Utils/ReplayPinyinList.cs b/Utils/ReplayPinyinList.cs new file mode 100644 index 0000000..d3a8e4d --- /dev/null +++ b/Utils/ReplayPinyinList.cs @@ -0,0 +1,61 @@ +using AnotherReplayReader.ReplayFile; +using System.Collections.Immutable; +using System.Linq; + +namespace AnotherReplayReader.Utils +{ + internal class ReplayPinyinList + { + private readonly PlayerIdentity _playerIdentity; + public ImmutableArray Replays { get; } = ImmutableArray.Empty; + public ImmutableArray Pinyins { get; } = ImmutableArray.Empty; + + public ReplayPinyinList(PlayerIdentity playerIdentity) : + this(ImmutableArray.Empty, playerIdentity) + { + } + + public ReplayPinyinList(ImmutableArray replay, PlayerIdentity playerIdentity) : + this(replay, + replay.Select(replay => new ReplayPinyinData(replay, playerIdentity)).ToImmutableArray(), + playerIdentity) + { + } + + private ReplayPinyinList(ImmutableArray replay, + ImmutableArray pinyins, + PlayerIdentity playerIdentity) + { + _playerIdentity = playerIdentity; + Replays = replay; + Pinyins = pinyins; + } + + public ReplayPinyinList SetItem(int index, Replay replay) + { + return new(Replays.SetItem(index, replay), + Pinyins.SetItem(index, new(replay, _playerIdentity)), + _playerIdentity); + } + } + + class ReplayPinyinData + { + public Replay Replay { get; } + public string? PinyinDetails { get; } + public string? PinyinMod { get; } + + public ReplayPinyinData(Replay replay, PlayerIdentity playerIdentity) + { + Replay = replay; + PinyinDetails = replay.GetDetails(playerIdentity).ToPinyin(); + PinyinMod = replay.Mod.ModName.ToPinyin(); + } + + public bool MatchPinyin(string? pinyin) + { + return PinyinDetails?.ContainsIgnoreCase(pinyin) is true + || PinyinMod?.ContainsIgnoreCase(pinyin) is true; + } + } +} diff --git a/Utils/ShortTimeSpan.cs b/Utils/ShortTimeSpan.cs new file mode 100644 index 0000000..ff83a66 --- /dev/null +++ b/Utils/ShortTimeSpan.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace AnotherReplayReader.Utils +{ + public readonly struct ShortTimeSpan : IEquatable, IComparable, IComparable + { + public readonly TimeSpan Value; + + public ShortTimeSpan(TimeSpan value) => Value = value; + public static implicit operator TimeSpan(ShortTimeSpan span) => span.Value; + public static implicit operator ShortTimeSpan(TimeSpan value) => new(value); + public override string ToString() => $"{(int)Value.TotalMinutes:00}:{Value.Seconds:00}"; + + public int CompareTo(ShortTimeSpan other) => Value.CompareTo(other.Value); + public int CompareTo(object obj) => obj is ShortTimeSpan span ? CompareTo(span) : 1; + public override bool Equals(object? obj) => obj is ShortTimeSpan span && Equals(span); + public bool Equals(ShortTimeSpan other) => Value.Equals(other.Value); + public override int GetHashCode() => Value.GetHashCode(); + public static bool operator ==(ShortTimeSpan left, ShortTimeSpan right) => left.Equals(right); + public static bool operator !=(ShortTimeSpan left, ShortTimeSpan right) => !(left == right); + public static bool operator <(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) < 0; + public static bool operator >(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) > 0; + } +} diff --git a/Utils/TaskQueue.cs b/Utils/TaskQueue.cs new file mode 100644 index 0000000..eabd2c8 --- /dev/null +++ b/Utils/TaskQueue.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using System.Windows.Threading; + +namespace AnotherReplayReader.Utils +{ + internal class TaskQueue + { + private readonly object _lock = new(); + private readonly Dispatcher _dispatcher; + private Task _current = Task.CompletedTask; + + public TaskQueue(Dispatcher dispatcher) + { + _dispatcher = dispatcher; + } + + public Task Enqueue(Func getTask, CancellationToken cancelToken) + { + using var locker = new Lock(_lock); + _current = _current.ContinueWith(async t => + { + await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken); + }, cancelToken); + return _current.IgnoreCancel(); + } + + } +} diff --git a/Window1.xaml b/Window1.xaml index 9a64fee..fdd9701 100644 --- a/Window1.xaml +++ b/Window1.xaml @@ -8,15 +8,15 @@ mc:Ignorable="d" Title="Window1" Height="450" Width="800"> - + - +