From 08700abd4f3e9ecda46d220698af9cee2a28b413 Mon Sep 17 00:00:00 2001 From: lanyi Date: Fri, 22 Oct 2021 08:54:04 +0200 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E7=A6=BB=E7=8E=A9=E5=AE=B6=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E6=8F=92=E4=BB=B6=EF=BC=8C=E4=BB=A5=E5=8F=8A=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E6=A3=80=E6=B5=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- About.xaml | 92 ++++++++++++++++++++++------- About.xaml.cs | 66 +++++++++++++++++++-- AnotherReplayReader.csproj | 22 +++++-- AnotherReplayReader.sln | 14 ++++- Apm/DataRow.cs | 2 +- ApmWindow.xaml | 1 + ApmWindow.xaml.cs | 2 +- App.xaml.cs | 44 +++++++++++++- Auth.cs | 88 ++++------------------------ BigMinimapCache.cs | 9 +++ Cache.cs | 39 ++++++++++--- ILMergeConfig.json | 36 ------------ IpAndPlayer.cs | 44 ++++++++++++++ MainWindow.xaml | 1 + MainWindow.xaml.cs | 73 ++++++++++++++++++++--- PlayerIdentity.cs | 103 ++++++++++++++++----------------- Plugin.cs | 78 +++++++++++++++++++++++++ UpdateChecker.cs | 90 +++++++++++++++++++++++++++++ Utils/Network.cs | 27 +++++++++ Utils/RegistryUtils.cs | 25 ++++++-- Utils/TaskQueue.cs | 5 +- Utils/Verifier.cs | 115 +++++++++++++++++++++++++++++++++++++ Utils/WpfExtensions.cs | 29 ++++++++++ Window1.xaml.cs | 13 +---- app.manifest | 76 ++++++++++++++++++++++++ 25 files changed, 860 insertions(+), 234 deletions(-) delete mode 100644 ILMergeConfig.json create mode 100644 IpAndPlayer.cs create mode 100644 Plugin.cs create mode 100644 UpdateChecker.cs create mode 100644 Utils/Network.cs create mode 100644 Utils/Verifier.cs create mode 100644 Utils/WpfExtensions.cs create mode 100644 app.manifest diff --git a/About.xaml b/About.xaml index 823edef..caa5145 100644 --- a/About.xaml +++ b/About.xaml @@ -1,4 +1,5 @@  - + Height="420" + Width="450" + WindowStartupLocation="CenterOwner" + Loaded="OnAboutWindowLoaded"> + - - + + - - - - - - - - - - - - - - - - - + + + + 已经有新版本了呢! + + + + + + + + + 这个工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW + 有任何问题可以先去找苏醒或节操问题( + 解析录像的代码主要来源于 + R Schneider 的研究 + 以及 BoolBada 的 + KWReplayAutoSaver + + 解析 Big 的代码来源于 + Jana Mohn + 的 TechnologyAssembler + 解析 Tga 的代码来源于 + Pfim + 使用了 + NPinyin + 以支持按照拼音来查询信息 + APM 图表是通过 + OxyPlot + 画出来的 + + 欢迎来到红警3吧: + https://tieba.baidu.com/ra3 + + + + + + + + diff --git a/About.xaml.cs b/About.xaml.cs index b28e71d..691425f 100644 --- a/About.xaml.cs +++ b/About.xaml.cs @@ -1,22 +1,80 @@ -using System.Diagnostics; +using AnotherReplayReader.Utils; +using System.Diagnostics; +using System.Linq; using System.Windows; +using System.Windows.Documents; namespace AnotherReplayReader { /// /// About.xaml 的交互逻辑 /// - public partial class About : Window + internal partial class About : Window { - public About() + private readonly Cache _cache; + private readonly UpdateCheckerVersionData? _updateData; + + public About(Cache cache) { + _cache = cache; InitializeComponent(); - _idBox.Text = Auth.Id; + } + + public About(Cache cache, UpdateCheckerVersionData updateData) + { + _cache = cache; + _updateData = updateData; + InitializeComponent(); + } + + private async void OnAboutWindowLoaded(object sender, RoutedEventArgs e) + { + foreach (var hyperlink in this.FindVisualChildren()) + { + hyperlink.RequestNavigate += OnHyperlinkRequestNavigate; + } + + await _cache.Initialization; + _checkForUpdates.IsChecked = _cache.GetOrDefault(UpdateChecker.CheckForUpdatesKey, false); + var data = _updateData + ?? _cache.GetOrDefault(UpdateChecker.CachedDataKey, null); + if (data is { } updateData && updateData.IsNewVersion()) + { + _updatePanel.Visibility = Visibility.Visible; + _updateInfo.Inlines.Add(updateData.Description); + _updateInfo.Inlines.Add(new LineBreak()); + updateData.Urls.Select(u => + { + var h = new Hyperlink(); + h.Inlines.Add(u); + h.NavigateUri = new(u); + h.RequestNavigate += OnHyperlinkRequestNavigate; + return h; + }); + } + + _ = Auth.Id.ContinueWith(t => Dispatcher.Invoke(() => + { + if (t.Result is { } id) + { + _idBox.Text = id; + } + else + { + _bottom.Visibility = Visibility.Collapsed; + } + })); } private void OnHyperlinkRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e) { Process.Start(e.Uri.ToString()); } + + private async void OnCheckForUpdatesCheckedChanged(object sender, RoutedEventArgs e) + { + _cache.Set(UpdateChecker.CheckForUpdatesKey, _checkForUpdates.IsChecked is true); + await _cache.Save(); + } } } diff --git a/AnotherReplayReader.csproj b/AnotherReplayReader.csproj index 1ed9371..ad2bea9 100644 --- a/AnotherReplayReader.csproj +++ b/AnotherReplayReader.csproj @@ -3,6 +3,7 @@ WinExe net461 true + true publish\ false true @@ -13,6 +14,7 @@ bin\$(Configuration)\ latest enable + app.manifest @@ -20,9 +22,6 @@ - - - @@ -31,14 +30,15 @@ - - + + + True @@ -52,4 +52,16 @@ Settings.Designer.cs + + + <_FilesToMove Include="$(OutputPath)*.dll"/> + + + + '$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')"/> + \ No newline at end of file diff --git a/AnotherReplayReader.sln b/AnotherReplayReader.sln index da7c8fd..74283e8 100644 --- a/AnotherReplayReader.sln +++ b/AnotherReplayReader.sln @@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30907.101 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherReplayReader", "AnotherReplayReader.csproj", "{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnotherReplayReader", "AnotherReplayReader.csproj", "{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnotherReplayReader.PlayerIdentity", "..\AnotherReplayReader.PlayerIdentity\AnotherReplayReader.PlayerIdentity.csproj", "{78678E57-CBA2-46E4-B764-4CB6E66EF156}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherReplayReader.PluginSystem", "..\AnotherReplayReader.PluginSystem\AnotherReplayReader.PluginSystem.csproj", "{0730D7D0-64A3-4F3F-8D8C-46E7676C659F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -15,6 +19,14 @@ Global {A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Debug|Any CPU.Build.0 = Debug|Any CPU {A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Release|Any CPU.ActiveCfg = Release|Any CPU {A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Release|Any CPU.Build.0 = Release|Any CPU + {78678E57-CBA2-46E4-B764-4CB6E66EF156}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {78678E57-CBA2-46E4-B764-4CB6E66EF156}.Debug|Any CPU.Build.0 = Debug|Any CPU + {78678E57-CBA2-46E4-B764-4CB6E66EF156}.Release|Any CPU.ActiveCfg = Release|Any CPU + {78678E57-CBA2-46E4-B764-4CB6E66EF156}.Release|Any CPU.Build.0 = Release|Any CPU + {0730D7D0-64A3-4F3F-8D8C-46E7676C659F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0730D7D0-64A3-4F3F-8D8C-46E7676C659F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0730D7D0-64A3-4F3F-8D8C-46E7676C659F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0730D7D0-64A3-4F3F-8D8C-46E7676C659F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/Apm/DataRow.cs b/Apm/DataRow.cs index a5ca21f..b39692f 100644 --- a/Apm/DataRow.cs +++ b/Apm/DataRow.cs @@ -112,7 +112,7 @@ namespace AnotherReplayReader.Apm { string GetIpAndName(Player player) => player.IsComputer ? "这是 AI" - : identity.QueryRealNameAndIP(player.PlayerIp); + : identity.QueryRealNameAndIP(player.PlayerIp) ?? string.Empty; dataList.Add(new("局域网 IP", plotter.Players.Select(GetIpAndName))); } apmRowIndex = dataList.Count; diff --git a/ApmWindow.xaml b/ApmWindow.xaml index 835ef66..10a673a 100644 --- a/ApmWindow.xaml +++ b/ApmWindow.xaml @@ -11,6 +11,7 @@ Title="APM" Height="550" Width="800" + WindowStartupLocation="CenterOwner" Loaded="OnApmWindowLoaded"> diff --git a/ApmWindow.xaml.cs b/ApmWindow.xaml.cs index 183fb15..d9bab29 100644 --- a/ApmWindow.xaml.cs +++ b/ApmWindow.xaml.cs @@ -83,7 +83,7 @@ namespace AnotherReplayReader } catch (Exception e) { - MessageBox.Show($"加载录像信息失败:\r\n{e}"); + MessageBox.Show(this, $"加载录像信息失败:\r\n{e}"); } } diff --git a/App.xaml.cs b/App.xaml.cs index efec509..7e927bb 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,4 +1,7 @@ using System; +using System.IO; +using System.Reflection; +using System.Runtime.InteropServices; using System.Threading; using System.Windows; @@ -10,8 +13,25 @@ namespace AnotherReplayReader public partial class App : Application { private int _isInException = 0; + + public const string Version = "0.7"; + public const string Name = "自动录像机"; + public const string NameWithVersion = Name + " v" + Version; + + private static readonly Lazy _libsFolder = new(GetLibraryFolder, LazyThreadSafetyMode.PublicationOnly); + public static string LibsFolder => _libsFolder.Value; + public App() { + static Assembly? LoadFromLibsFolder(object sender, ResolveEventArgs args) + { + var assemblyPath = Path.Combine(LibsFolder, new AssemblyName(args.Name).Name + ".dll"); + return File.Exists(assemblyPath) + ? Assembly.LoadFrom(assemblyPath) + : null; + } + AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(LoadFromLibsFolder); + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => { if (Interlocked.Increment(ref _isInException) > 1) @@ -21,7 +41,7 @@ namespace AnotherReplayReader Dispatcher.Invoke(() => { const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?"; - var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", "自动录像机", MessageBoxButton.YesNo); + var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", Name, MessageBoxButton.YesNo); if (choice == MessageBoxResult.Yes) { Debug.Instance.RequestSave(); @@ -29,5 +49,27 @@ namespace AnotherReplayReader }); }; } + + private static string GetLibraryFolder() => Path.Combine(GetExecutableFolder(), nameof(AnotherReplayReader) + "Data"); + + private static string GetExecutableFolder() + { + char[]? buffer = null; + uint result; + do + { + buffer = new char[(buffer?.Length ?? 128) * 2]; + result = GetModuleFileNameW(IntPtr.Zero, buffer, buffer.Length); + if (result is 0) + { + throw new Exception("Failed to retrieve executable name"); + } + } + while (result >= buffer.Length); + return Path.GetDirectoryName(new(buffer, 0, Array.IndexOf(buffer, '\0'))); + } + + [DllImport("Kernel32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ExactSpelling = true, SetLastError = true)] + private static extern uint GetModuleFileNameW(IntPtr module, char[] fileName, int size); } } diff --git a/Auth.cs b/Auth.cs index 1cd777a..aad436f 100644 --- a/Auth.cs +++ b/Auth.cs @@ -1,95 +1,31 @@ -using Microsoft.Win32; +using AnotherReplayReader.PluginSystem; +using AnotherReplayReader.Utils; using System; -using System.IO; using System.Linq; -using System.Security.Cryptography; using System.Text; +using System.Threading.Tasks; namespace AnotherReplayReader { internal static class Auth { - public static string? Id { get; } + private static readonly TaskCompletionSource> _source = new(); + public static Task Id { get; } = _source.Task.Unwrap(); - static Auth() + public static void LoadPlugin(IPlugin plugin) { - Id = null; - - 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; - } - catch - { - return; - } - - string? randomKey; - try - { - var folderPath = Cache.CacheDirectory; - if (!Directory.Exists(folderPath)) - { - Directory.CreateDirectory(folderPath); - } - - var keyPath = Path.Combine(folderPath, "id"); - if (!File.Exists(keyPath)) - { - File.WriteAllText(keyPath, Guid.NewGuid().ToString()); - } - - randomKey = File.ReadAllText(keyPath); - } - catch - { - return; - } - - if (string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey)) - { - 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}")); + var authService = plugin.CreateAuthService(RegistryUtils.RetrieveInHklm64, Cache.CacheDirectory); + authService.Id.ContinueWith(_source.TrySetResult); } - public static string GetKey() + public static async Task IdAsKey() { - if (Id == null) - { - return string.Empty; - } - - var text = $"{Id}{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}"; - var bytes = Encoding.UTF8.GetBytes(text); - var pre = Encoding.UTF8.GetBytes("playertable!"); - var salt = new byte[9]; - using (var rng = new RNGCryptoServiceProvider()) - { - rng.GetNonZeroBytes(salt); - } - - for (var i = 0; i < bytes.Length; ++i) - { - bytes[i] = (byte)(bytes[i] ^ salt[i % salt.Length]); - } - - return Convert.ToBase64String(salt.Concat(bytes).Select((x, i) => (byte)(x ^ pre[i % pre.Length])).ToArray()); - } - - public static byte[]? IdAsKey() - { - if (string.IsNullOrEmpty(Id)) + var id = await Id.ConfigureAwait(false); + if (string.IsNullOrEmpty(id)) { return null; } - var bytes = Encoding.UTF8.GetBytes(Id); + var bytes = Encoding.UTF8.GetBytes(id); var destination = Enumerable.Repeat(0xEA, 24).ToArray(); Array.Copy(bytes, destination, Math.Min(bytes.Length, destination.Length)); return destination; diff --git a/BigMinimapCache.cs b/BigMinimapCache.cs index b090dd5..e565ff1 100644 --- a/BigMinimapCache.cs +++ b/BigMinimapCache.cs @@ -3,6 +3,7 @@ using Microsoft.Win32; using System; using System.IO; using System.Linq; +using System.Reflection; using System.Text.RegularExpressions; using System.Threading.Tasks; using TechnologyAssembler.Core.IO; @@ -102,6 +103,14 @@ namespace AnotherReplayReader DronePlatform.BuildTechnologyAssembler(); _skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef); } + catch (ReflectionTypeLoadException e) + { + Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{e}\r\nLoader Exceptions: {e.LoaderExceptions.Length}"; + foreach (var e2 in e.LoaderExceptions) + { + Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{e2}\r\n"; + } + } catch (Exception exception) { Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n"; diff --git a/Cache.cs b/Cache.cs index 310f5a6..5a679ca 100644 --- a/Cache.cs +++ b/Cache.cs @@ -2,18 +2,20 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using static System.Text.Json.JsonSerializer; namespace AnotherReplayReader { - internal sealed class Cache + public sealed class Cache { - public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader"); + public static string OldCacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader"); + public static string CacheDirectory => App.LibsFolder; public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache"); private readonly ConcurrentDictionary _storage = new(); - + public Task Initialization { get; } public Cache() @@ -27,6 +29,28 @@ namespace AnotherReplayReader Directory.CreateDirectory(CacheDirectory); } + try + { + var old = new DirectoryInfo(OldCacheDirectory); + if (old.Exists) + { + foreach (var file in old.EnumerateFiles()) + { + try + { + file.MoveTo(Path.Combine(CacheDirectory, file.Name)); + } + catch { } + } + try + { + old.Delete(); + } + catch { } + } + } + catch { } + using var cacheStream = File.OpenRead(CacheFilePath); var futureCache = DeserializeAsync>(cacheStream).ConfigureAwait(false); if (await futureCache is not { } cached) @@ -42,7 +66,7 @@ namespace AnotherReplayReader }); } - public T GetOrDefault(string key, in T defaultValue) + public T GetOrDefault(string key, T defaultValue) { try { @@ -55,12 +79,11 @@ namespace AnotherReplayReader return defaultValue; } - public void Set(string key, in T value) + public void Set(string key, T value) { try { _storage[key] = Serialize(value); - Save(); } catch { } } @@ -73,7 +96,6 @@ namespace AnotherReplayReader { _storage[key] = Serialize(value); } - Save(); } catch { } } @@ -81,10 +103,9 @@ namespace AnotherReplayReader public void Remove(string key) { _storage.TryRemove(key, out _); - Save(); } - public async void Save() + public async Task Save() { try { diff --git a/ILMergeConfig.json b/ILMergeConfig.json deleted file mode 100644 index 8ad6429..0000000 --- a/ILMergeConfig.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "General": { - "OutputFile": null, - "TargetPlatform": null, - "KeyFile": null, - "AlternativeILMergePath": null, - "InputAssemblies": [ - "NPinyin.Core.dll", - "Pfim.dll", - "TechnologyAssembler.Core.dll" - ] - }, - "Advanced": { - "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/IpAndPlayer.cs b/IpAndPlayer.cs new file mode 100644 index 0000000..e5432d8 --- /dev/null +++ b/IpAndPlayer.cs @@ -0,0 +1,44 @@ +using AnotherReplayReader.Utils; +using System.Text.Json.Serialization; + +namespace AnotherReplayReader +{ + public sealed class IpAndPlayer + { + public static string SimpleIPToString(uint ip) + { + return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}"; + } + + [JsonPropertyName("IP")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public uint Ip + { + get => _ip; + set + { + _ip = value; + IpString = SimpleIPToString(_ip); + } + } + [JsonIgnore] + public string IpString { get; private set; } = "0.0.0.0"; + + [JsonPropertyName("ID")] + public string Id + { + get => _id; + set + { + _id = value; + _pinyin = _id.ToPinyin(); + } + } + [JsonIgnore] + public string? PinyinId => _pinyin; + + private uint _ip; + private string _id = string.Empty; + private string? _pinyin; + } +} diff --git a/MainWindow.xaml b/MainWindow.xaml index a4e59d9..6cea1f2 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -6,6 +6,7 @@ xmlns:local="clr-namespace:AnotherReplayReader" mc:Ignorable="d" Title="MainWindow" Width="800" Height="600" + WindowStartupLocation="CenterScreen" Loaded="OnMainWindowLoaded"> diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 2ec01ad..f2af168 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -125,17 +125,54 @@ namespace AnotherReplayReader InitializeComponent(); Closing += (sender, eventArgs) => { - _cache.Save(); + _cache.Save().Wait(); Application.Current.Shutdown(); }; } - private async void OnMainWindowLoaded(object sender, EventArgs e) + private async void OnMainWindowLoaded(object sender, EventArgs eventArgs) { Debug.Initialize(); + await _cache.Initialization; ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath); var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); - await _taskQueue.Enqueue(() => LoadReplays(null, token), token); + _ = _taskQueue.Enqueue(() => LoadReplays(null, token), token); + + var wantUsePlugin = true; + try + { + wantUsePlugin = await Plugin.LoadPlayerIdentityPlugin(_playerIdentity); + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to load plugin: {e}"; + MessageBox.Show(this, $"插件加载失败:{e.Message}"); + } + + const string permissionKey = "questionAsked"; + if (!wantUsePlugin && _cache.GetOrDefault(permissionKey, false) is not true) + { + _cache.Set(permissionKey, true); + var sb = new StringWriter(); + sb.WriteLine("要不要自动检查更新呢?"); + sb.WriteLine("之后也可以在“关于”窗口里,设置自动更新的选项"); + var choice = MessageBox.Show(this, sb.ToString(), App.Name, MessageBoxButton.YesNo); + _cache.Set(UpdateChecker.CheckForUpdatesKey, choice is MessageBoxResult.Yes); + await _cache.Save(); + } + + _ = UpdateChecker.CheckForUpdates(_cache).ContinueWith(t => Dispatcher.InvokeAsync(() => + { + var updateData = t.Result; + if (updateData.IsNewVersion()) + { + var about = new About(_cache, updateData) + { + Owner = this + }; + about.ShowDialog(); + } + })); } private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken) @@ -318,6 +355,22 @@ namespace AnotherReplayReader { var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); await _taskQueue.Enqueue(() => LoadReplays(null, token), token); + var text = _replayFolderPathBox.Text; + const string assemblyMagic = "!DreamSign"; + const string jsonMagic = "!FantasySeal"; + switch (_replayFolderPathBox.Text) + { + case "!SpellCard": + _replayDetailsBox.Text = $"{assemblyMagic}\r\n"; + _replayDetailsBox.Text += $"{jsonMagic}\r\n"; + break; + case assemblyMagic: + Plugin.Sign(); + break; + case jsonMagic: + UpdateChecker.Sign(); + break; + } } private async void OnReplaySelectionChanged(object sender, EventArgs e) @@ -334,7 +387,10 @@ namespace AnotherReplayReader private void OnAboutButtonClick(object sender, RoutedEventArgs e) { - var aboutWindow = new About(); + var aboutWindow = new About(_cache) + { + Owner = this + }; aboutWindow.ShowDialog(); } @@ -348,7 +404,7 @@ namespace AnotherReplayReader InitialDirectory = _properties.ReplayFolderPath, }; - var result = openFileDialog.ShowDialog(); + var result = openFileDialog.ShowDialog(this); if (result == true) { var fileName = openFileDialog.FileName; @@ -364,7 +420,10 @@ namespace AnotherReplayReader private void OnDetailsButtonClick(object sender, RoutedEventArgs e) { - var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity); + var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity) + { + Owner = this + }; detailsWindow.ShowDialog(); } @@ -399,7 +458,7 @@ namespace AnotherReplayReader } catch (Exception exception) { - MessageBox.Show($"无法修复录像:\r\n{exception}"); + MessageBox.Show(this, $"无法修复录像:\r\n{exception}"); } var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None); diff --git a/PlayerIdentity.cs b/PlayerIdentity.cs index b9f8c38..7dd5b7e 100644 --- a/PlayerIdentity.cs +++ b/PlayerIdentity.cs @@ -1,55 +1,29 @@ -using AnotherReplayReader.Utils; +using AnotherReplayReader.PluginSystem; +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 static System.Text.Json.JsonSerializer; namespace AnotherReplayReader { - internal sealed class IpAndPlayer - { - public static string SimpleIPToString(uint ip) - { - return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}"; - } - - [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] - public uint Ip - { - get => _ip; - set - { - _ip = value; - IpString = SimpleIPToString(_ip); - } - } - public string IpString { get; private set; } = "0.0.0.0"; - public string Id - { - get => _id; - set - { - _id = value; - _pinyin = _id.ToPinyin(); - } - } - public string? PinyinId => _pinyin; - - private uint _ip; - private string _id = string.Empty; - private string? _pinyin; - } - internal class PlayerIdentity { + private class NetworkSerializer : IGenericCallable + { + public TResult Invoke(TArg arg) => throw new NotImplementedException(); + + public Task InvokeAsync(TArg arg) + { + var url = arg as string ?? throw new NotImplementedException(); + return Network.HttpGetJson(url); + } + } + private const string StoredKey = "pt"; private const string IvKey = "jw"; private static readonly JsonSerializerOptions _jsonOptions = new() @@ -58,6 +32,7 @@ namespace AnotherReplayReader }; private readonly object _lock = new(); private readonly Cache _cache; + private readonly TaskCompletionSource _serviceSource = new(); private IReadOnlyDictionary? _list; public bool IsUsable => Lock.Run(_lock, () => _list is not null); @@ -69,24 +44,32 @@ namespace AnotherReplayReader Fetch(); } + public void LoadPlugin(IPlugin plugin) + { + var service = plugin.CreatePlayerIdentityService(Network.UrlEncode, new NetworkSerializer()); + _serviceSource.TrySetResult(service); + } + public Task Fetch() { 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 response = await request.GetResponseAsync().ConfigureAwait(false); - using var stream = response.GetResponseStream(); - var list = await DeserializeAsync>(stream, _jsonOptions).ConfigureAwait(false); + if (await Auth.Id is null) + { + return; + } + + var service = await _serviceSource.Task.ConfigureAwait(false); + var list = await service.Fetch().ConfigureAwait(false); var converted = list?.ToDictionary(x => x.Ip, x => x.Id); lock (_lock) { _list = converted; } - if (list is null || Auth.IdAsKey() is not { } encryptKey) + if (list is null || await Auth.IdAsKey() is not { } encryptKey) { _cache.Set(StoredKey, null); return; @@ -106,6 +89,12 @@ namespace AnotherReplayReader }); } + public async Task UpdateIpTable(uint ip, string idText) + { + var service = await _serviceSource.Task.ConfigureAwait(false); + return await service.UpdateIpTable(ip, idText).ConfigureAwait(false); + } + public List AsSortedList() { using var locker = new Lock(_lock); @@ -116,15 +105,15 @@ namespace AnotherReplayReader .ToList() ?? new(0); } - public string GetRealName(uint ip) + public string? GetRealName(uint ip) { - if (ip == 0) + using var locker = new Lock(_lock); + if (_list is null) { - return string.Empty; + return null; } - using var locker = new Lock(_lock); - if (_list is null || !_list.TryGetValue(ip, out var name)) + if (ip is 0 || !_list.TryGetValue(ip, out var name)) { return string.Empty; } @@ -135,6 +124,10 @@ namespace AnotherReplayReader public string FormatRealName(uint ip) { var name = GetRealName(ip); + if (name is null) + { + return string.Empty; + } if (string.IsNullOrEmpty(name)) { name = IpAndPlayer.SimpleIPToString(ip); @@ -142,10 +135,14 @@ namespace AnotherReplayReader return $"({name})"; } - public string QueryRealNameAndIP(uint ip) + public string? QueryRealNameAndIP(uint ip) { - var ipText = IpAndPlayer.SimpleIPToString(ip); var name = GetRealName(ip); + if (name is null) + { + return null; + } + var ipText = IpAndPlayer.SimpleIPToString(ip); if (string.IsNullOrEmpty(name)) { return ipText; @@ -160,7 +157,7 @@ namespace AnotherReplayReader await _cache.Initialization; var stored = _cache.GetOrDefault(StoredKey, string.Empty); var iv = Convert.FromBase64String(_cache.GetOrDefault(IvKey, string.Empty)); - if (string.IsNullOrWhiteSpace(stored) || Auth.IdAsKey() is not { } key) + if (string.IsNullOrWhiteSpace(stored) || await Auth.IdAsKey() is not { } key) { return; } diff --git a/Plugin.cs b/Plugin.cs new file mode 100644 index 0000000..6b5c9db --- /dev/null +++ b/Plugin.cs @@ -0,0 +1,78 @@ +using AnotherReplayReader.PluginSystem; +using AnotherReplayReader.Utils; +using Microsoft.Win32; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Security.Cryptography; +using System.Text.Json; +using System.Threading.Tasks; + +namespace AnotherReplayReader +{ + + internal static class Plugin + { + public static Task LoadPlayerIdentityPlugin(PlayerIdentity playerIdentity) + { + return Task.Run(async () => + { + var name = $"{nameof(AnotherReplayReader)}.{nameof(PlayerIdentity)}"; + var fileName = Path.Combine(App.LibsFolder, $"{name}.dll"); + if (File.Exists(fileName)) + { + var assembly = await Load(fileName); + var pluginType = assembly.GetType($"{name}.Plugin", true); + var plugin = (IPlugin)Activator.CreateInstance(pluginType); + Auth.LoadPlugin(plugin); + playerIdentity.LoadPlugin(plugin); + return true; + } + return false; + }); + } + + public static async Task Load(string path) + { + using var file = File.OpenRead(path); + using var metaFile = File.OpenRead($"{path}.meta"); + var metaTask = JsonSerializer.DeserializeAsync(metaFile, Network.CommonJsonOptions); + using var memory = new MemoryStream(); + await file.CopyToAsync(memory); + var array = memory.ToArray(); + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(array); + var meta = await metaTask ?? throw new InvalidDataException("Failed to load meta file"); + if (!Enumerable.SequenceEqual(hash, meta.ByteData.Value)) + { + throw new InvalidDataException($"Hash does not match, might be corrupt {Convert.ToBase64String(hash)} vs {meta.Data}"); + } + if (!Verifier.Verify(meta)) + { + throw new InvalidDataException("Invalid metadata, might be corrupt"); + } + return Assembly.Load(array); + } + + public static void Sign() + { + var openFileDialog = new OpenFileDialog + { + Filter = "dll (*.dll)|*.dll|所有文件 (*.*)|*.*", + InitialDirectory = AppContext.BaseDirectory, + }; + + var result = openFileDialog.ShowDialog(); + if (result is true) + { + using var file = openFileDialog.OpenFile(); + using var memory = new MemoryStream(); + file.CopyTo(memory); + using var sha256 = SHA256.Create(); + var hash = sha256.ComputeHash(memory.ToArray()); + Verifier.Sign(hash); + } + } + } +} diff --git a/UpdateChecker.cs b/UpdateChecker.cs new file mode 100644 index 0000000..78c7a71 --- /dev/null +++ b/UpdateChecker.cs @@ -0,0 +1,90 @@ +using AnotherReplayReader.Utils; +using System; +using System.Collections.Immutable; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using System.Windows; + +namespace AnotherReplayReader +{ + internal class UpdateCheckerVersionData + { + public string NewVersion { get; } + public string Description { get; } + public ImmutableArray Urls { get; } + + public UpdateCheckerVersionData(string newVersion, + string description, + ImmutableArray urls) + { + NewVersion = newVersion; + Description = description; + Urls = urls; + } + + public bool IsNewVersion() => NewVersion != App.Version; + } + + internal class UpdateChecker + { + public const string CheckForUpdatesKey = "checkForUpdates"; + public const string CachedDataKey = "cachedUpdateData"; + private static readonly ImmutableArray _updateSources = new[] + { + "https://lanyi.altervista.org/playertable/file.json" + }.ToImmutableArray(); + + public static Task CheckForUpdates(Cache cache) + { + var taskSource = new TaskCompletionSource(); + if (cache.GetOrDefault(CheckForUpdatesKey, false) is not true) + { + return taskSource.Task; + } + foreach (var source in _updateSources) + { + Task.Run(async () => + { + try + { + var data = await DownloadAndVerify(source).ConfigureAwait(false); + if (data.IsNewVersion()) + { + cache.Set(CachedDataKey, data); + taskSource.TrySetResult(data); + } + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Update check failed: {e}"; + } + }); + } + return taskSource.Task; + } + + private static async Task DownloadAndVerify(string url) + { + var payload = await Network.HttpGetJson(url) + ?? throw new InvalidDataException(nameof(VerifierPayload) + " is null"); + if (!Verifier.Verify(payload)) + { + throw new InvalidDataException(nameof(VerifierPayload) + " cannot be verified"); + } + return JsonSerializer.Deserialize(payload.ByteData.Value, Network.CommonJsonOptions) + ?? throw new InvalidDataException(nameof(UpdateCheckerVersionData) + " is null"); + } + + public static void Sign() + { + var sample = new UpdateCheckerVersionData(App.Version, "Alice Margatroid", _updateSources); + var sampleText = JsonSerializer.Serialize(sample, Network.CommonJsonOptions); + Verifier.Sign(sampleText); + } + } +} diff --git a/Utils/Network.cs b/Utils/Network.cs new file mode 100644 index 0000000..18e032d --- /dev/null +++ b/Utils/Network.cs @@ -0,0 +1,27 @@ +using System.Net.Http; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using System.Web; + +namespace AnotherReplayReader.Utils +{ + public static class Network + { + public static readonly JsonSerializerOptions CommonJsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public static string UrlEncode(string text) => HttpUtility.UrlEncode(text); + + public static async Task HttpGetJson(string url, + CancellationToken cancelToken = default) + { + using var client = new HttpClient(); + using var response = await client.GetAsync(url, cancelToken).ConfigureAwait(false); + using var stream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false); + return await JsonSerializer.DeserializeAsync(stream, CommonJsonOptions, cancelToken).ConfigureAwait(false); + } + } +} diff --git a/Utils/RegistryUtils.cs b/Utils/RegistryUtils.cs index 8e58b84..b14a9a5 100644 --- a/Utils/RegistryUtils.cs +++ b/Utils/RegistryUtils.cs @@ -3,15 +3,15 @@ using System; namespace AnotherReplayReader.Utils { - internal static class RegistryUtils + public static class RegistryUtils { - public static string? Retrieve(RegistryHive hive, string path, string value) + public static string? Retrieve32(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; + using var key = view32.OpenSubKey(path, false); + return key?.GetValue(value) as string; } catch (Exception e) { @@ -20,9 +20,24 @@ namespace AnotherReplayReader.Utils } } + public static string? RetrieveInHklm64(string path, string value) + { + try + { + using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64); + using var key = view64?.OpenSubKey(path, false); + return key?.GetValue(value) as string; + } + catch (Exception e) + { + Debug.Instance.DebugMessage += $"Failed to retrieve registy HKLM64:{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); + return Retrieve32(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value); } } } diff --git a/Utils/TaskQueue.cs b/Utils/TaskQueue.cs index eabd2c8..e1f3e8c 100644 --- a/Utils/TaskQueue.cs +++ b/Utils/TaskQueue.cs @@ -21,8 +21,9 @@ namespace AnotherReplayReader.Utils using var locker = new Lock(_lock); _current = _current.ContinueWith(async t => { - await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken); - }, cancelToken); + var task = await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken); + await task.ConfigureAwait(false); + }, cancelToken).Unwrap(); return _current.IgnoreCancel(); } diff --git a/Utils/Verifier.cs b/Utils/Verifier.cs new file mode 100644 index 0000000..baca98f --- /dev/null +++ b/Utils/Verifier.cs @@ -0,0 +1,115 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading; +using System.Windows; + +namespace AnotherReplayReader.Utils +{ + internal class VerifierPayload + { + public string Data { get; } + public string Signature { get; } + [JsonIgnore] + public Lazy ByteData { get; } + [JsonIgnore] + public Lazy ByteSignature { get; } + + public VerifierPayload(string data, string signature) + { + Data = data; + Signature = signature; + ByteData = new(() => Convert.FromBase64String(Data), + LazyThreadSafetyMode.PublicationOnly); + ByteSignature = new(() => Convert.FromBase64String(Signature), + LazyThreadSafetyMode.PublicationOnly); + } + + public static VerifierPayload FromBytes(byte[] data, byte[] signature) + { + return new(Convert.ToBase64String(data), Convert.ToBase64String(signature)); + } + } + + internal class Verifier + { + private const string _publicKey = @"3vw5CoFRDFt2ri4jLDTu75cw1U/tCRjya7q8X/IdULaOJOYG8C+uqrF2Atb4ou+4SrmF+bvJM9cFsf3yO7XpeIDpkxD3KGbIEw+0JixTIIm+y5xlLKDDwbZHnYjJOBTt6JBn0yqwx7vY2UEZIcRU6wlOmUapnkpiaC2anNhSPqk=AQAB"; + + public static bool Verify(VerifierPayload payload) + { + using var rsa = new RSACng(); + rsa.FromXmlString(_publicKey); + return rsa.VerifyData(payload.ByteData.Value, payload.ByteSignature.Value, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + } + + public static void Sign(object sample) + { + var fileName = Path.GetTempFileName(); + try + { + var ea = Enumerable.Repeat(0xEA, 1024).ToArray(); + const string splitter = "|DuplexBarrier|"; + var isByteArray = false; + if (sample is string sampleText) + { + File.WriteAllText(fileName, $"输入私钥信息以及需要签名的数据,用 `{splitter}` 分开\r\n\r\n{sampleText}"); + } + else if (sample is byte[] readyArray) + { + isByteArray = true; + File.WriteAllText(fileName, $"输入私钥信息以及需要签名的数据{splitter}{Convert.ToBase64String(readyArray)}{splitter}"); + } + using (var process = Process.Start("notepad.exe", fileName)) + { + process.WaitForExit(); + } + var splitted = File.ReadAllText(fileName).Split(new[] { splitter }, StringSplitOptions.RemoveEmptyEntries); + File.WriteAllBytes(fileName, ea); + byte[] bytes; + byte[] signature; + + using (var rsa = new RSACng()) + { + rsa.FromXmlString(splitted[0]); + var text = splitted[1]; + splitted = null; + GC.Collect(); + MessageBox.Show(text, $"快来确认一下~"); + bytes = isByteArray + ? Convert.FromBase64String(text) + : Encoding.UTF8.GetBytes(text); + signature = rsa.SignData(bytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + } + GC.Collect(); + var payload = VerifierPayload.FromBytes(bytes, signature); + var serialized = JsonSerializer.Serialize(payload, Network.CommonJsonOptions); + var choice = MessageBox.Show(serialized, "嗯哼", MessageBoxButton.YesNo); + while (choice == MessageBoxResult.Yes) + { + File.WriteAllText(fileName, serialized); + using (var process = Process.Start("notepad.exe", fileName)) + { + process.WaitForExit(); + } + using var rsa = new RSACng(); + rsa.FromXmlString(_publicKey); + var checkContent = File.ReadAllText(fileName); + var check = JsonSerializer.Deserialize(checkContent, Network.CommonJsonOptions) ?? new("", ""); + var checkData = Convert.FromBase64String(check.Data); + var checkSignature = Convert.FromBase64String(check.Signature); + var verified = rsa.VerifyData(checkData, checkSignature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss); + choice = MessageBox.Show($"结果:{verified}", "嗯哼", MessageBoxButton.YesNo); + } + } + finally + { + File.Delete(fileName); + } + } + } +} diff --git a/Utils/WpfExtensions.cs b/Utils/WpfExtensions.cs new file mode 100644 index 0000000..8431141 --- /dev/null +++ b/Utils/WpfExtensions.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Windows; + +namespace AnotherReplayReader.Utils +{ + internal static class WpfExtensions + { + public static IEnumerable FindVisualChildren(this DependencyObject depObj) where T : DependencyObject + { + foreach (var x in LogicalTreeHelper.GetChildren(depObj)) + { + if (x is not DependencyObject child) + { + continue; + } + + if (child is T tchild) + { + yield return tchild; + } + + foreach (var childOfChild in FindVisualChildren(child)) + { + yield return childOfChild; + } + } + } + } +} diff --git a/Window1.xaml.cs b/Window1.xaml.cs index b9ed94e..8a3fd33 100644 --- a/Window1.xaml.cs +++ b/Window1.xaml.cs @@ -1,13 +1,10 @@ using AnotherReplayReader.Utils; using System; -using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Net; using System.Threading.Tasks; -using System.Web; using System.Windows; -using static System.Text.Json.JsonSerializer; namespace AnotherReplayReader { @@ -113,17 +110,11 @@ namespace AnotherReplayReader } } - private static async Task UpdateIpTable(IPAddress ip, string idText) + private async Task UpdateIpTable(IPAddress ip, string idText) { var bytes = ip.GetAddressBytes(); var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3]; - var text = HttpUtility.UrlEncode(idText); - - var key = HttpUtility.UrlEncode(Auth.GetKey()); - var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}"); - using var response = await request.GetResponseAsync().ConfigureAwait(false); - using var stream = response.GetResponseStream(); - return await DeserializeAsync(stream).ConfigureAwait(false); + return await _identity.UpdateIpTable(checked((uint)ipNum), idText).ConfigureAwait(false); } private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) diff --git a/app.manifest b/app.manifest new file mode 100644 index 0000000..cfd94c3 --- /dev/null +++ b/app.manifest @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +