From 888ddce4efba834b853a90154c3ab8cb67885b7a Mon Sep 17 00:00:00 2001 From: lanyi Date: Tue, 19 Oct 2021 17:42:18 +0200 Subject: [PATCH] =?UTF-8?q?=E6=94=B9=E4=BA=86=E4=B8=80=E4=BA=BF=E4=B8=AA?= =?UTF-8?q?=E4=B8=9C=E8=A5=BF=EF=BC=8C=E4=BF=AE=E4=BA=86=E4=B8=80=E4=BA=BF?= =?UTF-8?q?=E4=B8=AA=20BUG?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- About.xaml | 21 +- About.xaml.cs | 14 +- AnotherReplayReader.csproj | 36 +- APM.xaml => ApmWindow.xaml | 4 +- APM.xaml.cs => ApmWindow.xaml.cs | 87 ++- App.xaml | 3 +- App.xaml.cs | 25 +- Auth.cs | 47 +- BigMinimapCache.cs | 254 ++------ Cache.cs | 62 +- Debug.xaml.cs | 63 +- DronePlatform.cs | 25 + Excludes.txt | 1 - ILMergeConfig.json | 145 +---- MainWindow.xaml | 6 +- MainWindow.xaml.cs | 579 +++++++----------- MinimapReader.cs | 143 ++--- ModData.cs | 108 +--- PinyinExtensions.cs | 76 --- PlayerIdentity.cs | 174 +++--- Replay.cs | 433 ------------- ReplayAutoSaver.cs | 148 +++++ CommandChunk.cs => ReplayFile/CommandChunk.cs | 122 ++-- ReplayFile/Player.cs | 41 ++ ReplayFile/Replay.cs | 346 +++++++++++ ReplayFile/ReplayChunk.cs | 37 ++ ReplayFile/ReplayExtensions.cs | 40 ++ ReplayFile/ReplayFooter.cs | 90 +++ TechnologyAssembler.Core.dll | Bin 0 -> 304128 bytes Utils/CancelManager.cs | 48 ++ Utils/CancellableTaskExtensions.cs | 23 + Utils/ImmutableArrayExtensions.cs | 20 + Utils/Lock.cs | 32 + Utils/PinyinExtensions.cs | 27 + Utils/RegistryUtils.cs | 28 + Utils/ReplayPinyinList.cs | 61 ++ Utils/ShortTimeSpan.cs | 28 + Utils/TaskQueue.cs | 30 + Window1.xaml | 8 +- Window1.xaml.cs | 150 +++-- 40 files changed, 1870 insertions(+), 1715 deletions(-) rename APM.xaml => ApmWindow.xaml (96%) rename APM.xaml.cs => ApmWindow.xaml.cs (70%) create mode 100644 DronePlatform.cs delete mode 100644 Excludes.txt delete mode 100644 PinyinExtensions.cs delete mode 100644 Replay.cs create mode 100644 ReplayAutoSaver.cs rename CommandChunk.cs => ReplayFile/CommandChunk.cs (77%) create mode 100644 ReplayFile/Player.cs create mode 100644 ReplayFile/Replay.cs create mode 100644 ReplayFile/ReplayChunk.cs create mode 100644 ReplayFile/ReplayExtensions.cs create mode 100644 ReplayFile/ReplayFooter.cs create mode 100644 TechnologyAssembler.Core.dll create mode 100644 Utils/CancelManager.cs create mode 100644 Utils/CancellableTaskExtensions.cs create mode 100644 Utils/ImmutableArrayExtensions.cs create mode 100644 Utils/Lock.cs create mode 100644 Utils/PinyinExtensions.cs create mode 100644 Utils/RegistryUtils.cs create mode 100644 Utils/ReplayPinyinList.cs create mode 100644 Utils/ShortTimeSpan.cs create mode 100644 Utils/TaskQueue.cs 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 0000000000000000000000000000000000000000..2c15d9e139e2f6887b4aedb35b70d7e990543e87 GIT binary patch literal 304128 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�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å*b6=yj!rZ%{$MTi#XA^vrK#ny2$!{lh8k; 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

F%ZnR8iZw0) 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 literal 0 HcmV?d00001 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"> - + - +