分离玩家信息插件,以及更新检测
This commit is contained in:
parent
2a96f8efac
commit
08700abd4f
92
About.xaml
92
About.xaml
@ -1,4 +1,5 @@
|
|||||||
<Window x:Class="AnotherReplayReader.About"
|
<Window x:Class="AnotherReplayReader.About"
|
||||||
|
x:ClassModifier="internal"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
@ -6,29 +7,76 @@
|
|||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="About"
|
Title="About"
|
||||||
Height="301.893"
|
Height="420"
|
||||||
Width="429">
|
Width="450"
|
||||||
<Grid Margin="0,0,-8,-59">
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Loaded="OnAboutWindowLoaded">
|
||||||
|
<Grid Margin="0,0,0,-50">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="280"/>
|
<RowDefinition Height="420" />
|
||||||
<RowDefinition Height="50"/>
|
<RowDefinition />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBox x:Name="_idBox" HorizontalAlignment="Left" Margin="54,23,0,10" TextWrapping="Wrap" Text="TextBox" Width="300" BorderBrush="White" RenderTransformOrigin="0.497,0.438" Grid.Row="1"/>
|
<StackPanel Grid.Row="0"
|
||||||
<TextBlock x:Name="textBlock" Margin="36,10,36,32" TextWrapping="Wrap" Grid.RowSpan="2">
|
Orientation="Vertical">
|
||||||
<Run Text="【自动录像机0.7】"/><LineBreak/>
|
<StackPanel x:Name="_updatePanel"
|
||||||
<Run Text="本工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
Orientation="Vertical"
|
||||||
<Run Text="当 Mod 增加新的阵营时,会出现未知阵营和阵营错乱现象"/><LineBreak/>
|
Visibility="Collapsed">
|
||||||
<Run Text="有任何问题可以先去找苏醒或节操"/><LineBreak/>
|
<TextBlock x:Name="_updateInfo"
|
||||||
<Run Text="解析录像的代码主要来源于 louisdx 的研究:"/><LineBreak/>
|
Margin="24,16">
|
||||||
<Hyperlink NavigateUri="https://github.com/louisdx/cnc-replayreaders"><Run Text="https://github.com/louisdx/cnc-replayreaders"/></Hyperlink><LineBreak/>
|
<Run FontWeight="Bold">已经有新版本了呢!</Run>
|
||||||
<Run Text="解析 Big 的代码来源于" />
|
<LineBreak />
|
||||||
<Hyperlink NavigateUri="https://github.com/Qibbi"><Run Text="Jana Mohn" /></Hyperlink>
|
</TextBlock>
|
||||||
<Run Text="的 TechnologyAssembler" /><LineBreak />
|
<Separator />
|
||||||
<Run Text="解析 Tga 的代码来源于Pfim:"/><LineBreak/>
|
</StackPanel>
|
||||||
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
|
<StackPanel Orientation="Vertical"
|
||||||
<Run Text="欢迎来到红警3吧:"/><LineBreak/>
|
Margin="24,16">
|
||||||
<Hyperlink NavigateUri="https://tieba.baidu.com/f?kw=%BA%EC%BE%AF3"><Run Text="https://tieba.baidu.com/f?kw=%BA%EC%BE%AF3"/></Hyperlink><LineBreak/>
|
<TextBlock TextWrapping="Wrap">
|
||||||
<Run Text="ARMod 群号:161660710"/></TextBlock>
|
<Run Text="{Binding Source={x:Static local:App.NameWithVersion},
|
||||||
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="10,23,0,11" TextWrapping="Wrap" Width="60" Grid.Row="1"><Run Text="ID"/><LineBreak/><Run/></TextBlock>
|
Mode=OneWay,
|
||||||
|
StringFormat={}【{0}】}" />
|
||||||
|
<LineBreak />
|
||||||
|
这个工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW<LineBreak />
|
||||||
|
有任何问题可以先去找苏醒或节操问题(<LineBreak />
|
||||||
|
解析录像的代码主要来源于
|
||||||
|
<Hyperlink NavigateUri="https://www.gamereplays.org/community/index.php?showtopic=706067">R Schneider 的研究</Hyperlink>
|
||||||
|
以及 BoolBada 的
|
||||||
|
<Hyperlink NavigateUri="https://github.com/forcecore/KWReplayAutoSaver">KWReplayAutoSaver</Hyperlink>
|
||||||
|
<LineBreak />
|
||||||
|
解析 Big 的代码来源于
|
||||||
|
<Hyperlink NavigateUri="https://github.com/Qibbi">Jana Mohn</Hyperlink>
|
||||||
|
的 TechnologyAssembler<LineBreak />
|
||||||
|
解析 Tga 的代码来源于
|
||||||
|
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim">Pfim</Hyperlink><LineBreak />
|
||||||
|
使用了
|
||||||
|
<Hyperlink NavigateUri="https://github.com/2881099/NPinyin">NPinyin</Hyperlink>
|
||||||
|
以支持按照拼音来查询信息<LineBreak />
|
||||||
|
APM 图表是通过
|
||||||
|
<Hyperlink NavigateUri="https://oxyplot.github.io/">OxyPlot</Hyperlink>
|
||||||
|
画出来的<LineBreak />
|
||||||
|
<LineBreak />
|
||||||
|
欢迎来到红警3吧:
|
||||||
|
<Hyperlink NavigateUri="https://tieba.baidu.com/f?kw=%BA%EC%BE%AF3">https://tieba.baidu.com/ra3</Hyperlink><LineBreak />
|
||||||
|
<Run Text="ARMod 群号:161660710" />
|
||||||
|
</TextBlock>
|
||||||
|
<CheckBox x:Name="_checkForUpdates"
|
||||||
|
Margin="0,16,0,0"
|
||||||
|
Content="自动检查更新"
|
||||||
|
Checked="OnCheckForUpdatesCheckedChanged"
|
||||||
|
Unchecked="OnCheckForUpdatesCheckedChanged" />
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<DockPanel x:Name="_bottom"
|
||||||
|
Grid.Row="1">
|
||||||
|
<Label DockPanel.Dock="Left"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Content="ID" />
|
||||||
|
<TextBox x:Name="_idBox"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
VerticalContentAlignment="Center"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Text="TextBox"
|
||||||
|
BorderBrush="White" />
|
||||||
|
</DockPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -1,22 +1,80 @@
|
|||||||
using System.Diagnostics;
|
using AnotherReplayReader.Utils;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Documents;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// About.xaml 的交互逻辑
|
/// About.xaml 的交互逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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();
|
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>())
|
||||||
|
{
|
||||||
|
hyperlink.RequestNavigate += OnHyperlinkRequestNavigate;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _cache.Initialization;
|
||||||
|
_checkForUpdates.IsChecked = _cache.GetOrDefault(UpdateChecker.CheckForUpdatesKey, false);
|
||||||
|
var data = _updateData
|
||||||
|
?? _cache.GetOrDefault<UpdateCheckerVersionData?>(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)
|
private void OnHyperlinkRequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
||||||
{
|
{
|
||||||
Process.Start(e.Uri.ToString());
|
Process.Start(e.Uri.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async void OnCheckForUpdatesCheckedChanged(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_cache.Set(UpdateChecker.CheckForUpdatesKey, _checkForUpdates.IsChecked is true);
|
||||||
|
await _cache.Save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net461</TargetFramework>
|
<TargetFramework>net461</TargetFramework>
|
||||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||||
|
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
|
||||||
<PublishUrl>publish\</PublishUrl>
|
<PublishUrl>publish\</PublishUrl>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
@ -13,6 +14,7 @@
|
|||||||
<OutputPath>bin\$(Configuration)\</OutputPath>
|
<OutputPath>bin\$(Configuration)\</OutputPath>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Remove="publish\**" />
|
<Compile Remove="publish\**" />
|
||||||
@ -20,9 +22,6 @@
|
|||||||
<None Remove="publish\**" />
|
<None Remove="publish\**" />
|
||||||
<Page Remove="publish\**" />
|
<Page Remove="publish\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
|
||||||
<None Remove="ReplayAutoSaver.cs~RF188cd1d1.TMP" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System.Web" />
|
<Reference Include="System.Web" />
|
||||||
<Reference Include="TechnologyAssembler.Core">
|
<Reference Include="TechnologyAssembler.Core">
|
||||||
@ -31,14 +30,15 @@
|
|||||||
</Reference>
|
</Reference>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ILMerge" Version="3.0.41" />
|
|
||||||
<PackageReference Include="ILMerge.MSBuild.Task" Version="1.0.7" />
|
|
||||||
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
|
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
|
||||||
<PackageReference Include="OxyPlot.Wpf" Version="2.1.0" />
|
<PackageReference Include="OxyPlot.Wpf" Version="2.1.0" />
|
||||||
<PackageReference Include="Pfim" Version="0.10.1" />
|
<PackageReference Include="Pfim" Version="0.10.1" />
|
||||||
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||||
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AnotherReplayReader.PluginSystem\AnotherReplayReader.PluginSystem.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Compile Update="Properties\Settings.Designer.cs">
|
<Compile Update="Properties\Settings.Designer.cs">
|
||||||
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
@ -52,4 +52,16 @@
|
|||||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<Target Name="CustomAfterBuild" AfterTargets="Build">
|
||||||
|
<ItemGroup>
|
||||||
|
<_FilesToMove Include="$(OutputPath)*.dll"/>
|
||||||
|
</ItemGroup>
|
||||||
|
<Message Text="_FilesToMove: @(_FilesToMove->'%(Filename)%(Extension)')" Importance="high"/>
|
||||||
|
<Message Text="DestFiles:
|
||||||
|
@(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')"
|
||||||
|
Importance="high"/>
|
||||||
|
<Move SourceFiles="@(_FilesToMove)"
|
||||||
|
DestinationFiles=
|
||||||
|
"@(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')"/>
|
||||||
|
</Target>
|
||||||
</Project>
|
</Project>
|
@ -3,7 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
|||||||
# Visual Studio Version 16
|
# Visual Studio Version 16
|
||||||
VisualStudioVersion = 16.0.30907.101
|
VisualStudioVersion = 16.0.30907.101
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
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
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
@ -112,7 +112,7 @@ namespace AnotherReplayReader.Apm
|
|||||||
{
|
{
|
||||||
string GetIpAndName(Player player) => player.IsComputer
|
string GetIpAndName(Player player) => player.IsComputer
|
||||||
? "这是 AI"
|
? "这是 AI"
|
||||||
: identity.QueryRealNameAndIP(player.PlayerIp);
|
: identity.QueryRealNameAndIP(player.PlayerIp) ?? string.Empty;
|
||||||
dataList.Add(new("局域网 IP", plotter.Players.Select(GetIpAndName)));
|
dataList.Add(new("局域网 IP", plotter.Players.Select(GetIpAndName)));
|
||||||
}
|
}
|
||||||
apmRowIndex = dataList.Count;
|
apmRowIndex = dataList.Count;
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
Title="APM"
|
Title="APM"
|
||||||
Height="550"
|
Height="550"
|
||||||
Width="800"
|
Width="800"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
Loaded="OnApmWindowLoaded">
|
Loaded="OnApmWindowLoaded">
|
||||||
<Grid Margin="0">
|
<Grid Margin="0">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
@ -83,7 +83,7 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
MessageBox.Show(this, $"加载录像信息失败:\r\n{e}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
44
App.xaml.cs
44
App.xaml.cs
@ -1,4 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
@ -10,8 +13,25 @@ namespace AnotherReplayReader
|
|||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
private int _isInException = 0;
|
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<string> _libsFolder = new(GetLibraryFolder, LazyThreadSafetyMode.PublicationOnly);
|
||||||
|
public static string LibsFolder => _libsFolder.Value;
|
||||||
|
|
||||||
public App()
|
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) =>
|
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||||
{
|
{
|
||||||
if (Interlocked.Increment(ref _isInException) > 1)
|
if (Interlocked.Increment(ref _isInException) > 1)
|
||||||
@ -21,7 +41,7 @@ namespace AnotherReplayReader
|
|||||||
Dispatcher.Invoke(() =>
|
Dispatcher.Invoke(() =>
|
||||||
{
|
{
|
||||||
const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?";
|
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)
|
if (choice == MessageBoxResult.Yes)
|
||||||
{
|
{
|
||||||
Debug.Instance.RequestSave();
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
88
Auth.cs
88
Auth.cs
@ -1,95 +1,31 @@
|
|||||||
using Microsoft.Win32;
|
using AnotherReplayReader.PluginSystem;
|
||||||
|
using AnotherReplayReader.Utils;
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal static class Auth
|
internal static class Auth
|
||||||
{
|
{
|
||||||
public static string? Id { get; }
|
private static readonly TaskCompletionSource<Task<string?>> _source = new();
|
||||||
|
public static Task<string?> Id { get; } = _source.Task.Unwrap();
|
||||||
|
|
||||||
static Auth()
|
public static void LoadPlugin(IPlugin plugin)
|
||||||
{
|
{
|
||||||
Id = null;
|
var authService = plugin.CreateAuthService(RegistryUtils.RetrieveInHklm64, Cache.CacheDirectory);
|
||||||
|
authService.Id.ContinueWith(_source.TrySetResult);
|
||||||
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;
|
public static async Task<byte[]?> IdAsKey()
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var folderPath = Cache.CacheDirectory;
|
var id = await Id.ConfigureAwait(false);
|
||||||
if (!Directory.Exists(folderPath))
|
if (string.IsNullOrEmpty(id))
|
||||||
{
|
|
||||||
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}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetKey()
|
|
||||||
{
|
|
||||||
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))
|
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var bytes = Encoding.UTF8.GetBytes(Id);
|
var bytes = Encoding.UTF8.GetBytes(id);
|
||||||
var destination = Enumerable.Repeat<byte>(0xEA, 24).ToArray();
|
var destination = Enumerable.Repeat<byte>(0xEA, 24).ToArray();
|
||||||
Array.Copy(bytes, destination, Math.Min(bytes.Length, destination.Length));
|
Array.Copy(bytes, destination, Math.Min(bytes.Length, destination.Length));
|
||||||
return destination;
|
return destination;
|
||||||
|
@ -3,6 +3,7 @@ using Microsoft.Win32;
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using TechnologyAssembler.Core.IO;
|
using TechnologyAssembler.Core.IO;
|
||||||
@ -102,6 +103,14 @@ namespace AnotherReplayReader
|
|||||||
DronePlatform.BuildTechnologyAssembler();
|
DronePlatform.BuildTechnologyAssembler();
|
||||||
_skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
|
_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)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
||||||
|
37
Cache.cs
37
Cache.cs
@ -2,14 +2,16 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static System.Text.Json.JsonSerializer;
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
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");
|
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, string> _storage = new();
|
private readonly ConcurrentDictionary<string, string> _storage = new();
|
||||||
@ -27,6 +29,28 @@ namespace AnotherReplayReader
|
|||||||
Directory.CreateDirectory(CacheDirectory);
|
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);
|
using var cacheStream = File.OpenRead(CacheFilePath);
|
||||||
var futureCache = DeserializeAsync<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
|
var futureCache = DeserializeAsync<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
|
||||||
if (await futureCache is not { } cached)
|
if (await futureCache is not { } cached)
|
||||||
@ -42,7 +66,7 @@ namespace AnotherReplayReader
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetOrDefault<T>(string key, in T defaultValue)
|
public T GetOrDefault<T>(string key, T defaultValue)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -55,12 +79,11 @@ namespace AnotherReplayReader
|
|||||||
return defaultValue;
|
return defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Set<T>(string key, in T value)
|
public void Set<T>(string key, T value)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_storage[key] = Serialize(value);
|
_storage[key] = Serialize(value);
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@ -73,7 +96,6 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
_storage[key] = Serialize(value);
|
_storage[key] = Serialize(value);
|
||||||
}
|
}
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
@ -81,10 +103,9 @@ namespace AnotherReplayReader
|
|||||||
public void Remove(string key)
|
public void Remove(string key)
|
||||||
{
|
{
|
||||||
_storage.TryRemove(key, out _);
|
_storage.TryRemove(key, out _);
|
||||||
Save();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void Save()
|
public async Task Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
44
IpAndPlayer.cs
Normal file
44
IpAndPlayer.cs
Normal file
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,7 @@
|
|||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Width="800" Height="600"
|
Title="MainWindow" Width="800" Height="600"
|
||||||
|
WindowStartupLocation="CenterScreen"
|
||||||
Loaded="OnMainWindowLoaded">
|
Loaded="OnMainWindowLoaded">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
@ -125,17 +125,54 @@ namespace AnotherReplayReader
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
Closing += (sender, eventArgs) =>
|
Closing += (sender, eventArgs) =>
|
||||||
{
|
{
|
||||||
_cache.Save();
|
_cache.Save().Wait();
|
||||||
Application.Current.Shutdown();
|
Application.Current.Shutdown();
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnMainWindowLoaded(object sender, EventArgs e)
|
private async void OnMainWindowLoaded(object sender, EventArgs eventArgs)
|
||||||
{
|
{
|
||||||
Debug.Initialize();
|
Debug.Initialize();
|
||||||
|
await _cache.Initialization;
|
||||||
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
|
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
|
||||||
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
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)
|
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
|
||||||
@ -318,6 +355,22 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
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)
|
private async void OnReplaySelectionChanged(object sender, EventArgs e)
|
||||||
@ -334,7 +387,10 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
|
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var aboutWindow = new About();
|
var aboutWindow = new About(_cache)
|
||||||
|
{
|
||||||
|
Owner = this
|
||||||
|
};
|
||||||
aboutWindow.ShowDialog();
|
aboutWindow.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -348,7 +404,7 @@ namespace AnotherReplayReader
|
|||||||
InitialDirectory = _properties.ReplayFolderPath,
|
InitialDirectory = _properties.ReplayFolderPath,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = openFileDialog.ShowDialog();
|
var result = openFileDialog.ShowDialog(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
var fileName = openFileDialog.FileName;
|
var fileName = openFileDialog.FileName;
|
||||||
@ -364,7 +420,10 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
private void OnDetailsButtonClick(object sender, RoutedEventArgs e)
|
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();
|
detailsWindow.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +458,7 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
MessageBox.Show($"无法修复录像:\r\n{exception}");
|
MessageBox.Show(this, $"无法修复录像:\r\n{exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
@ -1,55 +1,29 @@
|
|||||||
using AnotherReplayReader.Utils;
|
using AnotherReplayReader.PluginSystem;
|
||||||
|
using AnotherReplayReader.Utils;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
using static System.Text.Json.JsonSerializer;
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
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
|
internal class PlayerIdentity
|
||||||
{
|
{
|
||||||
|
private class NetworkSerializer : IGenericCallable
|
||||||
|
{
|
||||||
|
public TResult Invoke<TArg, TResult>(TArg arg) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
public Task<TResult?> InvokeAsync<TArg, TResult>(TArg arg)
|
||||||
|
{
|
||||||
|
var url = arg as string ?? throw new NotImplementedException();
|
||||||
|
return Network.HttpGetJson<TResult>(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private const string StoredKey = "pt";
|
private const string StoredKey = "pt";
|
||||||
private const string IvKey = "jw";
|
private const string IvKey = "jw";
|
||||||
private static readonly JsonSerializerOptions _jsonOptions = new()
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
@ -58,6 +32,7 @@ namespace AnotherReplayReader
|
|||||||
};
|
};
|
||||||
private readonly object _lock = new();
|
private readonly object _lock = new();
|
||||||
private readonly Cache _cache;
|
private readonly Cache _cache;
|
||||||
|
private readonly TaskCompletionSource<IPlayerIdentityService> _serviceSource = new();
|
||||||
private IReadOnlyDictionary<uint, string>? _list;
|
private IReadOnlyDictionary<uint, string>? _list;
|
||||||
|
|
||||||
public bool IsUsable => Lock.Run(_lock, () => _list is not null);
|
public bool IsUsable => Lock.Run(_lock, () => _list is not null);
|
||||||
@ -69,24 +44,32 @@ namespace AnotherReplayReader
|
|||||||
Fetch();
|
Fetch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void LoadPlugin(IPlugin plugin)
|
||||||
|
{
|
||||||
|
var service = plugin.CreatePlayerIdentityService(Network.UrlEncode, new NetworkSerializer());
|
||||||
|
_serviceSource.TrySetResult(service);
|
||||||
|
}
|
||||||
|
|
||||||
public Task Fetch()
|
public Task Fetch()
|
||||||
{
|
{
|
||||||
return Task.Run(async () =>
|
return Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
if (await Auth.Id is null)
|
||||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
|
{
|
||||||
using var response = await request.GetResponseAsync().ConfigureAwait(false);
|
return;
|
||||||
using var stream = response.GetResponseStream();
|
}
|
||||||
var list = await DeserializeAsync<List<IpAndPlayer>>(stream, _jsonOptions).ConfigureAwait(false);
|
|
||||||
|
var service = await _serviceSource.Task.ConfigureAwait(false);
|
||||||
|
var list = await service.Fetch<IpAndPlayer>().ConfigureAwait(false);
|
||||||
var converted = list?.ToDictionary(x => x.Ip, x => x.Id);
|
var converted = list?.ToDictionary(x => x.Ip, x => x.Id);
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
{
|
{
|
||||||
_list = converted;
|
_list = converted;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (list is null || Auth.IdAsKey() is not { } encryptKey)
|
if (list is null || await Auth.IdAsKey() is not { } encryptKey)
|
||||||
{
|
{
|
||||||
_cache.Set<string?>(StoredKey, null);
|
_cache.Set<string?>(StoredKey, null);
|
||||||
return;
|
return;
|
||||||
@ -106,6 +89,12 @@ namespace AnotherReplayReader
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateIpTable(uint ip, string idText)
|
||||||
|
{
|
||||||
|
var service = await _serviceSource.Task.ConfigureAwait(false);
|
||||||
|
return await service.UpdateIpTable(ip, idText).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public List<IpAndPlayer> AsSortedList()
|
public List<IpAndPlayer> AsSortedList()
|
||||||
{
|
{
|
||||||
using var locker = new Lock(_lock);
|
using var locker = new Lock(_lock);
|
||||||
@ -116,15 +105,15 @@ namespace AnotherReplayReader
|
|||||||
.ToList() ?? new(0);
|
.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 (ip is 0 || !_list.TryGetValue(ip, out var name))
|
||||||
if (_list is null || !_list.TryGetValue(ip, out var name))
|
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@ -135,6 +124,10 @@ namespace AnotherReplayReader
|
|||||||
public string FormatRealName(uint ip)
|
public string FormatRealName(uint ip)
|
||||||
{
|
{
|
||||||
var name = GetRealName(ip);
|
var name = GetRealName(ip);
|
||||||
|
if (name is null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
if (string.IsNullOrEmpty(name))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
name = IpAndPlayer.SimpleIPToString(ip);
|
name = IpAndPlayer.SimpleIPToString(ip);
|
||||||
@ -142,10 +135,14 @@ namespace AnotherReplayReader
|
|||||||
return $"({name})";
|
return $"({name})";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string QueryRealNameAndIP(uint ip)
|
public string? QueryRealNameAndIP(uint ip)
|
||||||
{
|
{
|
||||||
var ipText = IpAndPlayer.SimpleIPToString(ip);
|
|
||||||
var name = GetRealName(ip);
|
var name = GetRealName(ip);
|
||||||
|
if (name is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var ipText = IpAndPlayer.SimpleIPToString(ip);
|
||||||
if (string.IsNullOrEmpty(name))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
return ipText;
|
return ipText;
|
||||||
@ -160,7 +157,7 @@ namespace AnotherReplayReader
|
|||||||
await _cache.Initialization;
|
await _cache.Initialization;
|
||||||
var stored = _cache.GetOrDefault(StoredKey, string.Empty);
|
var stored = _cache.GetOrDefault(StoredKey, string.Empty);
|
||||||
var iv = Convert.FromBase64String(_cache.GetOrDefault(IvKey, 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;
|
return;
|
||||||
}
|
}
|
||||||
|
78
Plugin.cs
Normal file
78
Plugin.cs
Normal file
@ -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<bool> 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<Assembly> Load(string path)
|
||||||
|
{
|
||||||
|
using var file = File.OpenRead(path);
|
||||||
|
using var metaFile = File.OpenRead($"{path}.meta");
|
||||||
|
var metaTask = JsonSerializer.DeserializeAsync<VerifierPayload>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
90
UpdateChecker.cs
Normal file
90
UpdateChecker.cs
Normal file
@ -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<string> Urls { get; }
|
||||||
|
|
||||||
|
public UpdateCheckerVersionData(string newVersion,
|
||||||
|
string description,
|
||||||
|
ImmutableArray<string> 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<string> _updateSources = new[]
|
||||||
|
{
|
||||||
|
"https://lanyi.altervista.org/playertable/file.json"
|
||||||
|
}.ToImmutableArray();
|
||||||
|
|
||||||
|
public static Task<UpdateCheckerVersionData> CheckForUpdates(Cache cache)
|
||||||
|
{
|
||||||
|
var taskSource = new TaskCompletionSource<UpdateCheckerVersionData>();
|
||||||
|
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<UpdateCheckerVersionData> DownloadAndVerify(string url)
|
||||||
|
{
|
||||||
|
var payload = await Network.HttpGetJson<VerifierPayload>(url)
|
||||||
|
?? throw new InvalidDataException(nameof(VerifierPayload) + " is null");
|
||||||
|
if (!Verifier.Verify(payload))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException(nameof(VerifierPayload) + " cannot be verified");
|
||||||
|
}
|
||||||
|
return JsonSerializer.Deserialize<UpdateCheckerVersionData>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Utils/Network.cs
Normal file
27
Utils/Network.cs
Normal file
@ -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<T?> HttpGetJson<T>(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<T>(stream, CommonJsonOptions, cancelToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,15 +3,15 @@ using System;
|
|||||||
|
|
||||||
namespace AnotherReplayReader.Utils
|
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
|
try
|
||||||
{
|
{
|
||||||
using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
|
using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
|
||||||
using var ra3Key = view32.OpenSubKey(path, false);
|
using var key = view32.OpenSubKey(path, false);
|
||||||
return ra3Key?.GetValue(value) as string;
|
return key?.GetValue(value) as string;
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,9 @@ namespace AnotherReplayReader.Utils
|
|||||||
using var locker = new Lock(_lock);
|
using var locker = new Lock(_lock);
|
||||||
_current = _current.ContinueWith(async t =>
|
_current = _current.ContinueWith(async t =>
|
||||||
{
|
{
|
||||||
await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
|
var task = await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
|
||||||
}, cancelToken);
|
await task.ConfigureAwait(false);
|
||||||
|
}, cancelToken).Unwrap();
|
||||||
return _current.IgnoreCancel();
|
return _current.IgnoreCancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
115
Utils/Verifier.cs
Normal file
115
Utils/Verifier.cs
Normal file
@ -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<byte[]> ByteData { get; }
|
||||||
|
[JsonIgnore]
|
||||||
|
public Lazy<byte[]> 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 = @"<RSAKeyValue><Modulus>3vw5CoFRDFt2ri4jLDTu75cw1U/tCRjya7q8X/IdULaOJOYG8C+uqrF2Atb4ou+4SrmF+bvJM9cFsf3yO7XpeIDpkxD3KGbIEw+0JixTIIm+y5xlLKDDwbZHnYjJOBTt6JBn0yqwx7vY2UEZIcRU6wlOmUapnkpiaC2anNhSPqk=</Modulus><Exponent>AQAB</Exponent></RSAKeyValue>";
|
||||||
|
|
||||||
|
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<byte>(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<VerifierPayload>(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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
29
Utils/WpfExtensions.cs
Normal file
29
Utils/WpfExtensions.cs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal static class WpfExtensions
|
||||||
|
{
|
||||||
|
public static IEnumerable<T> FindVisualChildren<T>(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<T>(child))
|
||||||
|
{
|
||||||
|
yield return childOfChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,13 +1,10 @@
|
|||||||
using AnotherReplayReader.Utils;
|
using AnotherReplayReader.Utils;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using static System.Text.Json.JsonSerializer;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -113,17 +110,11 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<bool> UpdateIpTable(IPAddress ip, string idText)
|
private async Task<bool> UpdateIpTable(IPAddress ip, string idText)
|
||||||
{
|
{
|
||||||
var bytes = ip.GetAddressBytes();
|
var bytes = ip.GetAddressBytes();
|
||||||
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
||||||
var text = HttpUtility.UrlEncode(idText);
|
return await _identity.UpdateIpTable(checked((uint)ipNum), idText).ConfigureAwait(false);
|
||||||
|
|
||||||
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<bool>(stream).ConfigureAwait(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
|
76
app.manifest
Normal file
76
app.manifest
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||||
|
<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
|
||||||
|
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
|
||||||
|
<security>
|
||||||
|
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<!-- UAC Manifest Options
|
||||||
|
If you want to change the Windows User Account Control level replace the
|
||||||
|
requestedExecutionLevel node with one of the following.
|
||||||
|
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
|
||||||
|
<requestedExecutionLevel level="highestAvailable" uiAccess="false" />
|
||||||
|
|
||||||
|
Specifying requestedExecutionLevel element will disable file and registry virtualization.
|
||||||
|
Remove this element if your application requires this virtualization for backwards
|
||||||
|
compatibility.
|
||||||
|
-->
|
||||||
|
<requestedExecutionLevel level="asInvoker" uiAccess="false" />
|
||||||
|
</requestedPrivileges>
|
||||||
|
</security>
|
||||||
|
</trustInfo>
|
||||||
|
|
||||||
|
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||||
|
<application>
|
||||||
|
<!-- A list of the Windows versions that this application has been tested on
|
||||||
|
and is designed to work with. Uncomment the appropriate elements
|
||||||
|
and Windows will automatically select the most compatible environment. -->
|
||||||
|
|
||||||
|
<!-- Windows Vista -->
|
||||||
|
<!--<supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 7 -->
|
||||||
|
<!--<supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8 -->
|
||||||
|
<!--<supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 8.1 -->
|
||||||
|
<!--<supportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}" />-->
|
||||||
|
|
||||||
|
<!-- Windows 10 -->
|
||||||
|
<!--<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />-->
|
||||||
|
|
||||||
|
</application>
|
||||||
|
</compatibility>
|
||||||
|
|
||||||
|
<!-- Indicates that the application is DPI-aware and will not be automatically scaled by Windows at higher
|
||||||
|
DPIs. Windows Presentation Foundation (WPF) applications are automatically DPI-aware and do not need
|
||||||
|
to opt in. Windows Forms applications targeting .NET Framework 4.6 that opt into this setting, should
|
||||||
|
also set the 'EnableWindowsFormsHighDpiAutoResizing' setting to 'true' in their app.config.
|
||||||
|
|
||||||
|
Makes the application long-path aware. See https://docs.microsoft.com/windows/win32/fileio/maximum-file-path-limitation -->
|
||||||
|
<!--
|
||||||
|
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||||
|
<windowsSettings>
|
||||||
|
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
||||||
|
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
|
||||||
|
</windowsSettings>
|
||||||
|
</application>
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- Enable themes for Windows common controls and dialogs (Windows XP and later) -->
|
||||||
|
<dependency>
|
||||||
|
<dependentAssembly>
|
||||||
|
<assemblyIdentity
|
||||||
|
type="win32"
|
||||||
|
name="Microsoft.Windows.Common-Controls"
|
||||||
|
version="6.0.0.0"
|
||||||
|
processorArchitecture="*"
|
||||||
|
publicKeyToken="6595b64144ccf1df"
|
||||||
|
language="*"
|
||||||
|
/>
|
||||||
|
</dependentAssembly>
|
||||||
|
</dependency>
|
||||||
|
</assembly>
|
Loading…
x
Reference in New Issue
Block a user