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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+