Compare commits

..

18 Commits
0.6 ... master

Author SHA1 Message Date
22cfb1f0a9 完善日志 2025-04-08 09:41:56 +02:00
a942768c8d Merge pull request '干掉了浩方查马甲代码' (#1) from 干掉了浩方查马甲代码 into master
Reviewed-on: #1
2025-01-30 19:54:11 +08:00
e16eefa166 fix 2025-01-30 12:53:12 +01:00
66458e831c 干掉了浩方查马甲代码 2024-08-24 01:54:11 +02:00
08700abd4f 分离玩家信息插件,以及更新检测 2021-10-22 08:54:04 +02:00
2a96f8efac 击杀阵亡比 2021-10-20 22:40:03 +02:00
0863b2fd71 APM! 2021-10-20 22:22:19 +02:00
78a7310a3b 修未知指令的十六进制代码 2021-10-19 23:20:09 +02:00
23207b9085 更改命名 2021-10-19 23:17:30 +02:00
6f5c21aa8f 修离线缓存 2021-10-19 23:13:28 +02:00
888ddce4ef 改了一亿个东西,修了一亿个 BUG 2021-10-19 17:42:18 +02:00
5b907309e0 2021-10-14 14:22:34 +02:00
c3a41c0d4e 尝试切换到 SDK 风格的项目文件 2021-10-14 13:22:49 +02:00
b92d1d90ad 支持筛选录像 2021-10-13 20:17:32 +02:00
3f5070df77 2021-10-13 16:47:59 +02:00
6fd0bf0046 修了一堆东西 2021-10-13 16:42:38 +02:00
1f5f1c8e6c 增加按照名字搜索 2021-10-09 12:13:44 +02:00
79d9bf4cba 修复部分录像解析错误 2021-10-08 14:10:13 +02:00
57 changed files with 3832 additions and 2595 deletions

View File

@ -1,63 +0,0 @@
<Window x:Class="AnotherReplayReader.APM"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AnotherReplayReader"
mc:Ignorable="d"
Title="APM"
Height="450"
Width="800">
<Grid>
<Label Margin="20,10,0,0"
HorizontalAlignment="Left"
Height="30"
VerticalAlignment="Top">
选择表格之后按 Ctrl+C 可以复制内容
</Label>
<Button x:Name="_setPlayerButton"
Content="设置玩家信息..."
Margin="0,19,22,0"
VerticalAlignment="Top"
Padding="5,2"
Visibility="Visible"
Click="OnSetPlayerButtonClick"
HorizontalAlignment="Right"/>
<DataGrid x:Name="_table"
Margin="20,40,22,19"
Grid.ColumnSpan="2"
CanUserSortColumns="True">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="MouseDoubleClick"
Handler="OnTableMouseDoubleClick" />
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="项"
Binding="{Binding Path=Name}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家1"
Binding="{Binding Path=Player1Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家2"
Binding="{Binding Path=Player2Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家3"
Binding="{Binding Path=Player3Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家4"
Binding="{Binding Path=Player4Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家5"
Binding="{Binding Path=Player5Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家6"
Binding="{Binding Path=Player6Value}"
IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</Grid>
</Window>

View File

@ -1,230 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace AnotherReplayReader
{
internal class DataValue : IComparable<DataValue>, IComparable
{
public int? NumberValue { get; }
public string Value { get; }
public DataValue(string value)
{
Value = value;
if (int.TryParse(value, out var numberValue))
{
NumberValue = numberValue;
}
}
public override string ToString() => Value;
public int CompareTo(DataValue other)
{
if (NumberValue.HasValue == other.NumberValue.HasValue)
{
if (!NumberValue.HasValue)
{
return Value.CompareTo(other.Value);
}
return NumberValue.Value.CompareTo(other.NumberValue.Value);
}
return NumberValue.HasValue ? 1 : -1;
}
public int CompareTo(object obj)
{
if (obj is DataValue other)
{
return CompareTo(other);
}
throw new NotSupportedException();
}
}
internal class DataRow
{
public string Name { get; }
public DataValue Player1Value => _values[0];
public DataValue Player2Value => _values[1];
public DataValue Player3Value => _values[2];
public DataValue Player4Value => _values[3];
public DataValue Player5Value => _values[4];
public DataValue Player6Value => _values[5];
private readonly IReadOnlyList<DataValue> _values;
public DataRow(string name, IEnumerable<string> values)
{
Name = name;
if (values.Count() < 6)
{
values = values.Concat(new string[6 - values.Count()]);
}
_values = values.Select(x => new DataValue(x)).ToArray();
}
}
internal static class DataTableFactory
{
static public List<DataRow> Get(Replay replay, PlayerIdentity identity)
{
var list = new List<byte>
{
0x0F,
0x5F,
0x12,
0x1B,
0x48,
0x52,
0xFC,
0xFD,
0x01,
0x21,
0x33,
0x34,
0x35,
0x37,
0x47,
0xF6,
0xF9,
0xF5,
0xF8,
0x2A,
0xFA,
0xFB,
0x07,
0x08,
0x05,
0x06,
0x09,
0x00,
0x0A,
0x03,
0x04,
0x28,
0x29,
0x0D,
0x0E,
0x15,
0x14,
0x36,
0x16,
0x2C,
0x1A,
0x4E,
0xFE,
0xFF,
0x32,
0x2E,
0x2F,
0x4B,
0x4C,
0x02,
0x0C,
0x10,
};
var dataList = new List<DataRow>
{
new DataRow("ID", replay.Players.Select(x => x.PlayerName))
};
if (replay.Type == ReplayType.Lan && identity.IsUsable)
{
dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIP))));
}
var commandCounts = replay.GetCommandCounts();
foreach (var command in list)
{
var counts = commandCounts.TryGetValue(command, out var stored) ? stored : new int[replay.Players.Count];
dataList.Add(new DataRow(RA3Commands.GetCommandName(command), counts.Select(x => $"{x}")));
commandCounts.Remove(command);
}
foreach (var commandCount in commandCounts)
{
dataList.Add(new DataRow(RA3Commands.GetCommandName(commandCount.Key), commandCount.Value.Select(x => $"{x}")));
}
return dataList;
}
}
/// <summary>
/// APM.xaml 的交互逻辑
/// </summary>
internal partial class APM : Window
{
private PlayerIdentity _identity;
public APM(Replay replay, PlayerIdentity identity)
{
InitializeComponent();
_identity = identity;
if (_identity.IsUsable)
{
Dispatcher.Invoke(() =>
{
_setPlayerButton.IsEnabled = true;
_setPlayerButton.Visibility = Visibility.Visible;
});
}
else
{
Dispatcher.Invoke(() =>
{
_setPlayerButton.IsEnabled = false;
_setPlayerButton.Visibility = Visibility.Hidden;
});
}
Task.Run(() =>
{
try
{
var dataList = DataTableFactory.Get(replay, _identity);
Dispatcher.Invoke(() =>
{
_table.Items.Clear();
foreach (var row in dataList)
{
_table.Items.Add(row);
}
_table.Columns[1].Header = dataList[0].Player1Value;
_table.Columns[2].Header = dataList[0].Player2Value;
_table.Columns[3].Header = dataList[0].Player3Value;
_table.Columns[4].Header = dataList[0].Player4Value;
_table.Columns[5].Header = dataList[0].Player5Value;
_table.Columns[6].Header = dataList[0].Player6Value;
});
}
catch (Exception e)
{
Dispatcher.Invoke(() =>
{
MessageBox.Show($"加载录像信息失败:\r\n{e}");
});
}
});
}
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
{
var window1 = new Window1(_identity);
window1.ShowDialog();
}
private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_table.SelectAll();
}
}
}

View File

@ -1,4 +1,5 @@
<Window x:Class="AnotherReplayReader.About"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@ -6,28 +7,76 @@
xmlns:local="clr-namespace:AnotherReplayReader"
mc:Ignorable="d"
Title="About"
Height="301.893"
Width="429">
<Grid Margin="0,0,-8,-59">
Height="420"
Width="450"
WindowStartupLocation="CenterOwner"
Loaded="OnAboutWindowLoaded">
<Grid Margin="0,0,0,-50">
<Grid.RowDefinitions>
<RowDefinition Height="280"/>
<RowDefinition Height="50"/>
<RowDefinition Height="420" />
<RowDefinition />
</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"/>
<TextBlock x:Name="textBlock" Margin="50,10,45,32" TextWrapping="Wrap" Grid.RowSpan="2">
<Run Text="【自动录像机0.6】"/><LineBreak/>
<Run Text="本工具目前额外支持以下mod的读取AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
<Run Text="当mod增加新的阵营时会出现未知阵营和阵营错乱现象"/><LineBreak/>
<Run Text="有任何问题可以先去找苏醒或节操"/><LineBreak/>
<Run Text="解析录像的代码主要来源于louisdx的研究"/><LineBreak/>
<Hyperlink NavigateUri="https://github.com/louisdx/cnc-replayreaders"><Run Text="https://github.com/louisdx/cnc-replayreaders"/></Hyperlink><LineBreak/>
<Run Text="解析Big的代码来源于OpenSage"/><LineBreak/>
<Hyperlink NavigateUri="https://github.com/OpenSAGE/OpenSAGE"><Run Text="https://github.com/OpenSAGE/OpenSAGE"/></Hyperlink><LineBreak/>
<Run Text="解析Tga的代码来源于Pfim"/><LineBreak/>
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
<Run Text="RA3吧"/><LineBreak/>
<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/>
<Run Text="ARmod群号656507961"/></TextBlock>
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="10,23,0,11" TextWrapping="Wrap" Width="60" Grid.Row="1"><Run Text="ID"/><LineBreak/><Run/></TextBlock>
<StackPanel Grid.Row="0"
Orientation="Vertical">
<StackPanel x:Name="_updatePanel"
Orientation="Vertical"
Visibility="Collapsed">
<TextBlock x:Name="_updateInfo"
Margin="24,16">
<Run FontWeight="Bold">已经有新版本了呢!</Run>
<LineBreak />
</TextBlock>
<Separator />
</StackPanel>
<StackPanel Orientation="Vertical"
Margin="24,16">
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding Source={x:Static local:App.NameWithVersion},
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>
</Window>

View File

@ -1,34 +1,68 @@
using System;
using System.Collections.Generic;
using AnotherReplayReader.Utils;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
namespace AnotherReplayReader
{
/// <summary>
/// About.xaml 的交互逻辑
/// </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();
_idBox.Text = Auth.ID;
}
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
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;
});
}
}
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();
}
}
}

View File

@ -1,193 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}</ProjectGuid>
<OutputType>WinExe</OutputType>
<RootNamespace>AnotherReplayReader</RootNamespace>
<AssemblyName>AnotherReplayReader</AssemblyName>
<TargetFrameworkVersion>v4.6.1</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<Deterministic>true</Deterministic>
<TargetFrameworkProfile />
<IsWebBootstrapper>false</IsWebBootstrapper>
<PublishUrl>publish\</PublishUrl>
<Install>true</Install>
<InstallFrom>Disk</InstallFrom>
<UpdateEnabled>false</UpdateEnabled>
<UpdateMode>Foreground</UpdateMode>
<UpdateInterval>7</UpdateInterval>
<UpdateIntervalUnits>Days</UpdateIntervalUnits>
<UpdatePeriodically>false</UpdatePeriodically>
<UpdateRequired>false</UpdateRequired>
<MapFileExtensions>true</MapFileExtensions>
<ApplicationRevision>1</ApplicationRevision>
<ApplicationVersion>0.0.1.%2a</ApplicationVersion>
<UseApplicationTrust>false</UseApplicationTrust>
<PublishWizardCompleted>true</PublishWizardCompleted>
<BootstrapperEnabled>true</BootstrapperEnabled>
<TargetFramework>net461</TargetFramework>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<GenerateBindingRedirectsOutputType>true</GenerateBindingRedirectsOutputType>
<PublishUrl>publish\</PublishUrl>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PropertyGroup>
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7.2</LangVersion>
<OutputPath>bin\$(Configuration)\</OutputPath>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<Prefer32Bit>false</Prefer32Bit>
<LangVersion>7.2</LangVersion>
<DocumentationFile>
</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
<ManifestCertificateThumbprint>DB7DFD435909EF54AE3424563219E8449DA47901</ManifestCertificateThumbprint>
</PropertyGroup>
<PropertyGroup>
<ManifestKeyFile>AnotherReplayReader_TemporaryKey.pfx</ManifestKeyFile>
</PropertyGroup>
<PropertyGroup>
<GenerateManifests>true</GenerateManifests>
</PropertyGroup>
<PropertyGroup>
<SignManifests>false</SignManifests>
</PropertyGroup>
<PropertyGroup />
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Data" />
<Compile Remove="publish\**" />
<EmbeddedResource Remove="publish\**" />
<None Remove="publish\**" />
<Page Remove="publish\**" />
</ItemGroup>
<ItemGroup>
<Reference Include="System.Web" />
<Reference Include="System.Web.Extensions" />
<Reference Include="System.Xaml" />
<Reference Include="System.Xml" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
<Reference Include="TechnologyAssembler.Core">
<HintPath>TechnologyAssembler.Core.dll</HintPath>
<Private>true</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="ILMerge">
<Version>3.0.29</Version>
</PackageReference>
<PackageReference Include="ILMerge.MSBuild.Task">
<Version>1.0.7</Version>
</PackageReference>
<PackageReference Include="OpenSage.FileFormats.Big">
<Version>1.0.0</Version>
</PackageReference>
<PackageReference Include="Pfim">
<Version>0.7.0</Version>
</PackageReference>
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
<PackageReference Include="OxyPlot.Wpf" Version="2.1.0" />
<PackageReference Include="Pfim" Version="0.10.1" />
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="9.0.0-preview.7.24405.7" />
<PackageReference Include="System.Text.Json" Version="5.0.2" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Compile Include="About.xaml.cs">
<DependentUpon>About.xaml</DependentUpon>
</Compile>
<Compile Include="APM.xaml.cs">
<DependentUpon>APM.xaml</DependentUpon>
</Compile>
<Compile Include="Auth.cs" />
<Compile Include="Cache.cs" />
<Compile Include="BigMinimapCache.cs" />
<Compile Include="Debug.xaml.cs">
<DependentUpon>Debug.xaml</DependentUpon>
</Compile>
<Compile Include="MinimapReader.cs" />
<Compile Include="ModData.cs" />
<Compile Include="PlayerIdentity.cs" />
<Compile Include="Replay.cs" />
<Compile Include="Window1.xaml.cs">
<DependentUpon>Window1.xaml</DependentUpon>
</Compile>
<Page Include="About.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="APM.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Debug.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="MainWindow.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="CommandChunk.cs" />
<Compile Include="MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Page Include="Window1.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
</ItemGroup>
<ItemGroup>
<Compile Include="Properties\AssemblyInfo.cs">
<SubType>Code</SubType>
</Compile>
<Compile Include="Properties\Resources.Designer.cs">
<AutoGen>True</AutoGen>
<DesignTime>True</DesignTime>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Include="Properties\Settings.Designer.cs">
<Compile Update="Properties\Settings.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
<DependentUpon>Settings.settings</DependentUpon>
<DesignTimeSharedInput>True</DesignTimeSharedInput>
</Compile>
<EmbeddedResource Include="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<None Include="app.config" />
<None Include="Properties\Settings.settings">
</ItemGroup>
<ItemGroup>
<None Update="Properties\Settings.settings">
<Generator>SettingsSingleFileGenerator</Generator>
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
</None>
</ItemGroup>
<ItemGroup>
<BootstrapperPackage Include=".NETFramework,Version=v4.6.1">
<Visible>False</Visible>
<ProductName>Microsoft .NET Framework 4.6.1 %28x86 和 x64%29</ProductName>
<Install>true</Install>
</BootstrapperPackage>
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
<Visible>False</Visible>
<ProductName>.NET Framework 3.5 SP1</ProductName>
<Install>false</Install>
</BootstrapperPackage>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="CustomAfterBuild" AfterTargets="Build">
<ItemGroup>
<_FilesToMove Include="$(OutputPath)*.dll" />
</ItemGroup>
<Message Text="_FilesToMove: @(_FilesToMove->'%(Filename)%(Extension)')" Importance="high" />
<Message Text="DestFiles:&#xD;&#xA; @(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')" Importance="high" />
<Move SourceFiles="@(_FilesToMove)" DestinationFiles="@(_FilesToMove->'$(OutputPath)$(ProjectName)Data\%(Filename)%(Extension)')" />
</Target>
</Project>

View File

@ -1,9 +1,9 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30907.101
# Visual Studio Version 17
VisualStudioVersion = 17.10.34928.147
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
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution

239
Apm/ApmPlotter.cs Normal file
View File

@ -0,0 +1,239 @@
using AnotherReplayReader.ReplayFile;
using OxyPlot;
using OxyPlot.Axes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace AnotherReplayReader.Apm
{
public class ApmPlotterFilterOptions
{
public bool CountUnknowns { get; }
public bool CountAutos { get; }
public bool CountClicks { get; }
public ApmPlotterFilterOptions(bool countUnknowns, bool countAutos, bool countClicks)
{
CountUnknowns = countUnknowns;
CountAutos = countAutos;
CountClicks = countClicks;
}
public bool ShouldSkip(byte command)
{
if (!CountUnknowns && ApmPlotter.IsUnknown(command))
{
return true;
}
if (!CountAutos && ApmPlotter.IsAuto(command))
{
return true;
}
if (!CountClicks && ApmPlotter.IsClick(command))
{
return true;
}
return false;
}
}
internal class ApmPlotter
{
private static readonly ConcurrentDictionary<byte, byte> UnknownCommands;
private static readonly ConcurrentDictionary<byte, byte> AutoCommands;
private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;
public Replay Replay { get; }
public ImmutableArray<Player> Players { get; }
public ImmutableArray<TimeSpan> PlayerLifes { get; }
public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
public TimeSpan ReplayLength { get; }
static ApmPlotter()
{
static KeyValuePair<byte, byte> Create(byte x) => new(x, default);
UnknownCommands = new(RA3Commands.UnknownCommands.Select(Create));
AutoCommands = new(RA3Commands.AutoCommands.Select(Create));
}
public ApmPlotter(Replay replay)
{
if (replay.Body is not { } body)
{
throw new InvalidOperationException("Replay must be fully parsed!");
}
Replay = replay;
// player list without post Commentator
Players = (replay.Players.Last().PlayerName == Replay.PostCommentator
? replay.Players.Take(replay.Players.Length - 1)
: replay.Players).ToImmutableArray();
// get all commands
IEnumerable<CommandChunk> Filter(ReplayChunk chunk)
{
foreach (var command in CommandChunk.Parse(chunk))
{
if (command.PlayerIndex >= Players.Length)
{
Debug.Instance.DebugMessage += $"Unknown command with invalid player index: {command}\r\n";
continue;
}
yield return command;
}
}
var query = from chunk in body
where chunk.Type is 1
select (chunk.Time, Filter(chunk).ToImmutableArray());
_commands = query.ToImmutableArray();
var playerLifes = new TimeSpan[Players.Length];
var threeSeconds = TimeSpan.FromSeconds(3);
var stricterLifes = new TimeSpan[Players.Length];
foreach (var (time, commands) in _commands)
{
var estimatedTime = time + threeSeconds;
foreach (var command in commands)
{
var commandId = command.CommandId;
var playerIndex = command.PlayerIndex;
if (commandId == 0x21)
{
if (playerLifes[playerIndex] < estimatedTime)
{
playerLifes[playerIndex] = estimatedTime;
}
}
else if (!IsUnknown(commandId) && !IsAuto(commandId) && playerIndex >= 0)
{
if (stricterLifes[playerIndex] < estimatedTime)
{
stricterLifes[playerIndex] = estimatedTime;
}
}
}
}
PlayerLifes = playerLifes.ToImmutableArray();
StricterPlayerLifes = stricterLifes.ToImmutableArray();
ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max();
}
public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options)
{
var lists = Players
.Select(x => new List<int> { 0 })
.ToArray();
var currentTime = TimeSpan.Zero;
var currentIndex = 0;
foreach (var (time, commands) in _commands)
{
while (time >= currentTime + resolution)
{
currentTime += resolution;
++currentIndex;
}
foreach (var command in commands)
{
if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
{
continue;
}
var list = lists[command.PlayerIndex];
var gap = currentIndex - list.Count;
if (gap >= 0)
{
list.AddRange(Enumerable.Repeat(0, gap + 1));
}
++lists[command.PlayerIndex][currentIndex];
}
}
var apmMultiplier = 1 / resolution.TotalMinutes;
DataPoint Create(int v, int i) => new(TimeSpanAxis.ToDouble(resolution) * i,
v * apmMultiplier);
return lists.Select(l => l.Select(Create).ToArray()).ToArray();
}
public double[] CalculateAverageApm(ApmPlotterFilterOptions options)
{
var apm = new double[Players.Length];
foreach (var (_, commands) in _commands)
{
foreach (var command in commands)
{
if (options.ShouldSkip(command.CommandId) || command.PlayerIndex < 0)
{
continue;
}
++apm[command.PlayerIndex];
}
}
for (var i = 0; i < apm.Length; ++i)
{
apm[i] /= PlayerLifes[i].TotalMinutes;
}
return apm;
}
public static double[] CalculateInstantApm(DataPoint[][] data, TimeSpan begin, TimeSpan end)
{
return data.Select(playerData =>
{
return playerData
.SkipWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < begin)
.TakeWhile(p => TimeSpanAxis.ToTimeSpan(p.X) < end)
.Average(p => new double?(p.Y)) ?? double.NaN;
}).ToArray();
}
public Dictionary<byte, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
{
var playerCommands = new Dictionary<byte, int[]>();
foreach (var (time, commands) in _commands)
{
if (time < begin)
{
continue;
}
if (time >= end)
{
break;
}
foreach (var command in commands)
{
if (!playerCommands.TryGetValue(command.CommandId, out var commandCount))
{
commandCount = playerCommands[command.CommandId] = new int[Players.Length];
}
if (command.PlayerIndex >= 0 && command.PlayerIndex < Players.Length)
{
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
}
}
}
return playerCommands;
}
public static bool IsUnknown(byte commandId)
{
if (UnknownCommands.ContainsKey(commandId))
{
return true;
}
if (RA3Commands.IsUnknownCommand(commandId))
{
UnknownCommands.TryAdd(commandId, default);
return true;
}
return false;
}
public static bool IsAuto(byte commandId) => AutoCommands.ContainsKey(commandId);
public static bool IsClick(byte commandId) => commandId is 0xF8 or 0xF5;
}
}

146
Apm/DataRow.cs Normal file
View File

@ -0,0 +1,146 @@
using AnotherReplayReader.ReplayFile;
using System;
using System.Collections.Generic;
using System.Linq;
namespace AnotherReplayReader.Apm
{
internal class DataRow
{
public const string AverageApmRow = "APM整局游戏";
public const string PartialApmRow = "APM当前时间段";
public string Name { get; }
public bool IsDisabled { get; }
public DataValue Player1Value => _values[0];
public DataValue Player2Value => _values[1];
public DataValue Player3Value => _values[2];
public DataValue Player4Value => _values[3];
public DataValue Player5Value => _values[4];
public DataValue Player6Value => _values[5];
private readonly IReadOnlyList<DataValue> _values;
public DataRow(string name,
IEnumerable<string> values,
bool isDisabled = false)
{
Name = name;
IsDisabled = isDisabled;
if (values.Count() < 6)
{
values = values.Concat(new string[6 - values.Count()]);
}
_values = values.Select(x => new DataValue(x)).ToArray();
}
public static List<DataRow> GetList(ApmPlotter plotter,
ApmPlotterFilterOptions options,
TimeSpan begin,
TimeSpan end,
out int apmRowIndex)
{
// these commands should appear in this order by default
var orderedCommands = new List<byte>();
orderedCommands.AddRange(
[
0xF5,
0xF8,
0x2A,
0xFA,
0xFB,
0x07,
0x08,
0x05,
0x06,
0x09,
0x00,
0x0A,
0x03,
0x04,
0x28,
0x29,
0x0D,
0x0E,
0x15,
0x14,
0x36,
0x16,
0x2C,
0x1A,
0x4E,
0xFE,
0xFF,
0x32,
0x2E,
0x2F,
0x4B,
0x4C,
0x02,
0x0C,
0x10,
]);
orderedCommands.AddRange(RA3Commands.AutoCommands);
orderedCommands.AddRange(RA3Commands.UnknownCommands);
var dataList = new List<DataRow>
{
new("ID", plotter.Players.Select(x => x.PlayerName))
};
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
if (isPartial)
{
string GetStatus(Player player, int i)
{
if (player.IsComputer)
{
return "这是 AI";
}
if (begin < plotter.StricterPlayerLifes[i])
{
return "存活";
}
return begin < plotter.PlayerLifes[i]
? "变身天眼帝国,或双手离开键盘"
: "可能已离开房间";
}
dataList.Add(new("存活状态(推测)", plotter.Players.Select(GetStatus)));
}
apmRowIndex = dataList.Count;
// kill-death ratio
if (plotter.Replay.Footer?.TryGetKillDeathRatios() is { } kdRatios)
{
var texts = kdRatios
.Take(plotter.Players.Length)
.Select(x => $"{x:0.##}");
dataList.Add(new("击杀阵亡比(存疑)", texts));
}
// get commands
var commandCounts = plotter.GetCommandCounts(begin, end);
// add commands in the order specified by the list
foreach (var command in orderedCommands)
{
var counts = commandCounts.TryGetValue(command, out var stored)
? stored
: new int[plotter.Players.Length];
if (!isPartial || counts.Any(x => x > 0))
{
dataList.Add(new(RA3Commands.GetCommandName(command),
counts.Select(x => $"{x}"),
options.ShouldSkip(command)));
}
commandCounts.Remove(command);
}
// add other commands
foreach (var commandCount in commandCounts)
{
dataList.Add(new(RA3Commands.GetCommandName(commandCount.Key),
commandCount.Value.Select(x => $"{x}"),
options.ShouldSkip(commandCount.Key)));
}
return dataList;
}
}
}

43
Apm/DataValue.cs Normal file
View File

@ -0,0 +1,43 @@
using System;
namespace AnotherReplayReader.Apm
{
internal class DataValue : IComparable<DataValue>, IComparable
{
public int? NumberValue { get; }
public string Value { get; }
public DataValue(string value)
{
Value = value;
if (int.TryParse(value, out var numberValue))
{
NumberValue = numberValue;
}
}
public override string ToString() => Value;
public int CompareTo(DataValue other)
{
if (NumberValue.HasValue == other.NumberValue.HasValue)
{
if (!NumberValue.HasValue)
{
return Value.CompareTo(other.Value);
}
return NumberValue.Value.CompareTo(other.NumberValue!.Value);
}
return NumberValue.HasValue ? 1 : -1;
}
public int CompareTo(object obj)
{
if (obj is DataValue other)
{
return CompareTo(other);
}
throw new NotSupportedException();
}
}
}

122
ApmWindow.xaml Normal file
View File

@ -0,0 +1,122 @@
<Window x:Class="AnotherReplayReader.ApmWindow"
x:ClassModifier="internal"
x:Name="ApmWindowInstance"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AnotherReplayReader"
xmlns:ox="http://oxyplot.org/wpf"
mc:Ignorable="d"
Title="APM"
Height="550"
Width="800"
WindowStartupLocation="CenterOwner"
Loaded="OnApmWindowLoaded">
<Grid Margin="0">
<Grid.RowDefinitions>
<RowDefinition Height="200*" />
<RowDefinition Height="300*" />
</Grid.RowDefinitions>
<GridSplitter Grid.Row="0"
VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Height="6"
BorderBrush="Gray"
BorderThickness="1" />
<DockPanel Grid.Row="0"
Margin="0,0,0,12">
<StackPanel DockPanel.Dock="Bottom"
Margin="16,0"
Orientation="Horizontal">
<Label Content="分辨率"
VerticalAlignment="Center" />
<TextBox x:Name="_resolution"
Width="42"
TextAlignment="Right"
VerticalAlignment="Center"
PreviewTextInput="OnResolutionPreviewTextInput"
TextChanged="OnResolutionTextChanged" />
<Label Content="秒"
VerticalAlignment="Center" />
<CheckBox x:Name="_skipUnknowns"
Margin="24,0,0,0"
Content="忽略未知操作"
IsChecked="True"
VerticalAlignment="Center"
Checked="OnSkipUnknownsCheckedChanged"
Unchecked="OnSkipUnknownsCheckedChanged" />
<CheckBox x:Name="_skipAutos"
Margin="24,0,0,0"
Content="忽略自动操作"
IsChecked="True"
VerticalAlignment="Center"
Checked="OnSkipAutosCheckedChanged"
Unchecked="OnSkipAutosCheckedChanged" />
<CheckBox x:Name="_skipClicks"
Margin="24,0,0,0"
Content="忽略左键点选"
IsChecked="False"
VerticalAlignment="Center"
Checked="OnSkipClicksCheckedChanged"
Unchecked="OnSkipClicksCheckedChanged" />
</StackPanel>
<ox:PlotView x:Name="_plot"
DataContext="{Binding ElementName=ApmWindowInstance, Path=PlotModel}"
Model="{Binding Model}" />
</DockPanel>
<DockPanel Grid.Row="1"
Margin="16,8,16,16">
<DockPanel DockPanel.Dock="Top">
<Label x:Name="_label"
Grid.Column="1"
Margin="0"
VerticalAlignment="Center">
选择表格之后按 Ctrl+C 可以复制内容
</Label>
</DockPanel>
<DataGrid x:Name="_table"
CanUserSortColumns="True"
VirtualizingPanel.ScrollUnit="Pixel">
<DataGrid.Resources>
<Style TargetType="DataGridCell">
<EventSetter Event="MouseDoubleClick"
Handler="OnTableMouseDoubleClick" />
</Style>
<Style TargetType="DataGridRow">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsDisabled}"
Value="True">
<Setter Property="Foreground"
Value="Gray" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="项"
Binding="{Binding Path=Name}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家1"
Binding="{Binding Path=Player1Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家2"
Binding="{Binding Path=Player2Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家3"
Binding="{Binding Path=Player3Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家4"
Binding="{Binding Path=Player4Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家5"
Binding="{Binding Path=Player5Value}"
IsReadOnly="True" />
<DataGridTextColumn Header="玩家6"
Binding="{Binding Path=Player6Value}"
IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Grid>
</Window>

387
ApmWindow.xaml.cs Normal file
View File

@ -0,0 +1,387 @@
using AnotherReplayReader.Apm;
using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using OxyPlot;
using OxyPlot.Annotations;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using System;
using System.ComponentModel;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
namespace AnotherReplayReader
{
/// <summary>
/// APM.xaml 的交互逻辑
/// </summary>
internal partial class ApmWindow : Window
{
public static readonly TimeSpan DefaultResolution = TimeSpan.FromSeconds(15);
private readonly Regex _resolutionRegex = new(@"[^0-9]+");
private readonly Replay _replay;
private readonly Task<ApmPlotter> _plotter;
private readonly ApmWindowPlotController _plotController;
public ApmWindowPlotViewModel PlotModel { get; } = new();
public TimeSpan PlotResolution { get; private set; } = DefaultResolution;
public bool SkipUnknowns { get; private set; } = true;
public bool SkipAutos { get; private set; } = true;
public bool SkipClicks { get; private set; } = false;
public ApmWindow(Replay replay)
{
_replay = replay;
_plotter = Task.Run(() => new ApmPlotter(replay));
_plotController = new(this);
InitializeComponent();
}
public async void FilterData(TimeSpan begin, TimeSpan end, bool updatePlot)
{
await FilterDataAsync(begin, end, updatePlot);
}
private async void OnApmWindowLoaded(object sender, RoutedEventArgs e)
{
_plot.Controller = _plotController;
_resolution.Text = PlotResolution.TotalSeconds.ToString();
_skipUnknowns.IsChecked = SkipUnknowns;
_skipAutos.IsChecked = SkipAutos;
_skipClicks.IsChecked = SkipClicks;
await InitializeApmWindowData();
}
private async Task InitializeApmWindowData()
{
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
_label.Content = "选择表格之后按 Ctrl+C 可以复制内容";
}
private async Task FilterDataAsync(TimeSpan begin, TimeSpan end, bool updatePlot)
{
try
{
await FilterDataAsyncThrowable(begin, end, updatePlot);
}
catch (Exception e)
{
MessageBox.Show(this, $"加载录像信息失败:\r\n{e}");
}
}
private async Task FilterDataAsyncThrowable(TimeSpan begin, TimeSpan end, bool updatePlot)
{
var resolution = PlotResolution;
var options = new ApmPlotterFilterOptions(!SkipUnknowns, !SkipAutos, !SkipClicks);
var (dataList, plotData, replayLength, isPartial) = await Task.Run(async () =>
{
var plotter = await _plotter.ConfigureAwait(false);
var list = DataRow.GetList(plotter, options, begin, end, out var apmIndex);
// avg apm
var avg = plotter.CalculateAverageApm(options);
// data for plotting and partial apm
var data = plotter.GetPoints(resolution, options);
// partial apm
var isPartial = begin > TimeSpan.Zero || end <= plotter.ReplayLength;
if (isPartial)
{
var instantApms = ApmPlotter.CalculateInstantApm(data, begin, end);
string PartialApmToString(double v, int i)
{
if (plotter.Players[i].IsComputer)
{
return "这是 AI";
}
return plotter.PlayerLifes[i] < begin
? "玩家已战败"
: $"{v:0.##}";
}
list.Insert(apmIndex, new(DataRow.PartialApmRow,
instantApms.Select(PartialApmToString)));
}
list.Insert(apmIndex, new(DataRow.AverageApmRow,
avg.Select(v => $"{v:0.##}")));
return (list, data, plotter.ReplayLength, isPartial);
});
if (updatePlot)
{
BuildPlot(plotData, replayLength);
}
_table.Items.Clear();
foreach (var row in dataList)
{
_table.Items.Add(row);
}
_table.Columns[1].Header = dataList[0].Player1Value;
_table.Columns[2].Header = dataList[0].Player2Value;
_table.Columns[3].Header = dataList[0].Player3Value;
_table.Columns[4].Header = dataList[0].Player4Value;
_table.Columns[5].Header = dataList[0].Player5Value;
_table.Columns[6].Header = dataList[0].Player6Value;
if (isPartial)
{
_label.Content = $"{(ShortTimeSpan)begin} 至 {(ShortTimeSpan)end} 之间的 APM 数据";
_label.FontWeight = System.Windows.FontWeights.Bold;
}
else
{
_label.Content = "以下是整局游戏的 APM 数据:";
_label.FontWeight = System.Windows.FontWeights.Normal;
}
}
private void BuildPlot(DataPoint[][] data, TimeSpan replayLength)
{
var model = new PlotModel();
model.Annotations.Add(new ApmWindowPlotTextAnnotation
{
Text = "鼠标左键拖拽选择,滚轮缩放,右键拖拽移动",
});
var maxApm = data.SelectMany(x => x).Max(x => x.Y);
var yaxis = new LinearAxis
{
Position = AxisPosition.Right,
IsZoomEnabled = false,
AbsoluteMinimum = -maxApm / 10,
AbsoluteMaximum = maxApm,
MajorStep = 50,
};
var lengthInSeconds = replayLength.TotalSeconds;
var limit = lengthInSeconds / 10;
var xaxis = new TimeSpanAxis
{
Position = AxisPosition.Bottom,
AbsoluteMinimum = -limit,
AbsoluteMaximum = lengthInSeconds + limit,
};
model.Axes.Add(yaxis);
model.Axes.Add(xaxis);
foreach (var (points, i) in data.Select((v, i) => (v, i)))
{
var series = new LineSeries
{
Title = _replay.Players[i].PlayerName,
};
series.Points.AddRange(points);
model.Series.Add(series);
model.Legends.Add(new Legend());
}
PlotModel.Model = model;
}
private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e)
{
_table.SelectAll();
}
private int NormalizeResolutionInput(string text)
{
if (!int.TryParse(text, out var value))
{
value = (int)PlotResolution.TotalSeconds;
}
return Math.Min(Math.Max(1, value), 3600);
}
private void OnResolutionPreviewTextInput(object sender, TextCompositionEventArgs e)
{
e.Handled = _resolutionRegex.IsMatch(e.Text);
}
private async void OnResolutionTextChanged(object sender, EventArgs e)
{
var seconds = NormalizeResolutionInput(_resolution.Text);
if (Math.Abs(seconds - PlotResolution.TotalSeconds) >= 0.5)
{
PlotResolution = TimeSpan.FromSeconds(seconds);
_resolution.Text = seconds.ToString();
_plotController.DiscardRectangle();
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
}
}
private async void OnSkipUnknownsCheckedChanged(object sender, RoutedEventArgs e)
{
if (_skipUnknowns.IsChecked != SkipUnknowns)
{
SkipUnknowns = _skipUnknowns.IsChecked is true;
_plotController.DiscardRectangle();
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
}
}
private async void OnSkipAutosCheckedChanged(object sender, RoutedEventArgs e)
{
if (_skipAutos.IsChecked != SkipAutos)
{
SkipAutos = _skipAutos.IsChecked is true;
_plotController.DiscardRectangle();
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
}
}
private async void OnSkipClicksCheckedChanged(object sender, RoutedEventArgs e)
{
if (_skipClicks.IsChecked != SkipClicks)
{
SkipClicks = _skipClicks.IsChecked is true;
_plotController.DiscardRectangle();
await FilterDataAsync(TimeSpan.MinValue, TimeSpan.MaxValue, true);
}
}
}
internal class ApmWindowPlotViewModel : INotifyPropertyChanged
{
private PlotModel _model = new();
public event PropertyChangedEventHandler? PropertyChanged;
public PlotModel Model
{
get => _model;
set
{
_model = value;
PropertyChanged?.Invoke(this, new(nameof(Model)));
}
}
}
internal class ApmWindowPlotController : PlotController
{
private readonly ApmWindow _window;
private RectangleAnnotation? _current;
private PlotModel Plot => _window.PlotModel.Model;
private double Resolution => _window.PlotResolution.TotalSeconds;
// private readonly Func<OxyMouseDownEventArgs>
public ApmWindowPlotController(ApmWindow window)
{
_window = window;
}
public void DiscardRectangle()
{
_current = null;
RemoveSelections();
Plot.InvalidatePlot(false);
}
public override bool HandleMouseDown(IView view, OxyMouseDownEventArgs args)
{
TryBeginDragRectagle(args);
return base.HandleMouseDown(view, args);
}
public override bool HandleMouseMove(IView view, OxyMouseEventArgs args)
{
TryDragRectangle(args);
return base.HandleMouseMove(view, args);
}
public override bool HandleMouseUp(IView view, OxyMouseEventArgs args)
{
TryEndDragRectangle();
return base.HandleMouseUp(view, args);
}
private void RemoveSelections()
{
var list = Plot.Annotations
.Select((x, i) => (Item: x, Index: i))
.Where(t => t.Item is RectangleAnnotation)
.Reverse()
.ToArray();
foreach (var (_, index) in list)
{
Plot.Annotations.RemoveAt(index);
}
}
private void TryBeginDragRectagle(OxyMouseDownEventArgs args)
{
if (args.ChangedButton == OxyMouseButton.Left)
{
var x = Plot.Axes[1].InverseTransform(args.Position.X);
x = Math.Round(x / Resolution) * Resolution;
RemoveSelections();
_current = new()
{
ClipByYAxis = true,
Fill = OxyColor.FromArgb(128, 0, 128, 255),
MinimumY = double.NegativeInfinity,
MaximumY = double.PositiveInfinity,
MinimumX = x,
MaximumX = x
};
Plot.Annotations.Add(_current);
Plot.InvalidatePlot(false);
}
}
private void TryDragRectangle(OxyMouseEventArgs args)
{
if (_current is not null)
{
var x = Plot.Axes[1].InverseTransform(args.Position.X);
var offsetMultiplier = Math.Round((x - _current.MinimumX) / Resolution);
x = offsetMultiplier * Resolution;
_current.MaximumX = _current.MinimumX + x;
if (_current.MaximumX < _current.MinimumX)
{
var temp = _current.MinimumX;
_current.MinimumX = _current.MaximumX;
_current.MaximumX = temp;
}
Plot.InvalidatePlot(false);
}
}
private void TryEndDragRectangle()
{
if (_current is not null)
{
if (Math.Abs(_current.MaximumX - _current.MinimumX) < Resolution)
{
RemoveSelections();
Plot.InvalidatePlot(false);
_window.FilterData(TimeSpan.MinValue, TimeSpan.MaxValue, false);
}
else
{
_window.FilterData(TimeSpan.FromSeconds(_current.MinimumX),
TimeSpan.FromSeconds(_current.MaximumX),
false);
}
_current = null;
}
}
}
public class ApmWindowPlotTextAnnotation : Annotation
{
public string Text { get; set; } = string.Empty;
public double X { get; set; } = 8;
public double Y { get; set; } = 8;
public override void Render(IRenderContext rc)
{
base.Render(rc);
double pX = PlotModel.PlotArea.Left + X;
double pY = PlotModel.PlotArea.Top + Y;
rc.DrawMultilineText(new(pX, pY),
Text,
PlotModel.TextColor,
PlotModel.DefaultFont,
PlotModel.DefaultFontSize,
PlotModel.SubtitleFontWeight);
}
}
}

View File

@ -2,7 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AnotherReplayReader"
StartupUri="MainWindow.xaml">
StartupUri="MainWindow.xaml"
ShutdownMode="OnMainWindowClose">
<Application.Resources>
</Application.Resources>

View File

@ -1,8 +1,8 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
namespace AnotherReplayReader
@ -12,5 +12,64 @@ namespace AnotherReplayReader
/// </summary>
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<string> _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)
{
return;
}
Dispatcher.Invoke(() =>
{
const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?";
var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", Name, MessageBoxButton.YesNo);
if (choice == MessageBoxResult.Yes)
{
Debug.Instance.RequestSave();
}
});
};
}
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);
}
}

93
Auth.cs
View File

@ -1,93 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Security.Cryptography;
using Microsoft.Win32;
namespace AnotherReplayReader
{
internal static class Auth
{
public static string ID { get; private set; }
static Auth()
{
ID = null;
var windowsID = null as string;
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(Exception)
{
return;
}
var randomKey = null as string;
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(Exception)
{
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());
}
}
}

View File

@ -1,172 +1,36 @@
using System;
using System.Collections.Generic;
using AnotherReplayReader.Utils;
using Microsoft.Win32;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.Win32;
using OpenSage.FileFormats.Big;
using TechnologyAssembler.Core.IO;
namespace AnotherReplayReader
{
internal sealed class BigMinimapCache
{
private sealed class CacheAdapter
{
public List<string> Bigs { get; set; }
public Dictionary<string, string> MapsToBigs { get; set; }
private readonly object _lock = new();
private SkuDefFileSystemProvider? _skudefFileSystem = null;
public CacheAdapter()
{
Bigs = new List<string>();
MapsToBigs = new Dictionary<string, string>();
}
public BigMinimapCache(string? ra3Directory)
{
Task.Run(() => Initialize(ra3Directory));
}
//private Cache _cache;
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = null;
public BigMinimapCache(Cache cache, string ra3Directory)
public bool TryGetEntry(string path, out Stream? bigEntry)
{
//_cache = cache;
bigEntry = null;
Task.Run(() =>
{
try
{
if (!Directory.Exists(ra3Directory))
{
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
return;
}
var bigSet = ParseSkudefs(Directory.EnumerateFiles(ra3Directory, "*.SkuDef"));
//var cached = _cache.GetOrDefault("bigsCache", new CacheAdapter());
var mapsToBigs = new Dictionary<string, string>();
foreach (var bigPath in bigSet/*.Where(x => !cached.Bigs.Contains(x))*/)
{
if (!File.Exists(bigPath))
{
Debug.Instance.DebugMessage += $"Big {bigPath} does not exist.\r\n";
continue;
}
Debug.Instance.DebugMessage += $"Trying to add Big {bigPath} to big minimap cache...\r\n";
try
{
using (var big = new BigArchive(bigPath))
{
foreach (var entry in big.Entries)
{
if (entry.FullName.EndsWith("_art.tga", StringComparison.OrdinalIgnoreCase))
{
mapsToBigs[entry.FullName] = bigPath;
}
}
}
}
catch(Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when reading big:\r\n {exception}\r\n";
}
}
//cached.Bigs = bigSet.ToList();
//_cache.Set("bigsCache", cached);
_mapsToBigs = mapsToBigs; //cached.MapsToBigs;
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
}
});
}
public static HashSet<string> ParseSkudefs(IEnumerable<string> skudefs)
{
var skudefSet = new HashSet<string>(skudefs.Select(x => x.ToLowerInvariant()));
var unreadSkudefs = new HashSet<string>();
var bigSet = new HashSet<string>();
void ReadSkudefLine(string baseDirectory, string line, string expectedCommand, Action<string> action)
{
try
{
char[] separators = { ' ', '\t' };
line = line.ToLowerInvariant();
var splitted = line.Split(separators, 2, StringSplitOptions.RemoveEmptyEntries);
if (splitted[0].Equals(expectedCommand))
{
var path = splitted[1];
if (!Path.IsPathRooted(path))
{
path = Path.Combine(baseDirectory, path);
}
action(path);
}
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when parsing skudef line:\r\n {exception}\r\n";
}
}
void ReadSkudef(string fileName, Action<string, string> onBaseDirectoryAndLine)
{
try
{
var baseDirectory = Path.GetDirectoryName(fileName).ToLowerInvariant();
foreach (var line in File.ReadAllLines(fileName))
{
onBaseDirectoryAndLine(baseDirectory, line);
}
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when parsing skudef file:\r\n {exception}\r\n";
}
}
foreach (var skudef in skudefSet)
{
ReadSkudef(skudef, (baseDirectory, line) =>
{
ReadSkudefLine(baseDirectory, line, "add-config", x =>
{
if (!skudefSet.Contains(x))
{
unreadSkudefs.Add(x);
}
});
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
});
}
foreach (var skudef in unreadSkudefs)
{
ReadSkudef(skudef, (baseDirectory, line) =>
{
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
});
}
return bigSet;
}
public bool TryGetBigByEntryPath(string path, out BigArchive big)
{
big = null;
if (_mapsToBigs == null)
using var locker = new Lock(_lock);
if (_skudefFileSystem is not { } fs)
{
return false;
}
if (!_mapsToBigs.ContainsKey(path))
if (!fs.FileExists(path))
{
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
return false;
@ -174,58 +38,83 @@ namespace AnotherReplayReader
try
{
var bigPath = _mapsToBigs[path];
big = new BigArchive(bigPath);
if(big.GetEntry(path) == null)
{
//_cache.Remove("bigsCache");
big.Dispose();
big = null;
return false;
}
bigEntry = fs.OpenStream(path, VirtualFileModeType.Open);
return true;
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
big = null;
return false;
}
return true;
return false;
}
public byte[] TryReadBytesFromBig(string path)
private void Initialize(string? ra3Directory)
{
if(_mapsToBigs == null)
{
return null;
}
if(!_mapsToBigs.ContainsKey(path))
{
return null;
}
try
{
var bigPath = _mapsToBigs[path];
using (var big = new BigArchive(path))
if (ra3Directory is null || !Directory.Exists(ra3Directory))
{
var entry = big.GetEntry(path);
using (var stream = entry.Open())
using (var reader = new BinaryReader(stream))
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
return;
}
var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
var currentLanguage_ = $"{currentLanguage}_";
double SkudefVersionSelector(string fullPath)
{
var value = -1.0;
try
{
return reader.ReadBytes((int)entry.Length);
const string skudefPrefix = "RA3";
const int prefix = 1;
const int language = 2;
const int majorVersion = 3;
const int minorVersion = 4;
const int majorVersionMultiplier = 10000;
const int correctLanguageBonus = 1000_0000;
value = 0;
var stem = Path.GetFileNameWithoutExtension(fullPath);
var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
if (!match.Success || match.Groups.Cast<Group>().Any(g => !g.Success))
{
return value;
}
value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0;
value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0;
value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier;
value += int.Parse(match.Groups[minorVersion].Value);
return value;
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n";
}
return value;
}
var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef")
orderby SkudefVersionSelector(p) descending
select p).First();
Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n";
using var locker = new Lock(_lock);
DronePlatform.BuildTechnologyAssembler();
_skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
}
catch (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 query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
//_cache.Remove("bigsCache");
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
}
return null;
}
}
}

View File

@ -3,59 +3,99 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Web.Script.Serialization;
using System.Threading.Tasks;
using static System.Text.Json.JsonSerializer;
namespace AnotherReplayReader
{
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 ConcurrentDictionary<string, string> _storage;
private readonly ConcurrentDictionary<string, string> _storage = new();
public Task Initialization { get; }
public Cache()
{
try
Initialization = Task.Run(async () =>
{
if (!Directory.Exists(CacheDirectory))
try
{
Directory.CreateDirectory(CacheDirectory);
}
if (!Directory.Exists(CacheDirectory))
{
Directory.CreateDirectory(CacheDirectory);
}
var serializer = new JavaScriptSerializer();
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
}
catch
{
_storage = new ConcurrentDictionary<string, string>();
}
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<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
if (await futureCache is not { } cached)
{
return;
}
foreach (var kv in cached)
{
_storage.TryAdd(kv.Key, kv.Value);
}
}
catch { }
});
}
public T GetOrDefault<T>(string key, in T defaultValue)
public T GetOrDefault<T>(string key, T defaultValue)
{
try
{
if (_storage.TryGetValue(key, out var valueString))
{
var serializer = new JavaScriptSerializer();
return serializer.Deserialize<T>(valueString);
return Deserialize<T>(valueString) ?? defaultValue;
}
}
catch { }
return defaultValue;
}
public void Set<T>(string key, in T value)
public void Set<T>(string key, T value)
{
try
{
var serializer = new JavaScriptSerializer();
_storage[key] = serializer.Serialize(value);
Save();
_storage[key] = Serialize(value);
}
catch { }
}
public void SetValues(params (string Key, object Value)[] values)
{
try
{
foreach (var (key, value) in values)
{
_storage[key] = Serialize(value);
}
}
catch { }
}
@ -63,15 +103,14 @@ namespace AnotherReplayReader
public void Remove(string key)
{
_storage.TryRemove(key, out _);
Save();
}
public void Save()
public async Task Save()
{
try
{
var serializer = new JavaScriptSerializer();
File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write);
await SerializeAsync(cacheStream, _storage).ConfigureAwait(false);
}
catch { }
}

179
ChineseEncoding.cs Normal file
View File

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using System.Text;
namespace Ra3.BattleNet.Database.Utils;
public static class ChineseEncoding
{
private const int EncodedChineseTotalLength = 120;
private const int EncodedChineseChecksumLength = 3;
private const int EncodedChineseContentLength = EncodedChineseTotalLength - EncodedChineseChecksumLength;
private const int ChineseCodeSize = 13;
private const int ChineseCodeMask = 0b1111111111111;
private const int AsciiSectionSize = 0x80;
private const int Gb2312EucOffset = 0xA0;
private const int Gb2312RowWidth = 94;
private const int Gb2312LastRow = 87;
private const int Gb2312FirstUnassignedSectionBegin = 10;
private const int Gb2312FirstUnassignedSectionSize = 6;
private const int Gb2312SecondUnassignedSectionBegin = 88;
private const int Base64CodeSize = 6;
private const int Base64CodeMask = 0b111111;
private const string Base64Table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
public static string DecodeChineseFromBase64(string original)
{
BigInteger bits = 0;
int index = 0;
foreach (var c in original)
{
BigInteger value = Base64Table.IndexOf(c);
if (value == -1)
{
throw new Exception("Invalid base64 string");
}
bits = bits | (value << index);
index += Base64CodeSize;
}
BigInteger checksum = 0;
var result = new List<byte>();
for (var bitIndex = 0; bitIndex < EncodedChineseContentLength; bitIndex += ChineseCodeSize)
{
int value = (int)((bits >> bitIndex) & ChineseCodeMask);
checksum += value;
if (value == 0)
{
break;
}
if (value < AsciiSectionSize)
{
if (value < 0x20 || value == 0x7F)
{
throw new Exception("Invalid ASCII");
}
result.Add((byte)value);
}
else
{
value -= AsciiSectionSize;
var index1 = value / Gb2312RowWidth + 1;
var index2 = value % Gb2312RowWidth + 1;
if (index1 >= Gb2312FirstUnassignedSectionBegin)
{
index1 += Gb2312FirstUnassignedSectionSize;
}
if (index1 >= Gb2312SecondUnassignedSectionBegin)
{
throw new Exception("Invalid first byte");
}
if (index2 > Gb2312RowWidth)
{
throw new Exception("Invalid second byte");
}
index1 += Gb2312EucOffset;
index2 += Gb2312EucOffset;
result.Add((byte)index1);
result.Add((byte)index2);
}
}
if ((bits >> EncodedChineseContentLength) != checksum % 8)
{
throw new Exception("Invalid checksum");
}
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
return Encoding.GetEncoding(936).GetString(result.ToArray());
}
public static string EncodeChineseToBase64(string original)
{
BigInteger bits = 0;
int index = 0;
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var bytes = Encoding.GetEncoding(936).GetBytes(original);
BigInteger checksum = 0;
for (var i = 0; i < bytes.Length;)
{
BigInteger c = bytes[i];
if (c < AsciiSectionSize)
{
if (c < 0x20 || c == 0x7F)
{
throw new Exception("Invalid ASCII");
}
bits = bits | (c << index);
index += ChineseCodeSize;
checksum += c;
i++;
}
else
{
if (c <= Gb2312EucOffset || c > (Gb2312EucOffset + Gb2312LastRow))
{
throw new Exception("Invalid first byte");
}
BigInteger c2 = bytes[i + 1];
if (c2 <= Gb2312EucOffset || c2 > (Gb2312EucOffset + Gb2312RowWidth))
{
throw new Exception("Invalid second byte");
}
var index1 = c - Gb2312EucOffset;
if (index1 >= Gb2312FirstUnassignedSectionBegin)
{
var offset = index1 - Gb2312FirstUnassignedSectionBegin;
if (offset < Gb2312FirstUnassignedSectionSize)
{
throw new Exception("AA-AF user defined zone not supported");
}
index1 -= Gb2312FirstUnassignedSectionSize;
}
var index2 = c2 - Gb2312EucOffset;
if (index2 > Gb2312RowWidth)
{
throw new Exception("Invalid second byte");
}
var value = (index1 - 1) * Gb2312RowWidth + (index2 - 1);
value = AsciiSectionSize + value;
bits = bits | (value << index);
index += ChineseCodeSize;
checksum += value;
i += 2;
}
}
bits = bits | ((checksum % 8) << EncodedChineseContentLength);
var result = "";
var bitsIndex = 0;
while (bitsIndex < EncodedChineseTotalLength)
{
int v = (int)((bits >> bitsIndex) & Base64CodeMask);
result += Base64Table[v];
bitsIndex += Base64CodeSize;
}
return result;
}
public static string GetPrettyName(string name)
{
if (name.Length != 20)
{
return name;
}
foreach (var c in name)
{
if (!Base64Table.Contains(c))
{
return name;
}
}
try
{
return DecodeChineseFromBase64(name);
}
catch
{
return name;
}
}
}

View File

@ -1,303 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
internal static class RA3Commands
{
//public static IReadOnlyDictionary<byte, Func<BinaryReader, string>> CommandParser { get; private set; }
public static IReadOnlyDictionary<byte, Action<BinaryReader>> CommandParser => _commandParser;
public static IReadOnlyDictionary<byte, string> CommandNames => _commandNames;
private static Dictionary<byte, Action<BinaryReader>> _commandParser;
private static Dictionary<byte, string> _commandNames;
static RA3Commands()
{
Action<BinaryReader> fixedSizeParser(byte command, int size)
{
return (BinaryReader current) =>
{
var lastByte = current.ReadBytes(size - 2).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
}
};
}
Action<BinaryReader> variableSizeParser (byte command, int offset)
{
return (BinaryReader current) =>
{
var totalBytes = 2;
totalBytes += current.ReadBytes(offset - 2).Length;
for(var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
{
totalBytes += 1;
var size = ((x >> 4) + 1) * 4;
totalBytes += current.ReadBytes(size).Length;
}
totalBytes += 1;
var chk = totalBytes;
};
};
var list = new List<(byte, Func<byte, int, Action<BinaryReader>>, int, string)>
{
(0x00, fixedSizeParser, 45, "展开建筑/建造碉堡(?)"),
(0x03, fixedSizeParser, 17, "开始升级"),
(0x04, fixedSizeParser, 17, "暂停/中止升级"),
(0x05, fixedSizeParser, 20, "开始生产单位或纳米核心"),
(0x06, fixedSizeParser, 20, "暂停/取消生产单位或纳米核心"),
(0x07, fixedSizeParser, 17, "开始建造建筑"),
(0x08, fixedSizeParser, 17, "暂停/取消建造建筑"),
(0x09, fixedSizeParser, 35, "摆放建筑"),
(0x0F, fixedSizeParser, 16, "(未知指令)"),
(0x14, fixedSizeParser, 16, "移动"),
(0x15, fixedSizeParser, 16, "移动攻击A"),
(0x16, fixedSizeParser, 16, "强制移动/碾压G"),
(0x21, fixedSizeParser, 20, "[游戏每3秒自动产生的指令]"),
(0x2C, fixedSizeParser, 29, "队形移动(左右键)"),
(0x32, fixedSizeParser, 53, "释放技能或协议(多个目标,如侦察扫描协议)"),
(0x34, fixedSizeParser, 45, "[游戏自动产生的UUID]"),
(0x35, fixedSizeParser, 1049, "[玩家信息(?)]"),
(0x36, fixedSizeParser, 16, "倒车移动D"),
(0x5F, fixedSizeParser, 11, "(未知指令)"),
(0x0A, variableSizeParser, 2, "出售建筑"),
(0x0D, variableSizeParser, 2, "右键攻击"),
(0x0E, variableSizeParser, 2, "强制攻击Ctrl"),
(0x12, variableSizeParser, 2, "(未知指令)"),
(0x1A, variableSizeParser, 2, "停止S"),
(0x1B, variableSizeParser, 2, "(未知指令)"),
(0x28, variableSizeParser, 2, "开始维修建筑"),
(0x29, variableSizeParser, 2, "停止维修建筑"),
(0x2A, variableSizeParser, 2, "选择所有单位Q"),
(0x2E, variableSizeParser, 2, "切换警戒/侵略/固守/停火模式"),
(0x2F, variableSizeParser, 2, "路径点模式Alt"),
(0x37, variableSizeParser, 2, "[游戏不定期自动产生的指令]"),
(0x47, variableSizeParser, 2, "[游戏在第五帧自动产生的指令]"),
(0x48, variableSizeParser, 2, "(未知指令)"),
(0x4C, variableSizeParser, 2, "删除信标或F9"),
(0x4E, variableSizeParser, 2, "选择协议"),
(0x52, variableSizeParser, 2, "(未知指令)"),
(0xF5, variableSizeParser, 5, "选择单位"),
(0xF6, variableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
(0xF8, variableSizeParser, 4, "鼠标左键单击/取消选择"),
(0xF9, variableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
(0xFA, variableSizeParser, 7, "创建编队"),
(0xFB, variableSizeParser, 2, "选择编队"),
(0xFC, variableSizeParser, 2, "(未知指令)"),
(0xFD, variableSizeParser, 7, "(未知指令)"),
(0xFE, variableSizeParser, 15, "释放技能或协议(无目标)"),
(0xFF, variableSizeParser, 34, "释放技能或协议(单个目标)"),
};
var specialList = new List<(byte, Action<BinaryReader>, string)>
{
(0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"),
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
(0x10, ParseGarrison0x10, "进驻建筑"),
(0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"),
(0x4B, ParsePlaceBeacon0x4B, "信标")
};
_commandParser = new Dictionary<byte, Action<BinaryReader>>();
_commandNames = new Dictionary<byte, string>();
foreach (var (id, maker, size, description) in list)
{
_commandParser.Add(id, maker(id, size));
_commandNames.Add(id, description);
}
foreach (var (id, parser, description) in specialList)
{
_commandParser.Add(id, parser);
_commandNames.Add(id, description);
}
_commandNames.Add(0x4D, "在信标里输入文字");
}
public static void UnknownCommandParser(BinaryReader current, byte commandID)
{
while(true)
{
var value = current.ReadByte();
if (value == 0xFF)
{
break;
}
}
//return $"(未知指令 0x{commandID:2X}";
}
public static string GetCommandName(byte commandID)
{
return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 0x{commandID:2X}";
}
private static void ParseSpecialChunk0x01(BinaryReader current)
{
var firstByte = current.ReadByte();
if (firstByte == 0xFF)
{
return;
}
var sixthByte = current.ReadBytes(5).Last();
if(sixthByte == 0xFF)
{
return;
}
var sixteenthByte = current.ReadBytes(10).Last();
var size = (int)(sixteenthByte + 1) * 4 + 14;
var lastByte = current.ReadBytes(size).Last();
if(lastByte != 0xFF)
{
throw new InvalidDataException();
}
return;
}
private static void ParseSetRallyPoint0x02(BinaryReader current)
{
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseUngarrison0x0C(BinaryReader current)
{
current.ReadByte();
var size = (current.ReadByte() + 1) * 4 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseGarrison0x10(BinaryReader current)
{
var type = current.ReadByte();
var size = -1;
if(type == 0x14)
{
size = 9;
}
else if(type == 0x04)
{
size = 10;
}
var lastByte = current.ReadBytes(size).Last();
if(lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseUUID0x33(BinaryReader current)
{
current.ReadByte();
var firstStringLength = (int)current.ReadByte();
current.ReadBytes(firstStringLength + 1);
var secondStringLength = current.ReadByte() * 2;
var lastByte = current.ReadBytes(secondStringLength + 6).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParsePlaceBeacon0x4B(BinaryReader current)
{
var type = current.ReadByte();
var size = -1;
if(type == 0x04)
{
size = 5;
}
else if(type == 0x07)
{
size = 13;
}
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
}
internal sealed class CommandChunk
{
public byte CommandID { get; private set; }
public int PlayerIndex { get; private set; }
public static List<CommandChunk> Parse(in ReplayChunk chunk)
{
if (chunk.Type != 1)
{
throw new NotImplementedException();
}
int ManglePlayerID(byte ID)
{
return ID / 8 - 2;
}
using (var stream = new MemoryStream(chunk.Data))
using (var reader = new BinaryReader(stream))
{
if(reader.ReadByte() != 1)
{
throw new InvalidDataException("Payload first byte not 1");
}
var list = new List<CommandChunk>();
var numberOfCommands = reader.ReadInt32();
for(var i = 0; i < numberOfCommands; ++i)
{
var commandID = reader.ReadByte();
var playerID = reader.ReadByte();
if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
{
RA3Commands.CommandParser[commandID](reader);
}
else
{
RA3Commands.UnknownCommandParser(reader, commandID);
}
list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) });
}
if(reader.BaseStream.Position != reader.BaseStream.Length)
{
throw new InvalidDataException("Payload not fully parsed");
}
return list;
}
}
}
}

View File

@ -6,8 +6,20 @@
xmlns:local="clr-namespace:AnotherReplayReader"
mc:Ignorable="d"
Title="Debug" Height="450" Width="800">
<Grid>
<TextBox x:Name="_textBox" Margin="10,34,10,10" TextWrapping="Wrap" Text="{Binding Path=DebugMessage, Mode=TwoWay}" ScrollViewer.VerticalScrollBarVisibility="Auto"/>
<Button x:Name="_export" Content="导出日志" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="75" Click="OnExport_Click"/>
</Grid>
<DockPanel Margin="10,10,10,10">
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Margin="0,0,0,10">
<Button Padding="8,2"
Margin="0,0,10,0"
Content="导出日志"
Click="OnExportButtonClick" />
<Button Padding="8,2"
Content="清空日志"
Click="OnClearButtonClick" />
</StackPanel>
<TextBox x:Name="_textBox"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</DockPanel>
</Window>

View File

@ -1,68 +1,119 @@
using System;
using AnotherReplayReader.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using Microsoft.Win32;
namespace AnotherReplayReader
{
public sealed class DebugMessageWrapper : INotifyPropertyChanged
public sealed class DebugMessageWrapper
{
public event PropertyChangedEventHandler PropertyChanged;
public string DebugMessage
public readonly struct Proxy
{
get => _debugMessage;
set
public readonly string Payload;
public Proxy(string text)
{
_debugMessage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DebugMessage"));
Payload = text;
}
public static Proxy operator +(Proxy p, string text)
{
return string.IsNullOrEmpty(p.Payload) ? new Proxy(text) : new Proxy(p.Payload + text);
}
}
private string _debugMessage;
private readonly object _lock = new();
private readonly List<string> _list = new();
public event Action<string>? NewText;
public Proxy DebugMessage
{
get => new();
set
{
var text = value.Payload;
using var locker = new Lock(_lock);
if (NewText is null || _list.Count > 0)
{
_list.Add(text);
if (NewText is not null)
{
text = string.Join(string.Empty, _list);
_list.Clear();
_list.Capacity = 0;
}
else
{
return;
}
}
NewText(text);
}
}
public event Action? RequestedSave;
public void RequestSave() => RequestedSave?.Invoke();
}
/// <summary>
/// Debug.xaml 的交互逻辑
/// </summary>
public sealed partial class Debug : Window
{
public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
public static readonly DebugMessageWrapper Instance = new();
private static readonly object _lock = new();
private static Debug? _window = null;
public Debug()
public static void Initialize()
{
DataContext = Instance;
InitializeComponent();
using var locker = new Lock(_lock);
if (_window is not null)
{
return;
}
var window = new Debug();
window.InitializeComponent();
_window = window;
Instance.NewText += _window.AppendText;
Instance.RequestedSave += _window.ExportInMainWindow;
}
private void OnExport_Click(object sender, RoutedEventArgs e)
{
var saveFileDialog = new SaveFileDialog
{
Filter = "文本文档 (*.txt)|*.txt|所有文件 (*.*)|*.*",
OverwritePrompt = true,
};
public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog());
var result = saveFileDialog.ShowDialog(this);
if (result == true)
protected override void OnClosing(CancelEventArgs e)
{
e.Cancel = true;
Hide();
}
private Debug() { }
private void OnExportButtonClick(object sender, RoutedEventArgs e) => Export(this);
private void OnClearButtonClick(object sender, RoutedEventArgs e) => _textBox.Clear();
private void AppendText(string s) => Dispatcher.InvokeAsync(() => _textBox.AppendText(s));
private void ExportInMainWindow() => Export(Application.Current.MainWindow);
private void Export(Window owner)
{
Dispatcher.Invoke(() =>
{
using (var file = saveFileDialog.OpenFile())
using (var writer = new StreamWriter(file))
var saveFileDialog = new SaveFileDialog
{
writer.Write(Instance.DebugMessage);
Filter = "文本文档 (*.txt)|*.txt|所有文件 (*.*)|*.*",
OverwritePrompt = true,
};
var result = saveFileDialog.ShowDialog(owner);
if (result == true)
{
using var file = saveFileDialog.OpenFile();
using var writer = new StreamWriter(file);
writer.Write(_textBox.Text);
}
}
});
}
}
}

25
DronePlatform.cs Normal file
View File

@ -0,0 +1,25 @@
using AnotherReplayReader.Utils;
using TechnologyAssembler;
using TechnologyAssembler.Core.Diagnostics;
namespace AnotherReplayReader
{
public static class DronePlatform
{
private static readonly object _lock = new();
private static bool _built = false;
public static void BuildTechnologyAssembler()
{
using var locker = new Lock(_lock);
if (_built)
{
return;
}
new TechnologyAssemblerCoreModule().Initialize();
Tracer.SetTraceLevel(7);
Tracer.TraceWrite += (source, type, message) => Debug.Instance.DebugMessage += $"[{source}][{type}] {message}\r\n";
_built = true;
}
}
}

View File

@ -1 +0,0 @@
Veldrid\.SDL2\.dll

View File

@ -1,121 +0,0 @@
{
"General": {
"InputAssemblies": [
"$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll",
"$(TargetDir)Microsoft.Extensions.DependencyModel.dll",
"$(TargetDir)Microsoft.Win32.Primitives.dll",
"$(TargetDir)NativeLibraryLoader.dll",
"$(TargetDir)netstandard.dll",
"$(TargetDir)Newtonsoft.Json.dll",
"$(TargetDir)NLog.dll",
"$(TargetDir)OpenSage.Core.dll",
"$(TargetDir)OpenSage.FileFormats.Big.dll",
"$(TargetDir)OpenSage.FileFormats.dll",
"$(TargetDir)OpenSage.FileFormats.RefPack.dll",
"$(TargetDir)OpenSage.Mathematics.dll",
"$(TargetDir)Pfim.dll",
"$(TargetDir)System.AppContext.dll",
"$(TargetDir)System.Collections.Concurrent.dll",
"$(TargetDir)System.Collections.dll",
"$(TargetDir)System.Collections.NonGeneric.dll",
"$(TargetDir)System.Collections.Specialized.dll",
"$(TargetDir)System.ComponentModel.dll",
"$(TargetDir)System.ComponentModel.EventBasedAsync.dll",
"$(TargetDir)System.ComponentModel.Primitives.dll",
"$(TargetDir)System.ComponentModel.TypeConverter.dll",
"$(TargetDir)System.Console.dll",
"$(TargetDir)System.Data.Common.dll",
"$(TargetDir)System.Diagnostics.Contracts.dll",
"$(TargetDir)System.Diagnostics.Debug.dll",
"$(TargetDir)System.Diagnostics.DiagnosticSource.dll",
"$(TargetDir)System.Diagnostics.FileVersionInfo.dll",
"$(TargetDir)System.Diagnostics.Process.dll",
"$(TargetDir)System.Diagnostics.StackTrace.dll",
"$(TargetDir)System.Diagnostics.TextWriterTraceListener.dll",
"$(TargetDir)System.Diagnostics.Tools.dll",
"$(TargetDir)System.Diagnostics.TraceSource.dll",
"$(TargetDir)System.Diagnostics.Tracing.dll",
"$(TargetDir)System.Drawing.Primitives.dll",
"$(TargetDir)System.Dynamic.Runtime.dll",
"$(TargetDir)System.Globalization.Calendars.dll",
"$(TargetDir)System.Globalization.dll",
"$(TargetDir)System.Globalization.Extensions.dll",
"$(TargetDir)System.IO.Compression.dll",
"$(TargetDir)System.IO.Compression.ZipFile.dll",
"$(TargetDir)System.IO.dll",
"$(TargetDir)System.IO.FileSystem.dll",
"$(TargetDir)System.IO.FileSystem.DriveInfo.dll",
"$(TargetDir)System.IO.FileSystem.Primitives.dll",
"$(TargetDir)System.IO.FileSystem.Watcher.dll",
"$(TargetDir)System.IO.IsolatedStorage.dll",
"$(TargetDir)System.IO.MemoryMappedFiles.dll",
"$(TargetDir)System.IO.Pipes.dll",
"$(TargetDir)System.IO.UnmanagedMemoryStream.dll",
"$(TargetDir)System.Linq.dll",
"$(TargetDir)System.Linq.Expressions.dll",
"$(TargetDir)System.Linq.Parallel.dll",
"$(TargetDir)System.Linq.Queryable.dll",
"$(TargetDir)System.Net.Http.dll",
"$(TargetDir)System.Net.NameResolution.dll",
"$(TargetDir)System.Net.NetworkInformation.dll",
"$(TargetDir)System.Net.Ping.dll",
"$(TargetDir)System.Net.Primitives.dll",
"$(TargetDir)System.Net.Requests.dll",
"$(TargetDir)System.Net.Security.dll",
"$(TargetDir)System.Net.Sockets.dll",
"$(TargetDir)System.Net.WebHeaderCollection.dll",
"$(TargetDir)System.Net.WebSockets.Client.dll",
"$(TargetDir)System.Net.WebSockets.dll",
"$(TargetDir)System.Numerics.Vectors.dll",
"$(TargetDir)System.ObjectModel.dll",
"$(TargetDir)System.Reflection.dll",
"$(TargetDir)System.Reflection.Extensions.dll",
"$(TargetDir)System.Reflection.Primitives.dll",
"$(TargetDir)System.Resources.Reader.dll",
"$(TargetDir)System.Resources.ResourceManager.dll",
"$(TargetDir)System.Resources.Writer.dll",
"$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll",
"$(TargetDir)System.Runtime.CompilerServices.VisualC.dll",
"$(TargetDir)System.Runtime.dll",
"$(TargetDir)System.Runtime.Extensions.dll",
"$(TargetDir)System.Runtime.Handles.dll",
"$(TargetDir)System.Runtime.InteropServices.dll",
"$(TargetDir)System.Runtime.InteropServices.RuntimeInformation.dll",
"$(TargetDir)System.Runtime.Numerics.dll",
"$(TargetDir)System.Runtime.Serialization.Formatters.dll",
"$(TargetDir)System.Runtime.Serialization.Json.dll",
"$(TargetDir)System.Runtime.Serialization.Primitives.dll",
"$(TargetDir)System.Runtime.Serialization.Xml.dll",
"$(TargetDir)System.Security.Claims.dll",
"$(TargetDir)System.Security.Cryptography.Algorithms.dll",
"$(TargetDir)System.Security.Cryptography.Csp.dll",
"$(TargetDir)System.Security.Cryptography.Encoding.dll",
"$(TargetDir)System.Security.Cryptography.Primitives.dll",
"$(TargetDir)System.Security.Cryptography.X509Certificates.dll",
"$(TargetDir)System.Security.Principal.dll",
"$(TargetDir)System.Security.SecureString.dll",
"$(TargetDir)System.Text.Encoding.CodePages.dll",
"$(TargetDir)System.Text.Encoding.dll",
"$(TargetDir)System.Text.Encoding.Extensions.dll",
"$(TargetDir)System.Text.RegularExpressions.dll",
"$(TargetDir)System.Threading.dll",
"$(TargetDir)System.Threading.Overlapped.dll",
"$(TargetDir)System.Threading.Tasks.dll",
"$(TargetDir)System.Threading.Tasks.Parallel.dll",
"$(TargetDir)System.Threading.Thread.dll",
"$(TargetDir)System.Threading.ThreadPool.dll",
"$(TargetDir)System.Threading.Timer.dll",
"$(TargetDir)System.ValueTuple.dll",
"$(TargetDir)System.Xml.ReaderWriter.dll",
"$(TargetDir)System.Xml.XDocument.dll",
"$(TargetDir)System.Xml.XmlDocument.dll",
"$(TargetDir)System.Xml.XmlSerializer.dll",
"$(TargetDir)System.Xml.XPath.dll",
"$(TargetDir)System.Xml.XPath.XDocument.dll"
]
},
"Advanced": {
"AllowWildCards": true
}
}

44
IpAndPlayer.cs Normal file
View 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;
}
}

View File

@ -5,7 +5,9 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:AnotherReplayReader"
mc:Ignorable="d"
Title="MainWindow" Width="800" Height="600">
Title="MainWindow" Width="800" Height="600"
WindowStartupLocation="CenterScreen"
Loaded="OnMainWindowLoaded">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="20"/>
@ -22,8 +24,8 @@
</Grid.ColumnDefinitions>
<Label x:Name="label" Content="录像文件夹" Grid.Column="0" Margin="0,-3,5,8" HorizontalContentAlignment="Right" HorizontalAlignment="Right" Width="75"/>
<TextBox x:Name="_replayFolderPathBox" Grid.Column="1" TextWrapping="Wrap" Text="{Binding Path=ReplayFolderPath, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="OnReplayFolderPathBoxTextChanged" Margin="0,0,0,10" Grid.ColumnSpan="2" />
<Button x:Name="_browseButton" Content="浏览..." Grid.Column="3" Margin="10,0,5,11" Click="OnBrowseButton_Click"/>
<Button x:Name="_aboutButton" Content="关于..." Click="OnAboutButton_Click" Grid.Column="4" Margin="5,0,12,11"/>
<Button x:Name="_browseButton" Content="浏览..." Grid.Column="3" Margin="10,0,5,11" Click="OnBrowseButtonClick"/>
<Button x:Name="_aboutButton" Content="关于..." Click="OnAboutButtonClick" Grid.Column="4" Margin="5,0,12,11"/>
</Grid>
<Grid Grid.Row="2">
<Grid.RowDefinitions>
@ -35,26 +37,57 @@
<ColumnDefinition Width="145"/>
<ColumnDefinition Width="472*"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="_replayFilterBox" Grid.Row="0" Grid.Column="2" Margin="120,10,10,0" TextWrapping="Wrap" Height="20" VerticalAlignment="Top" />
<Grid Grid.Row="0"
Grid.Column="2"
Margin="120,10,10,0"
Height="20"
VerticalAlignment="Top">
<TextBox x:Name="_replayFilterBox"
TextChanged="OnReplayFilterBoxTextChanged" />
<TextBlock IsHitTestVisible="False"
Text="输入录像名称、玩家名称或地图名称等 可以筛选录像"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Margin="10,0,0,0"
Foreground="DarkGray">
<TextBlock.Style>
<Style TargetType="{x:Type TextBlock}">
<Setter Property="Visibility"
Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding Text, ElementName=_replayFilterBox}"
Value="">
<Setter Property="Visibility"
Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</Grid>
<Button x:Name="_refreshButton" Content="刷新" Grid.Column="2" HorizontalAlignment="Left" Margin="0,11,0,0" VerticalAlignment="Top" Width="80" Click="OnReplayFolderPathBoxTextChanged"/>
<DataGrid x:Name="_dataGrid" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2" Margin="0,30,10,16" SelectionMode="Single" SelectionChanged="OnReplaySelectionChanged">
<DataGrid x:Name="_dataGrid" Grid.Row="0" Grid.Column="2" Grid.RowSpan="2" Margin="0,30,10,16"
SelectionMode="Single" SelectionChanged="OnReplaySelectionChanged"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="文件名" Binding="{Binding Path=FileName}" Width="4*" IsReadOnly="True"/>
<DataGridTextColumn Header="玩家人数" Binding="{Binding Path=NumberOfPlayingPlayers}" Width="1.5*" IsReadOnly="True"/>
<DataGridTextColumn Header="录像时长" Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
<DataGridTextColumn Header="录像时长"
Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
<DataGridTextColumn Header="Mod" Binding="{Binding Path=Mod}" Width="1.5*" IsReadOnly="True"/>
<DataGridTextColumn Header="录像日期" Binding="{Binding Path=Date, StringFormat='{}{0:yyyy/MM/dd HH:mm:SS}'}" Width="3*" IsReadOnly="True" />
</DataGrid.Columns>
</DataGrid>
<Button x:Name="_debugButton" Content="调试信息" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Click="OnDebugButton_Click" Margin="20,6,0,0" HorizontalAlignment="Left" Width="80"/>
<Button x:Name="_debugButton" Content="调试信息" Grid.Row="0" Grid.Column="0" VerticalAlignment="Top" Click="OnDebugButtonClick" Margin="20,6,0,0" HorizontalAlignment="Left" Width="80"/>
<TextBox x:Name="_replayDetailsBox" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Margin="20,30,20,0" Text="{Binding Path=ReplayDetails}" TextWrapping="Wrap"/>
<Image x:Name="_image" Grid.Row="1" Grid.Column="0" Margin="18,20,0,0" Stretch="Uniform" HorizontalAlignment="Left" Width="120" Height="120" VerticalAlignment="Top" />
<Button x:Name="_playButton" Grid.Row="1" Grid.Column="1" Content="播放录像"
Margin="10,0,20,96"
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayPlayable}" Click="OnPlayReplayButton_Click"/>
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayPlayable}" Click="OnPlayReplayButtonClick"/>
<Button x:Name="_saveAsButton" Grid.Row="1" Grid.Column="1" Content="修复录像"
Margin="10,0,20,56"
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayDamaged}" Click="OnFixReplayButton_Click"/>
Height="35" VerticalAlignment="Bottom" IsEnabled="{Binding Path=ReplayDamaged}" Click="OnFixReplayButtonClick"/>
<Button x:Name="_detailsButton"
Grid.Row="1"
Grid.Column="1"
@ -63,7 +96,7 @@
Height="35"
VerticalAlignment="Bottom"
IsEnabled="{Binding Path=ReplaySelected}"
Click="OnDetailsButton_Click" />
Click="OnDetailsButtonClick" />
</Grid>
</Grid>

View File

@ -1,6 +1,9 @@
using Microsoft.Win32;
using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
@ -9,6 +12,7 @@ using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace AnotherReplayReader
{
@ -16,21 +20,14 @@ namespace AnotherReplayReader
{
public MainWindowProperties()
{
string userDataLeafName = null;
string replayFolderName = null;
using (var view32 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32))
using (var ra3Key = view32.OpenSubKey(@"Software\Electronic Arts\Electronic Arts\Red Alert 3", false))
{
RA3Directory = ra3Key?.GetValue("Install Dir") as string;
userDataLeafName = ra3Key?.GetValue("UserDataLeafName") as string;
replayFolderName = ra3Key?.GetValue("ReplayFolderName") as string;
}
const RegistryHive hklm = RegistryHive.LocalMachine;
RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
if (string.IsNullOrWhiteSpace(userDataLeafName))
{
userDataLeafName = "Red Alert 3";
}
if (string.IsNullOrWhiteSpace(replayFolderName))
{
replayFolderName = "Replays";
@ -42,17 +39,21 @@ namespace AnotherReplayReader
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
}
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged<T>(T value, [CallerMemberName] string propertyName = "")
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
if (!Equals(target, newValue))
{
target = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
public string RA3Directory { get; }
public string? RA3Directory { get; }
public string RA3ReplayFolderPath { get; }
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
public string CustomMapsDirectory { get; }
@ -60,46 +61,38 @@ namespace AnotherReplayReader
public string ReplayFolderPath
{
get { return _replayFolderPath; }
set { _replayFolderPath = value; NotifyPropertyChanged(_replayFolderPath); }
get => _replayFolderPath;
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
}
public string ReplayFilterString
public string? ReplayDetails
{
get { return _replayFilterString; }
set { _replayFilterString = value; NotifyPropertyChanged(_replayFilterString); }
get => _replayDetails;
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
}
public string ReplayDetails
public bool ReplaySelected => CurrentReplay != null;
public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
public bool ReplayDamaged => CurrentReplay?.HasFooter == false;
public Replay? CurrentReplay
{
get { return _replayDetails; }
set { _replayDetails = value; NotifyPropertyChanged(_replayDetails); }
}
public bool ReplaySelected => _currentReplay != null;
public bool ReplayPlayable => _currentReplay?.HasFooter == true && _currentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
public bool ReplayDamaged => _currentReplay?.HasFooter == false;
public Replay CurrentReplay
{
get { return _currentReplay; }
get => _currentReplay;
set
{
_currentReplay = value;
NotifyPropertyChanged(_currentReplay);
SetAndNotifyPropertyChanged(ref _currentReplay, value);
ReplayDetails = "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayPlayable"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplaySelected"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayDamaged"));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayPlayable)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplaySelected)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ReplayDamaged)));
}
}
private volatile string _replayFolderPath;
private volatile string _replayDetails;
private volatile string _replayFilterString;
private volatile Replay _currentReplay;
private string _replayFolderPath = null!;
private string? _replayDetails;
private Replay? _currentReplay;
}
/// <summary>
@ -107,360 +100,287 @@ namespace AnotherReplayReader
/// </summary>
public partial class MainWindow : Window
{
private MainWindowProperties _properties = new MainWindowProperties();
private volatile List<Replay> _replayList;
private Cache _cache = new Cache();
private PlayerIdentity _playerIdentity;
private BigMinimapCache _minimapCache;
private MinimapReader _minimapReader;
private CancellationTokenSource _loadReplaysToken;
private readonly TaskQueue _taskQueue;
private readonly MainWindowProperties _properties = new();
private readonly Cache _cache = new();
private readonly BigMinimapCache _minimapCache;
private readonly MinimapReader _minimapReader;
private readonly CancelManager _cancelLoadReplays = new();
private readonly CancelManager _cancelFilterReplays = new();
private readonly CancelManager _cancelDisplayReplays = new();
private ReplayPinyinList _replayList;
private ImmutableArray<string> _filterStrings = ImmutableArray<string>.Empty;
private ImmutableArray<Replay> _filteredReplays = ImmutableArray<Replay>.Empty;
public MainWindow()
{
_taskQueue = new(Dispatcher);
_minimapCache = new BigMinimapCache(_properties.RA3Directory);
_minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
_replayList = new();
DataContext = _properties;
InitializeComponent();
var handling = new bool[1] { false };
Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
Closing += (sender, eventArgs) =>
{
if (handling == null || handling[0] == true)
{
return;
}
handling[0] = true;
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
_cache.Save().Wait();
Application.Current.Shutdown();
};
Closing += ((sender, eventArgs) => _cache.Save());
_playerIdentity = new PlayerIdentity(_cache);
_minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory);
_minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory);
LoadReplays();
_ = AutoSaveReplays();
}
private async Task AutoSaveReplays()
private async void OnMainWindowLoaded(object sender, EventArgs eventArgs)
{
const string ourPrefix = "自动保存";
var errorMessageCount = 0;
// filename and last write time
var previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
// filename and file size
var lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
Debug.Initialize();
await _cache.Initialization;
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
_ = _taskQueue.Enqueue(() => LoadReplays(null, token), token);
while (true)
const string permissionKey = "questionAsked";
if (_cache.GetOrDefault(permissionKey, false) is not true)
{
try
_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 changed = (from fileName in Directory.GetFiles(_properties.RA3ReplayFolderPath, "*.RA3Replay")
let info = new FileInfo(fileName)
where !info.Name.StartsWith(ourPrefix)
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
select info).ToList();
foreach (var info in changed)
var about = new About(_cache, updateData)
{
previousFiles[info.FullName] = info.LastWriteTimeUtc;
Owner = this
};
about.ShowDialog();
}
}));
}
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
_properties.CurrentReplay = null;
_image.Source = null;
if (_dataGrid.Items.Count > 0)
{
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
cancelToken.ThrowIfCancellationRequested();
_replayList = new();
var path = _properties.ReplayFolderPath;
if (!Directory.Exists(path))
{
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
return;
}
var result = await Task.Run(() =>
{
var list = new List<Replay>();
var clock = new Stopwatch();
clock.Start();
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
{
cancelToken.ThrowIfCancellationRequested();
try
{
var replay = new Replay(replayPath);
list.Add(replay);
}
var replays = changed.Select(info =>
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
try
{
using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
return new Replay(info.FullName, stream);
}
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
return null;
}
}).Where(replay => replay != null);
var newLastReplays = from replay in replays
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero)
let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds)
where threshold < 40 || endThreshold < 40
select replay;
var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase);
foreach (var savedLastReplay in lastReplays.Keys)
{
if (!toBeChecked.ContainsKey(savedLastReplay))
{
try
{
using (var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream));
}
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
}
}
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
continue;
}
foreach (var kv in toBeChecked)
if (clock.ElapsedMilliseconds > 300)
{
Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
var replay = kv.Value;
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
{
if (fileSize == replay.Size)
{
// skip if size is not changed
Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
continue;
}
}
Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
lastReplays[kv.Key] = replay.Size;
var date = replay.Date;
var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
if (replay.NumberOfPlayingPlayers <= 2)
{
var playingPlayers = from player in replay.Players
let faction = ModData.GetFaction(replay.Mod, player.FactionID)
where faction.Kind != FactionKind.Observer
select $"{player.PlayerName}({faction.Name})";
playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
}
var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
var destinationPath = Path.Combine(_properties.RA3ReplayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
try
{
File.Copy(replay.Path, destinationPath, true);
}
catch (Exception e)
{
throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
}
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
clock.Restart();
}
}
catch (Exception e)
return new ReplayPinyinList(list.ToImmutableArray());
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
_replayList = result;
await FilterReplays(string.Empty, nextSelected, filterToken);
}
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
_cancelDisplayReplays.Reset(cancelToken);
_filteredReplays = _replayList.Replays;
_properties.CurrentReplay = null;
_dataGrid.SelectedItem = null;
_image.Source = null;
if (_filterStrings.Any())
{
_properties.ReplayDetails = "正在筛选符合条件的录像…";
if (_dataGrid.Items.Count > 0)
{
var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
Debug.Instance.DebugMessage += errorString;
_ = Dispatcher.InvokeAsync(() =>
{
try
{
if (Interlocked.Increment(ref errorMessageCount) == 1)
{
MessageBox.Show(errorString);
}
}
finally
{
Interlocked.Decrement(ref errorMessageCount);
}
});
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
await Task.Delay(10 * 1000);
var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
var result = await Task.Run(() =>
{
var query = from replay in list.AsParallel().WithCancellation(cancelToken)
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
select replay.Replay;
return query.ToImmutableArray();
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
_filteredReplays = result;
}
_properties.CurrentReplay = null;
_dataGrid.SelectedItem = null;
_dataGrid.ItemsSource = _filteredReplays;
_dataGrid.Items.Refresh();
_properties.ReplayDetails = message;
if (nextSelected is not null)
{
for (var i = 0; i < _dataGrid.Items.Count; ++i)
{
if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
{
_dataGrid.SelectedIndex = i;
break;
}
}
}
}
private async void LoadReplays(string nextSelected = null)
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
{
const string loadingString = "正在加载录像列表,请稍候";
Dispatcher.Invoke(() =>
if (!IsLoaded)
{
if (_image != null)
{
_image.Source = null;
}
return;
}
cancelToken.ThrowIfCancellationRequested();
_properties.CurrentReplay = null;
_image.Source = null;
_properties.ReplayDetails = replayDetails;
if (_dataGrid != null)
{
_dataGrid.Items.Clear();
}
});
// 开始获取小地图
var mapPath = replay.MapPath;
var minimapTask = _minimapReader.TryReadTargaAsync(replay);
_loadReplaysToken?.Cancel();
_loadReplaysToken = new CancellationTokenSource();
var cancelToken = _loadReplaysToken.Token;
var path = _properties.ReplayFolderPath;
var task = Task.Run(async () =>
// 解析录像内容
try
{
var messages = "";
var replayList = new List<Replay>();
replay = await Task.Run(() => new Replay(replay.Path, true));
cancelToken.ThrowIfCancellationRequested();
}
catch (Exception e) when (e is not OperationCanceledException)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{e}\r\n";
return;
}
if (!Directory.Exists(path))
// 假如小地图变了(这……),那么重新加载小地图
if (replay.MapPath != mapPath)
{
minimapTask.Forget();
minimapTask = _minimapReader.TryReadTargaAsync(replay);
}
var newDetails = replay.GetDetails();
if (_properties.ReplayDetails != newDetails)
{
if (_replayList.Replays.FindIndex(r => r.PathEquals(replay)) is int index)
{
messages = "这个文件夹并不存在。";
_replayList = _replayList.SetItem(index, replay.CloneHeader());
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
await FilterReplays(newDetails, replay.Path, token);
return;
}
else
{
var replays = Directory.EnumerateFiles(path, "*.RA3Replay");
foreach (var replayPath in replays)
{
try
{
var replay = await Task.Run(() => new Replay(replayPath));
replayList.Add(replay);
_properties.ReplayDetails = loadingString + $"\n已加载 {replayList.Count} 个录像";
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
}
cancelToken.ThrowIfCancellationRequested();
}
}
return new { Replays = replayList, Messages = messages };
});
}
_properties.CurrentReplay = replay;
_properties.ReplayDetails = newDetails;
try
{
var result = await task;
_replayList = result.Replays;
var newSource = await minimapTask;
cancelToken.ThrowIfCancellationRequested();
DisplayReplays(result.Messages, nextSelected);
_image.Source = newSource;
/* _image.Width = source.Width;
_image.Height = source.Height; */
}
catch (OperationCanceledException) { }
}
private void DisplayReplays(string message = null, string nextSelected = null)
{
var filtered = _replayList;
Dispatcher.Invoke(() =>
catch (Exception e) when (e is not OperationCanceledException)
{
_properties.CurrentReplay = null;
_dataGrid.Items.Clear();
filtered.ForEach(x => _dataGrid.Items.Add(x));
_properties.ReplayDetails = message;
if (nextSelected != null)
{
for (var i = 0; i < _dataGrid.Items.Count; ++i)
{
var replay = _dataGrid.Items[i] as Replay;
if (replay == null)
{
continue;
}
if (replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
{
_dataGrid.SelectedIndex = i;
OnReplaySelectionChanged(null, null);
break;
}
}
}
});
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
}
}
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
{
LoadReplays();
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:
break;
case jsonMagic:
UpdateChecker.Sign();
break;
}
}
private void OnReplaySelectionChanged(object sender, EventArgs e)
private async void OnReplaySelectionChanged(object sender, EventArgs e)
{
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
if (_properties.CurrentReplay == null)
if (_dataGrid.SelectedItem is not Replay replay)
{
return;
}
Dispatcher.Invoke(() => { _image.Source = null; });
string GetSizeString(double size)
{
if (size > 1024 * 1024)
{
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
}
return $"{Math.Round(size / 1024)}KB";
}
const string formatA = "文件名:{0}\n大小{1}\n";
const string formatB = "地图:{0}\n日期{1}\n长度{2}\n";
const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n";
const string playerListTitle = "玩家列表:\n";
var replay = _properties.CurrentReplay;
var sizeString = GetSizeString(replay.Size);
var lengthString = "录像已损坏,请先修复录像";
if (replay.HasFooter)
{
lengthString = $"{replay.Length}";
}
var replaySaver = "[无法获取保存录像的玩家]";
try
{
replaySaver = replay.ReplaySaver.PlayerName;
}
catch { }
_properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString);
_properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString);
_properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver);
_properties.ReplayDetails += playerListTitle;
foreach (var player in replay.Players)
{
if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator"))
{
break;
}
var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name;
var realName = replay.Type == ReplayType.Lan ? _playerIdentity.QueryRealName(player.PlayerIP) : string.Empty;
_properties.ReplayDetails += $"{player.PlayerName + realName}{factionName}\n";
}
try
{
var mapPath = replay.MapPath.TrimEnd('/');
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
var minimapPath = $"{mapPath}/{mapName}_art.tga";
Dispatcher.Invoke(() =>
{
var source = _minimapReader.TryReadTarga(minimapPath, replay.Mod);
_image.Source = source;
/*_image.Width = source.Width;
_image.Height = source.Height;*/
});
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{exception}\r\n";
}
try
{
replay.ParseBody();
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{exception}\r\n";
}
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
_properties.ReplayDetails = replay.GetDetails();
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
}
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
{
var aboutWindow = new About();
var aboutWindow = new About(_cache)
{
Owner = this
};
aboutWindow.ShowDialog();
}
private void OnBrowseButton_Click(object sender, RoutedEventArgs e)
private void OnBrowseButtonClick(object sender, RoutedEventArgs e)
{
try
{
@ -470,13 +390,12 @@ namespace AnotherReplayReader
InitialDirectory = _properties.ReplayFolderPath,
};
var result = openFileDialog.ShowDialog();
var result = openFileDialog.ShowDialog(this);
if (result == true)
{
var fileName = openFileDialog.FileName;
var directoryName = Path.GetDirectoryName(fileName);
_properties.ReplayFolderPath = directoryName;
LoadReplays(fileName);
}
}
catch (Exception exception)
@ -485,29 +404,28 @@ namespace AnotherReplayReader
}
}
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
private void OnDetailsButtonClick(object sender, RoutedEventArgs e)
{
var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
var detailsWindow = new ApmWindow(_properties.CurrentReplay!)
{
Owner = this
};
detailsWindow.ShowDialog();
}
private void OnDebugButton_Click(object sender, RoutedEventArgs e)
{
var debug = new Debug();
debug.ShowDialog();
}
private void OnDebugButtonClick(object sender, RoutedEventArgs e) => Debug.ShowDialog();
private void OnPlayReplayButton_Click(object sender, RoutedEventArgs e)
private void OnPlayReplayButtonClick(object sender, RoutedEventArgs e)
{
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
}
private void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
private async void OnFixReplayButtonClick(object sender, RoutedEventArgs e)
{
var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay");
try
{
var replay = _properties.CurrentReplay;
var saveFileDialog = new SaveFileDialog
{
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
@ -519,19 +437,31 @@ namespace AnotherReplayReader
var result = saveFileDialog.ShowDialog(this);
if (result == true)
{
using (var file = saveFileDialog.OpenFile())
using (var writer = new BinaryWriter(file))
{
writer.WriteReplay(replay);
}
using var file = saveFileDialog.OpenFile();
using var writer = new BinaryWriter(file);
writer.Write(replay);
}
}
catch (Exception exception)
{
MessageBox.Show($"无法修复录像:\r\n{exception}");
MessageBox.Show(this, $"无法修复录像:\r\n{exception}");
}
LoadReplays();
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token);
}
private async void OnReplayFilterBoxTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
_filterStrings = _replayFilterBox.Text
.Split(',', ' ', '')
.Select(x => x.ToPinyin())
.Where(x => !string.IsNullOrEmpty(x))
.ToImmutableArray()!;
var currentReplayPath = _properties?.CurrentReplay?.Path;
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
}
}
}

View File

@ -1,13 +1,13 @@
using System;
using AnotherReplayReader.ReplayFile;
using Pfim;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using OpenSage.FileFormats.Big;
using Pfim;
using TechnologyAssembler.Core.IO;
namespace AnotherReplayReader
{
@ -24,104 +24,93 @@ namespace AnotherReplayReader
};
private readonly BigMinimapCache _cache;
private readonly string _ra3InstallPath;
private readonly string _mapFolderPath;
private readonly string _modFolderPath;
public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath)
public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath)
{
_cache = cache;
_ra3InstallPath = ra3InstallPath;
_mapFolderPath = mapFolderPath;
_modFolderPath = modFolderPath;
}
public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
public Task<BitmapSource?> TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0)
{
using (var targa = TryGetTarga(path, mod))
{
if(targa == null)
{
return null;
}
var mapPath = replay.MapPath.TrimEnd('/');
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
var minimapPath = $"{mapPath}/{mapName}_art.tga";
return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY));
}
try
{
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
return null;
}
public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
{
using var targa = TryGetTarga(path, mod);
if (targa == null)
{
return null;
}
try
{
var bitmap = BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
bitmap.Freeze();
return bitmap;
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
return null;
}
}
private Targa TryGetTarga(string path, Mod mod)
private Targa? TryGetTarga(string path, Mod mod)
{
var tga = null as Targa;
const string customMapPrefix = "data/maps/internal/";
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
{
var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length));
if(File.Exists(minimapPath))
{
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
}
}
// now, normalize paths
path = path.Replace('/', '\\');
if(!mod.IsRA3)
{
try
{
var modSkudefPaths = Enumerable.Empty<string>();
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
{
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
}
foreach (var modSkudefPath in modSkudefPaths)
{
var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
foreach (var modBigPath in modBigPaths)
{
using (var modBig = new BigArchive(modBigPath))
{
var entry = modBig.GetEntry(path);
if (entry != null)
{
return Targa.Create(entry.Open(), new PfimConfig());
}
}
}
}
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
}
}
if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
{
using (big)
{
return Targa.Create(big.GetEntry(path).Open(), new PfimConfig());
}
}
if (Directory.Exists(_ra3InstallPath))
{
var minimapPath = Path.Combine(_mapFolderPath, path);
if (File.Exists(minimapPath))
{
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
}
}
if (!mod.IsRA3)
{
var modSkudefPaths = Enumerable.Empty<string>();
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
{
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
}
DronePlatform.BuildTechnologyAssembler();
foreach (var modSkudefPath in modSkudefPaths)
{
try
{
using var fs = new SkuDefFileSystemProvider("modConfig", modSkudefPath);
if (!fs.FileExists(path))
{
continue;
}
using var stream = fs.OpenStream(path, VirtualFileModeType.Open);
return Targa.Create(stream, new PfimConfig());
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when reading minimap from mod bigs:\r\n {exception}\r\n";
}
}
}
if (_cache != null && _cache.TryGetEntry(path, out var big))
{
using (big)
{
return Targa.Create(big, new PfimConfig());
}
}
return null;
}
}

View File

@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
@ -42,13 +39,13 @@ namespace AnotherReplayReader
public int CompareTo(object other)
{
if(!(other is Mod))
if (!(other is Mod))
{
return GetType().FullName.CompareTo(other.GetType().FullName);
}
var otherMod = (Mod)other;
if(IsRA3 != otherMod.IsRA3)
if (IsRA3 != otherMod.IsRA3)
{
if (IsRA3)
{
@ -84,20 +81,13 @@ namespace AnotherReplayReader
internal static class ModData
{
private static readonly IReadOnlyDictionary<int, Faction> _ra3Factions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3ARFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3CoronaFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3DawnFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3INSFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3FSFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3EisenreichFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3TNWFactions;
private static readonly IReadOnlyDictionary<int, Faction> _ra3WOPFactions;
private static readonly Faction _unknown;
private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营");
private static readonly IReadOnlyDictionary<string, IReadOnlyDictionary<int, Faction>> _factions;
static ModData()
{
_ra3Factions = new Dictionary<int, Faction>
var ra3Factions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -106,8 +96,8 @@ namespace AnotherReplayReader
{ 4, new Faction(FactionKind.Player, "盟军") },
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_ra3ARFactions = new Dictionary<int, Faction>
};
var arFactions = new Dictionary<int, Faction>
{
{ 1, new Faction(FactionKind.Player, "阳炎") },
{ 2, new Faction(FactionKind.Player, "天琼") },
@ -123,7 +113,7 @@ namespace AnotherReplayReader
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
{ 0, new Faction(FactionKind.Player, "AI") },
};
_ra3CoronaFactions = new Dictionary<int, Faction>
var coronaFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -134,7 +124,7 @@ namespace AnotherReplayReader
{ 8, new Faction(FactionKind.Player, "苏联") },
{ 9, new Faction(FactionKind.Player, "神州") },
};
_ra3DawnFactions = new Dictionary<int, Faction>
var dawnFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -147,7 +137,7 @@ namespace AnotherReplayReader
{ 11, new Faction(FactionKind.Player, "随机") },
{ 12, new Faction(FactionKind.Player, "苏联") },
};
_ra3INSFactions = new Dictionary<int, Faction>
var insFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -157,7 +147,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_ra3FSFactions = new Dictionary<int, Faction>
var fsFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -167,7 +157,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_ra3EisenreichFactions = new Dictionary<int, Faction>
var eisenreichFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -177,7 +167,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_ra3TNWFactions = new Dictionary<int, Faction>
var tnwFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -187,7 +177,7 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_ra3WOPFactions = new Dictionary<int, Faction>
var wopFactions = new Dictionary<int, Faction>
{
{ 0, new Faction(FactionKind.Player, "AI") },
{ 1, new Faction(FactionKind.Observer, "观察员") },
@ -197,61 +187,31 @@ namespace AnotherReplayReader
{ 7, new Faction(FactionKind.Player, "随机") },
{ 8, new Faction(FactionKind.Player, "苏联") },
};
_unknown = new Faction(FactionKind.Unknown, "未知阵营");
_factions = new Dictionary<string, IReadOnlyDictionary<int, Faction>>(StringComparer.CurrentCultureIgnoreCase)
{
["RA3"] = ra3Factions,
["Armor Rush"] = arFactions,
["ART"] = arFactions,
["corona"] = coronaFactions,
["Dawn"] = dawnFactions,
["Insurrection"] = insFactions,
["1.12+FS"] = fsFactions,
["Eisenreich"] = eisenreichFactions,
["The New World"] = tnwFactions,
["War Of Powers"] = wopFactions
};
}
public static Faction GetFaction(Mod mod, int factionID)
public static Faction GetFaction(Mod mod, int factionId)
{
new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
if
(mod.ModName.Equals("RA3"))
if (_factions.TryGetValue(mod.ModName, out var table))
{
return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
if (table.TryGetValue(factionId, out var result))
{
return result;
}
}
if (mod.ModName.Equals("Armor Rush"))
{
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("ART"))
{
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("corona"))
{
return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("Dawn"))
{
return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("Insurrection"))
{
return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("1.12+FS"))
{
return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("Eisenreich"))
{
return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("The New World"))
{
return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
if (mod.ModName.Equals("War Of Powers"))
{
return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
}
return _unknown;
}
}

View File

@ -1,159 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Web;
using System.Web.Script.Serialization;
using System.Text;
using System.Threading.Tasks;
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}";
}
public uint IP
{
get { return _ip; }
set
{
_ip = value;
IPString = SimpleIPToString(_ip);
}
}
public string IPString { get; private set; }
public string ID { get; set; }
private uint _ip;
}
internal class PlayerIdentity
{
public bool IsUsable => IsListUsable(_list);
private Cache _cache;
private volatile IReadOnlyDictionary<uint, string> _list;
public PlayerIdentity(Cache cache)
{
_cache = cache;
Fetch();
try
{
var stored = _cache.GetOrDefault("pt", string.Empty);
if (string.IsNullOrWhiteSpace(stored))
{
return;
}
var bytes = Convert.FromBase64String(stored);
var id = Encoding.UTF8.GetBytes(Auth.ID);
var data = Encoding.UTF8.GetString(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
var serializer = new JavaScriptSerializer();
var cachedTable = serializer.Deserialize<List<IPAndPlayer>>(data);
if(cachedTable == null)
{
_list = null;
return;
}
var converted = cachedTable.ToDictionary(x => x.IP, x => x.ID);
converted[0] = "【没有网络连接,正在使用上次保存的数据】";
_list = converted;
}
catch { }
}
public static bool IsListUsable(IReadOnlyDictionary<uint, string> list)
{
return list != null && list.Count != 0;
}
public Task Fetch()
{
return Task.Run(() =>
{
try
{
var key = HttpUtility.UrlEncode(Auth.GetKey());
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
using (var stream = request.GetResponse().GetResponseStream())
using (var reader = new StreamReader(stream))
{
var response = reader.ReadToEnd();
var serializer = new JavaScriptSerializer();
var temp = serializer.Deserialize<List<IPAndPlayer>>(response);
if(temp == null)
{
_list = null;
return;
}
var converted = temp.ToDictionary(x => x.IP, x => x.ID);
_list = converted;
var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp));
var id = Encoding.UTF8.GetBytes(Auth.ID);
var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
_cache.Set("pt", base64);
}
}
catch { }
});
}
public List<IPAndPlayer> AsSortedList()
{
var list = _list;
if(!IsListUsable(list))
{
return new List<IPAndPlayer>();
}
return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList();
}
public string QueryRealName(uint ip)
{
var list = _list;
if (!IsListUsable(list))
{
return string.Empty;
}
if(ip == 0)
{
return string.Empty;
}
var name = list.TryGetValue(ip, out var realName) ? realName : IPAndPlayer.SimpleIPToString(ip);
return $"{name}";
}
public string QueryRealNameAndIP(uint ip)
{
var list = _list;
if (!IsListUsable(list))
{
return string.Empty;
}
if (ip == 0)
{
return string.Empty;
}
var name = list.TryGetValue(ip, out var realName) ? realName + "" : string.Empty;
return name + IPAndPlayer.SimpleIPToString(ip);
}
}
}

View File

@ -4,18 +4,6 @@ using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// 有关程序集的一般信息由以下
// 控制。更改这些特性值可修改
// 与程序集关联的信息。
[assembly: AssemblyTitle("AnotherReplayReader")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("AnotherReplayReader")]
[assembly: AssemblyCopyright("Copyright © 2019")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// 将 ComVisible 设置为 false 会使此程序集中的类型
//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
//请将此类型的 ComVisible 特性设置为 true。
@ -39,17 +27,3 @@ using System.Windows;
//(未在页面中找到资源时使用,
//、应用程序或任何主题专用资源字典中找到时使用)
)]
// 程序集的版本信息由下列四个值组成:
//
// 主版本
// 次版本
// 生成号
// 修订号
//
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
// 方法是按如下所示使用“*”: :
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("0.0.3.0")]
[assembly: AssemblyFileVersion("0.0.3.0")]

431
Replay.cs
View File

@ -1,431 +0,0 @@
using System;
using System.IO;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
internal sealed class Player
{
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
{
{ "E", "简单" },
{ "M", "中等" },
{ "H", "困难" },
{ "B", "凶残" },
};
public string PlayerName { get; private set; }
public uint PlayerIP { get; private set; }
public string PlayerRealName { get; private set; }
public int FactionID { get; private set; }
public int Team { get; private set; }
public Player(string[] playerEntry)
{
var isComputer = playerEntry[0][0] == 'C';
PlayerName = playerEntry[0].Substring(1);
if (isComputer)
{
PlayerName = ComputerNames[PlayerName];
PlayerIP = 0;
FactionID = int.Parse(playerEntry[2]);
Team = int.Parse(playerEntry[4]);
}
else
{
PlayerIP = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
FactionID = int.Parse(playerEntry[5]);
Team = int.Parse(playerEntry[7]);
}
}
}
internal enum ReplayType
{
Skirmish,
Lan,
Online
}
internal sealed class ReplayChunk
{
public uint TimeCode { get; private set; }
public byte Type { get; private set; }
public byte[] Data { get; private set; }
public ReplayChunk(uint timeCode, BinaryReader reader)
{
TimeCode = timeCode; // reader.ReadUInt32();
Type = reader.ReadByte();
var chunkSize = reader.ReadInt32();
Data = reader.ReadBytes(chunkSize);
if(reader.ReadInt32() != 0)
{
throw new InvalidDataException("Replay Chunk not ended with zero");
}
}
}
internal enum ReplayFooterOption
{
SeekToFooter,
CurrentlyAtFooter,
}
internal sealed class ReplayFooter
{
public const uint Terminator = 0x7FFFFFFF;
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
public uint FinalTimeCode { get; private set; }
public byte[] Data { get; private set; }
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
{
var currentPosition = reader.BaseStream.Position;
reader.BaseStream.Seek(-4, SeekOrigin.End);
var footerLength = reader.ReadInt32();
if (option == ReplayFooterOption.SeekToFooter)
{
currentPosition = reader.BaseStream.Length - footerLength;
}
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
var footer = reader.ReadBytes(footerLength);
if(footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
{
throw new InvalidDataException("Invalid footer");
}
using (var footerStream = new MemoryStream(footer))
using (var footerReader = new BinaryReader(footerStream))
{
var footerString = footerReader.ReadBytes(17);
if(!footerString.SequenceEqual(FooterString))
{
throw new InvalidDataException("Invalid footer, no footer string");
}
FinalTimeCode = footerReader.ReadUInt32();
Data = footerReader.ReadBytes(footer.Length - 25);
if(footerReader.ReadInt32() != footerLength)
{
throw new InvalidDataException();
}
}
}
public ReplayFooter(uint finalTimeCode)
{
FinalTimeCode = finalTimeCode;
Data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
}
List<float> TryGetKillDeathRatio()
{
if(Data.Length < 24)
{
return null;
}
var ratios = new List<float>();
using (var stream = new MemoryStream(Data, Data.Length - 24, 24))
using (var reader = new BinaryReader(stream))
{
ratios.Add(reader.ReadSingle());
}
return ratios;
}
}
internal static class ReplayExtensions
{
public static string ReadUTF16String(this BinaryReader reader)
{
var currentBytes = new List<byte>();
byte[] lastTwoBytes = null;
while(true)
{
lastTwoBytes = reader.ReadBytes(2);
if (lastTwoBytes.Length != 2)
{
throw new InvalidDataException();
}
if(lastTwoBytes.All(x => x == 0))
{
break;
}
currentBytes.AddRange(lastTwoBytes);
}
return Encoding.Unicode.GetString(currentBytes.ToArray());
}
public static void WriteChunk(this BinaryWriter writer, ReplayChunk chunk)
{
writer.Write(chunk.TimeCode);
writer.Write(chunk.Type);
writer.Write(chunk.Data.Length);
writer.Write(chunk.Data);
writer.Write(0);
}
public static void WriteFooter(this BinaryWriter writer, ReplayFooter footer)
{
writer.Write(ReplayFooter.Terminator);
writer.Write(ReplayFooter.FooterString);
writer.Write(footer.FinalTimeCode);
writer.Write(footer.Data);
writer.Write(ReplayFooter.FooterString.Length + footer.Data.Length + 8);
}
public static void WriteReplay(this BinaryWriter writer, Replay replay)
{
writer.Write(replay.RawHeader);
var lastTimeCode = (uint)0;
foreach(var chunk in replay.Body)
{
lastTimeCode = chunk.TimeCode;
writer.WriteChunk(chunk);
}
if (replay.HasFooter)
{
writer.WriteFooter(replay.Footer);
}
else
{
writer.WriteFooter(new ReplayFooter(lastTimeCode));
}
}
}
internal sealed class Replay
{
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
public static readonly Dictionary<ReplayType, string> TypeStrings = new Dictionary<ReplayType, string>()
{
{ ReplayType.Skirmish, "遭遇战录像" },
{ ReplayType.Lan, "局域网录像" },
{ ReplayType.Online, "官网录像" },
};
public string Path { get; private set; }
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
public DateTime Date { get; private set; }
public bool HasFooter => Footer != null;
public TimeSpan? Length { get; private set; }
public string MapName { get; private set; }
public string MapPath { get; private set; }
public IReadOnlyList<Player> Players => _players;
public int NumberOfPlayingPlayers { get; private set; }
public long Size { get; private set; }
public Mod Mod { get; private set; }
public ReplayType Type { get; private set; }
public string TypeString => TypeStrings[Type];
public bool HasCommentator { get; private set; }
public Player ReplaySaver => Players[_replaySaverIndex];
public byte[] RawHeader { get; private set; }
public IReadOnlyList<ReplayChunk> Body => _body;
public ReplayFooter Footer { get; private set; }
private List<Player> _players;
private byte _replaySaverIndex;
private long _headerSize;
private List<ReplayChunk> _body;
public Replay(string path)
{
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
Parse(path, stream);
}
}
public Replay(string path, Stream stream)
{
Parse(path, stream);
}
private void Parse(string path, Stream stream)
{
Path = path;
using (var reader = new BinaryReader(stream))
{
Size = reader.BaseStream.Length;
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
if(!headerMagic.SequenceEqual(HeaderMagic))
{
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
}
var isSkirmish = reader.ReadByte() == 0x04;
reader.ReadBytes(4 * 4); // version and builds
reader.ReadBytes(2); // commentary flag, and padding zero byte
reader.ReadUTF16String(); // title
reader.ReadUTF16String(); // description
MapName = reader.ReadUTF16String(); // map name
reader.ReadUTF16String(); // map id
NumberOfPlayingPlayers = reader.ReadByte();
for(var i = 0; i <= NumberOfPlayingPlayers; ++i)
{
reader.ReadUInt32();
reader.ReadUTF16String(); // utf16 player name
if(!isSkirmish)
{
reader.ReadByte(); // team
}
}
var offset = reader.ReadInt32();
var cnc3MagicLength = reader.ReadInt32();
_headerSize = reader.BaseStream.Position + offset;
reader.ReadBytes(cnc3MagicLength);
var modInfo = reader.ReadBytes(22);
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
var timeStamp = reader.ReadUInt32();
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
reader.ReadBytes(31);
var descriptionsLength = reader.ReadInt32();
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
var entries = null as Dictionary<string, string>;
try
{
entries = description.Split(';').Where(x => !string.IsNullOrWhiteSpace(x)).ToDictionary(x => x.Split('=')[0], x => x.Split('=')[1]);
}
catch(Exception exception)
{
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
}
try
{
_players = entries["S"].Split(':')
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
.Select(x => new Player(x.Split(',')))
.ToList();
}
catch (Exception exception)
{
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
}
MapPath = entries["M"].Substring(3);
HasCommentator = !entries["PC"].Equals("-1");
var lanFlag = int.Parse(entries["GT"]) == 0;
if (lanFlag)
{
if(_players.First().PlayerIP == 0)
{
Type = ReplayType.Skirmish;
}
else
{
Type = ReplayType.Lan;
}
}
else
{
Type = ReplayType.Online;
}
_replaySaverIndex = reader.ReadByte();
reader.ReadBytes(8); // 8 bit paddings
var fileNameLength = reader.ReadInt32();
reader.ReadBytes(fileNameLength * 2);
reader.ReadBytes(16);
var verMagicLength = reader.ReadInt32();
reader.ReadBytes(verMagicLength);
reader.ReadBytes(85);
if(reader.BaseStream.Position != _headerSize)
{
throw new InvalidDataException();
}
reader.BaseStream.Seek(-4, SeekOrigin.End);
try
{
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
}
catch(Exception)
{
Length = null;
Footer = null;
}
}
}
public void ParseBody()
{
_body = new List<ReplayChunk>();
using (var stream = new FileStream(Path, FileMode.Open))
using (var reader = new BinaryReader(stream))
{
RawHeader = reader.ReadBytes((int)_headerSize);
while (true)
{
var timeCode = reader.ReadUInt32();
if (timeCode == ReplayFooter.Terminator)
{
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
break;
}
_body.Add(new ReplayChunk(timeCode, reader));
}
}
}
public Dictionary<byte, int[]> GetCommandCounts()
{
var playerCommands = new Dictionary<byte, int[]>();
foreach(var chunk in _body)
{
if(chunk.Type != 1)
{
continue;
}
foreach(var command in CommandChunk.Parse(chunk))
{
var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count];
if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
{
commandCount = commandCount
.Concat(new int[command.PlayerIndex - commandCount.Length + 1])
.ToArray();
}
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
playerCommands[command.CommandID] = commandCount;
}
}
return playerCommands;
}
}
}

148
ReplayAutoSaver.cs Normal file
View File

@ -0,0 +1,148 @@
using AnotherReplayReader.ReplayFile;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
namespace AnotherReplayReader
{
public static class ReplayAutoSaver
{
private static int _errorMessageCount = 0;
public static void SpawnAutoSaveReplaysTask(string replayFolderPath)
{
Task.Run(() => AutoSaveReplays(replayFolderPath));
}
private static async Task AutoSaveReplays(string replayFolderPath)
{
const string ourPrefix = "自动保存";
// filename and last write time
var previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
// filename and file size
var lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
while (true)
{
try
{
var changed = (from fileName in Directory.GetFiles(replayFolderPath, "*.RA3Replay")
let info = new FileInfo(fileName)
where !info.Name.StartsWith(ourPrefix)
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
select info).ToList();
foreach (var info in changed)
{
previousFiles[info.FullName] = info.LastWriteTimeUtc;
}
var replays = changed.Select(info =>
{
Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
try
{
using var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
return new Replay(info.FullName, stream);
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
return null;
}
}).Where(replay => replay != null);
var newLastReplays = from replay in replays
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero)
let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds)
where threshold < 40 || endThreshold < 40
select replay;
var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase);
foreach (var savedLastReplay in lastReplays.Keys)
{
if (!toBeChecked.ContainsKey(savedLastReplay))
{
try
{
using var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream));
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
}
}
}
foreach (var kv in toBeChecked)
{
Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
var replay = kv.Value;
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
{
if (fileSize == replay.Size)
{
// skip if size is not changed
Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
continue;
}
}
Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
lastReplays[kv.Key] = replay.Size;
var date = replay.Date;
var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
if (replay.NumberOfPlayingPlayers <= 2)
{
var playingPlayers = from player in replay.Players
let faction = ModData.GetFaction(replay.Mod, player.FactionId)
where faction.Kind != FactionKind.Observer
select $"{player.PlayerName}({faction.Name})";
playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
}
var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
var destinationPath = Path.Combine(replayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
try
{
File.Copy(replay.Path, destinationPath, true);
}
catch (Exception e)
{
throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
}
}
}
catch (Exception e)
{
var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
Debug.Instance.DebugMessage += errorString;
if (Interlocked.Increment(ref _errorMessageCount) == 1)
{
_ = Application.Current.Dispatcher.InvokeAsync(() =>
{
try
{
MessageBox.Show(errorString);
}
finally
{
Interlocked.Decrement(ref _errorMessageCount);
}
});
}
}
await Task.Delay(10 * 1000).ConfigureAwait(false);
}
}
}
}

View File

@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.IO;
namespace AnotherReplayReader.ReplayFile
{
internal sealed class CommandChunk
{
public byte CommandId { get; private set; }
public int PlayerIndex { get; private set; }
public static List<CommandChunk> Parse(ReplayChunk chunk)
{
if (chunk.Type != 1)
{
throw new NotImplementedException();
}
static int ManglePlayerId(byte id)
{
return id / 8 - 2;
}
using var reader = chunk.GetReader();
if (reader.ReadByte() != 1)
{
throw new InvalidDataException("Payload first byte not 1");
}
var list = new List<CommandChunk>();
var numberOfCommands = reader.ReadInt32();
for (var i = 0; i < numberOfCommands; ++i)
{
var commandId = reader.ReadByte();
var playerId = reader.ReadByte();
reader.ReadCommandData(commandId);
list.Add(new CommandChunk
{
CommandId = commandId,
PlayerIndex = ManglePlayerId(playerId)
});
}
if (reader.BaseStream.Position != reader.BaseStream.Length)
{
throw new InvalidDataException("Payload not fully parsed");
}
return list;
}
public override string ToString()
{
return $"[玩家 {PlayerIndex}{RA3Commands.GetCommandName(CommandId)}]";
}
}
}

47
ReplayFile/Player.cs Normal file
View File

@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace AnotherReplayReader.ReplayFile
{
internal sealed class Player
{
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
{
{ "E", "简单" },
{ "M", "中等" },
{ "H", "困难" },
{ "B", "凶残" },
};
public bool IsComputer { get; }
public string PlayerName { get; }
public uint PlayerIp { get; }
public int FactionId { get; }
public int Team { get; }
public Player(string[] playerEntry)
{
IsComputer = playerEntry[0][0] == 'C';
PlayerName = playerEntry[0].Substring(1);
if (IsComputer)
{
PlayerName = ComputerNames[PlayerName];
PlayerIp = 0;
FactionId = int.Parse(playerEntry[2]);
Team = int.Parse(playerEntry[4]);
}
else
{
PlayerIp = uint.Parse(playerEntry[1], System.Globalization.NumberStyles.HexNumber);
FactionId = int.Parse(playerEntry[5]);
Team = int.Parse(playerEntry[7]);
}
if (PlayerName.Length == 20)
{
PlayerName = Ra3.BattleNet.Database.Utils.ChineseEncoding.DecodeChineseFromBase64(PlayerName);
}
}
}
}

289
ReplayFile/RA3Commands.cs Normal file
View File

@ -0,0 +1,289 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
namespace AnotherReplayReader.ReplayFile
{
internal static class RA3Commands
{
private static readonly Dictionary<byte, Action<BinaryReader>> _commandParser;
private static readonly Dictionary<byte, string> _commandNames;
public static ImmutableArray<byte> UnknownCommands { get; } = new byte[]
{
0x0F, // unk
0x5F, // unk
0x12, // unk
0x1B, // unk
0x48, // unk
0x52, // unk
0xFC, // unk
0xFD, // unk
}.ToImmutableArray();
public static ImmutableArray<byte> AutoCommands { get; } = new byte[]
{
0x01, // auto gen
0x21, // 3 seconds heartbeat
0x33, // uuid
0x34, // uuid
0x35, // player info
0x37, // indeterminate autogen
0x47, // 5th frame auto gen
0xF6, // auto, maybe from barrack deploy
0xF9, // auto, maybe unit from structures
}.ToImmutableArray();
static RA3Commands()
{
Action<BinaryReader> FixedSizeParser(byte command, int size)
{
return (BinaryReader current) =>
{
var lastByte = current.ReadBytes(size - 2).Last();
if (lastByte != 0xFF)
{
Debug.Instance.DebugMessage += $"Failed to parse command {command:X}, last byte is {lastByte:X}";
// throw new InvalidDataException($"Failed to parse command {command:X}, last byte is {lastByte:X}");
}
};
}
Action<BinaryReader> VariableSizeParser(byte command, int offset)
{
return (BinaryReader current) =>
{
var totalBytes = 2;
totalBytes += current.ReadBytes(offset - 2).Length;
for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
{
totalBytes += 1;
var size = ((x >> 4) + 1) * 4;
totalBytes += current.ReadBytes(size).Length;
}
totalBytes += 1;
var chk = totalBytes;
};
};
var list = new List<(byte, Func<byte, int, Action<BinaryReader>>, int, string)>
{
(0x00, FixedSizeParser, 45, "展开建筑/建造碉堡(?)"),
(0x03, FixedSizeParser, 17, "开始升级"),
(0x04, FixedSizeParser, 17, "暂停/中止升级"),
(0x05, FixedSizeParser, 20, "开始生产单位或纳米核心"),
(0x06, FixedSizeParser, 20, "暂停/取消生产单位或纳米核心"),
(0x07, FixedSizeParser, 17, "开始建造建筑"),
(0x08, FixedSizeParser, 17, "暂停/取消建造建筑"),
(0x09, FixedSizeParser, 35, "摆放建筑"),
(0x0F, FixedSizeParser, 16, "(未知指令 0x0F"),
(0x14, FixedSizeParser, 16, "移动"),
(0x15, FixedSizeParser, 16, "移动攻击A"),
(0x16, FixedSizeParser, 16, "强制移动/碾压G"),
(0x21, FixedSizeParser, 20, "[游戏每3秒自动产生的指令]"),
(0x2C, FixedSizeParser, 29, "队形移动(左右键)"),
(0x32, FixedSizeParser, 53, "释放技能或协议(多个目标,如侦察扫描协议)"),
(0x34, FixedSizeParser, 45, "[游戏自动产生的UUID]"),
(0x35, FixedSizeParser, 1049, "[玩家信息(?)]"),
(0x36, FixedSizeParser, 16, "倒车移动D"),
(0x5F, FixedSizeParser, 11, "(未知指令 0x5F"),
(0x0A, VariableSizeParser, 2, "出售建筑"),
(0x0D, VariableSizeParser, 2, "右键攻击"),
(0x0E, VariableSizeParser, 2, "强制攻击Ctrl"),
(0x12, VariableSizeParser, 2, "(未知指令 0x12"),
(0x1A, VariableSizeParser, 2, "停止S"),
(0x1B, VariableSizeParser, 2, "(未知指令 0x1B"),
(0x28, VariableSizeParser, 2, "开始维修建筑"),
(0x29, VariableSizeParser, 2, "停止维修建筑"),
(0x2A, VariableSizeParser, 2, "选择所有单位Q"),
(0x2E, VariableSizeParser, 2, "切换警戒/侵略/固守/停火模式"),
(0x2F, VariableSizeParser, 2, "路径点模式Alt"),
(0x37, VariableSizeParser, 2, "[游戏不定期自动产生的指令]"),
(0x47, VariableSizeParser, 2, "[游戏在第五帧自动产生的指令]"),
(0x48, VariableSizeParser, 2, "(未知指令 0x48"),
(0x4C, VariableSizeParser, 2, "删除信标(或 F9"),
(0x4E, VariableSizeParser, 2, "选择协议"),
(0x52, VariableSizeParser, 2, "(未知指令 0x52"),
(0xF5, VariableSizeParser, 5, "选择单位"),
(0xF6, VariableSizeParser, 5, "[未知指令,貌似会在展开兵营核心时自动产生?]"),
(0xF8, VariableSizeParser, 4, "鼠标左键单击/取消选择"),
(0xF9, VariableSizeParser, 2, "[可能是步兵自行从进驻的建筑撤出]"),
(0xFA, VariableSizeParser, 7, "创建编队"),
(0xFB, VariableSizeParser, 2, "选择编队"),
(0xFC, VariableSizeParser, 2, "(未知指令 0xFC"),
(0xFD, VariableSizeParser, 7, "(未知指令 0xFD"),
(0xFE, VariableSizeParser, 15, "释放技能或协议(无目标)"),
(0xFF, VariableSizeParser, 34, "释放技能或协议(单个目标)"),
};
var specialList = new List<(byte, Action<BinaryReader>, string)>
{
(0x01, ParseSpecialChunk0x01, "[游戏自动生成的指令]"),
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
(0x10, ParseGarrison0x10, "进驻建筑"),
(0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
(0x4B, ParsePlaceBeacon0x4B, "信标")
};
_commandParser = new Dictionary<byte, Action<BinaryReader>>();
_commandNames = new Dictionary<byte, string>();
foreach (var (id, maker, size, description) in list)
{
_commandParser.Add(id, maker(id, size));
_commandNames.Add(id, description);
}
foreach (var (id, parser, description) in specialList)
{
_commandParser.Add(id, parser);
_commandNames.Add(id, description);
}
_commandNames.Add(0x4D, "在信标里输入文字");
}
public static void ReadCommandData(this BinaryReader reader, byte commandId)
{
if (_commandParser.TryGetValue(commandId, out var parser))
{
_commandParser[commandId](reader);
}
else
{
UnknownCommandParser(reader, commandId);
}
}
public static bool IsUnknownCommand(byte commandId)
{
return !_commandNames.ContainsKey(commandId);
}
public static string GetCommandName(byte commandId)
{
return _commandNames.TryGetValue(commandId, out var storedName)
? storedName
: $"(未知指令 0x{commandId:X2}";
}
public static void UnknownCommandParser(BinaryReader current, byte commandId)
{
while (true)
{
var value = current.ReadByte();
if (value == 0xFF)
{
break;
}
}
//return $"(未知指令 0x{commandID:2X}";
}
private static void ParseSpecialChunk0x01(BinaryReader current)
{
var firstByte = current.ReadByte();
if (firstByte == 0xFF)
{
return;
}
var sixthByte = current.ReadBytes(5).Last();
if (sixthByte == 0xFF)
{
return;
}
var sixteenthByte = current.ReadBytes(10).Last();
var size = (int)(sixteenthByte + 1) * 4 + 14;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
return;
}
private static void ParseSetRallyPoint0x02(BinaryReader current)
{
var size = (current.ReadBytes(23).Last() + 1) * 2 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseUngarrison0x0C(BinaryReader current)
{
current.ReadByte();
var size = (current.ReadByte() + 1) * 4 + 1;
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseGarrison0x10(BinaryReader current)
{
var type = current.ReadByte();
var size = -1;
if (type == 0x14)
{
size = 9;
}
else if (type == 0x04)
{
size = 10;
}
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParseUuid0x33(BinaryReader current)
{
current.ReadByte();
var firstStringLength = (int)current.ReadByte();
current.ReadBytes(firstStringLength + 1);
var secondStringLength = current.ReadByte() * 2;
var lastByte = current.ReadBytes(secondStringLength + 6).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
private static void ParsePlaceBeacon0x4B(BinaryReader current)
{
var type = current.ReadByte();
var size = -1;
if (type == 0x04)
{
size = 5;
}
else if (type == 0x07)
{
size = 13;
}
var lastByte = current.ReadBytes(size).Last();
if (lastByte != 0xFF)
{
throw new InvalidDataException();
}
}
}
}

315
ReplayFile/Replay.cs Normal file
View File

@ -0,0 +1,315 @@
using AnotherReplayReader.Utils;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
namespace AnotherReplayReader.ReplayFile
{
internal enum ReplayType
{
Skirmish,
Lan,
Online
}
internal sealed class Replay
{
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
public static readonly Dictionary<ReplayType, string> TypeStrings = new()
{
{ ReplayType.Skirmish, "遭遇战录像" },
{ ReplayType.Lan, "局域网录像" },
{ ReplayType.Online, "官网录像" },
};
public const double FrameRate = 15.0;
public const string PostCommentator = "post Commentator";
private readonly byte _replaySaverIndex;
private readonly byte[]? _rawHeader;
public string Path { get; }
public DateTime Date { get; }
public string MapName { get; }
public string MapPath { get; }
public ImmutableArray<Player> Players { get; }
public int NumberOfPlayingPlayers { get; }
public Mod Mod { get; }
public ReplayType Type { get; }
public bool HasCommentator { get; }
public long Size { get; }
public ReplayFooter? Footer { get; }
public ImmutableArray<ReplayChunk>? Body { get; }
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
public Player ReplaySaver => Players[_replaySaverIndex];
public string TypeString => TypeStrings[Type];
public bool HasFooter => Footer != null;
public ShortTimeSpan? Length => Footer?.ReplayLength;
public Replay(string path, bool parseBody = false) :
this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody)
{
}
public Replay(string path, Stream stream, bool parseBody = false)
{
Path = path;
using var reader = new BinaryReader(stream);
Size = reader.BaseStream.Length;
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
if (!headerMagic.SequenceEqual(HeaderMagic))
{
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
}
var isSkirmish = reader.ReadByte() == 0x04;
reader.ReadBytes(4 * 4); // version and builds
reader.ReadBytes(2); // commentary flag, and padding zero byte
reader.ReadUTF16String(); // title
reader.ReadUTF16String(); // description
MapName = reader.ReadUTF16String(); // map name
reader.ReadUTF16String(); // map id
NumberOfPlayingPlayers = reader.ReadByte();
for (var i = 0; i <= NumberOfPlayingPlayers; ++i)
{
reader.ReadUInt32();
reader.ReadUTF16String(); // utf16 player name
if (!isSkirmish)
{
reader.ReadByte(); // team
}
}
var offset = reader.ReadInt32();
var cnc3MagicLength = reader.ReadInt32();
var headerSize = checked((int)(reader.BaseStream.Position + offset));
reader.ReadBytes(cnc3MagicLength);
var modInfo = reader.ReadBytes(22);
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
var timeStamp = reader.ReadUInt32();
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
reader.ReadBytes(31);
var descriptionsLength = reader.ReadInt32();
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
var entries = null as Dictionary<string, string>;
try
{
var query = from splitted in description.Split(';')
where !string.IsNullOrWhiteSpace(splitted)
select splitted.Split(new[] { '=' }, 2);
entries = query.ToDictionary(x => x[0], x => x[1]);
}
catch (Exception e)
{
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
}
try
{
Players = entries["S"].Split(':')
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
.Select(x => new Player(x.Split(',')))
.ToImmutableArray();
}
catch (Exception exception)
{
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
}
MapPath = entries["M"].Substring(3);
HasCommentator = !entries["PC"].Equals("-1");
var lanFlag = int.Parse(entries["GT"]) == 0;
if (lanFlag)
{
if (Players.First().PlayerIp == 0)
{
Type = ReplayType.Skirmish;
}
else
{
Type = ReplayType.Lan;
}
}
else
{
Type = ReplayType.Online;
}
_replaySaverIndex = reader.ReadByte();
reader.ReadBytes(8); // 8 bit paddings
var fileNameLength = reader.ReadInt32();
reader.ReadBytes(fileNameLength * 2);
reader.ReadBytes(16);
var verMagicLength = reader.ReadInt32();
reader.ReadBytes(verMagicLength);
reader.ReadBytes(85);
if (reader.BaseStream.Position != headerSize)
{
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
return;
}
if (!parseBody)
{
// jump to footer directly
reader.BaseStream.Seek(-4, SeekOrigin.End);
try
{
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
Footer = null;
}
return;
}
var body = new List<ReplayChunk>();
try
{
while (true)
{
var timeCode = reader.ReadUInt32();
if (timeCode == ReplayFooter.Terminator)
{
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
break;
}
body.Add(new ReplayChunk(timeCode, reader));
}
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
}
byte[]? rawHeader = null;
try
{
// 重新读取原来的整个录像头
reader.BaseStream.Seek(0, SeekOrigin.Begin);
rawHeader = reader.ReadBytes(headerSize);
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
return;
}
if (rawHeader.Length != headerSize)
{
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n";
return;
}
_rawHeader = rawHeader;
Body = body.ToImmutableArray();
}
public Replay CloneHeader()
{
using var stream = new MemoryStream();
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
{
WriteTo(writer);
}
stream.Position = 0;
return new Replay(Path, stream);
}
public bool PathEquals(Replay replay) => PathEquals(replay.Path);
public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);
public string GetDetails()
{
static string GetSizeString(double size)
{
if (size > 1024 * 1024)
{
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
}
return $"{Math.Round(size / 1024)}KB";
}
string size = GetSizeString(Size);
string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
var replaySaver = _replaySaverIndex < Players.Length
? ReplaySaver.PlayerName
: "[无法获取保存录像的玩家]";
using var writer = new StringWriter();
writer.WriteLine("文件名:{0}", FileName);
writer.WriteLine("大小:{0}", size);
writer.WriteLine("地图:{0}", MapName);
writer.WriteLine("日期:{0}", Date);
writer.WriteLine("长度:{0}", length);
writer.WriteLine("录像类别:{0}", TypeString);
writer.WriteLine("这个文件是{0}保存的", replaySaver);
writer.WriteLine("玩家列表:");
foreach (var player in Players)
{
if (player == Players.Last() && player.PlayerName.Equals(PostCommentator))
{
break;
}
var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
writer.WriteLine($"{player.PlayerName}{factionName}");
}
return writer.ToString();
}
public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);
private void WriteTo(BinaryWriter writer, bool skipBody)
{
if ((_rawHeader is null || Body is null) && !skipBody)
{
throw new InvalidOperationException("Replay body must be parsed before writing replay");
}
writer.Write(_rawHeader);
var lastTimeCode = Footer?.FinalTimeCode;
if (Body is not null)
{
foreach (var chunk in Body)
{
lastTimeCode = chunk.TimeCode;
writer.Write(chunk);
}
}
if (Footer is not null)
{
writer.Write(Footer);
}
else if (lastTimeCode is uint lastTimeCodeValue)
{
writer.Write(new ReplayFooter(lastTimeCodeValue));
}
}
}
}

40
ReplayFile/ReplayChunk.cs Normal file
View File

@ -0,0 +1,40 @@
using System;
using System.IO;
namespace AnotherReplayReader.ReplayFile
{
internal sealed class ReplayChunk
{
private readonly byte[] _data;
public uint TimeCode { get; }
public byte Type { get; }
public TimeSpan Time => TimeSpan.FromSeconds(TimeCode / Replay.FrameRate);
public ReplayChunk(uint timeCode, BinaryReader reader)
{
TimeCode = timeCode; // reader.ReadUInt32();
Type = reader.ReadByte();
var chunkSize = reader.ReadInt32();
_data = reader.ReadBytes(chunkSize);
if (reader.ReadInt32() != 0)
{
throw new InvalidDataException("Replay Chunk not ended with zero");
}
}
public BinaryReader GetReader() => new(new MemoryStream(_data, false));
public void WriteTo(BinaryWriter writer)
{
writer.Write(TimeCode);
writer.Write(Type);
writer.Write(_data.Length);
writer.Write(_data);
writer.Write(0);
}
}
}

View File

@ -0,0 +1,40 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace AnotherReplayReader.ReplayFile
{
internal static class ReplayExtensions
{
public static string ReadUTF16String(this BinaryReader reader)
{
var currentBytes = new List<byte>();
var lastTwoBytes = Array.Empty<byte>();
while (true)
{
lastTwoBytes = reader.ReadBytes(2);
if (lastTwoBytes.Length != 2)
{
throw new InvalidDataException();
}
if (lastTwoBytes.All(x => x == 0))
{
break;
}
currentBytes.AddRange(lastTwoBytes);
}
return Encoding.Unicode.GetString(currentBytes.ToArray());
}
public static void Write(this BinaryWriter writer, ReplayChunk chunk) => chunk.WriteTo(writer);
public static void Write(this BinaryWriter writer, ReplayFooter footer) => footer.WriteTo(writer);
public static void Write(this BinaryWriter writer, Replay replay) => replay.WriteTo(writer);
}
}

View File

@ -0,0 +1,92 @@
using System;
using System.IO;
using System.Linq;
using System.Text;
namespace AnotherReplayReader.ReplayFile
{
internal enum ReplayFooterOption
{
SeekToFooter,
CurrentlyAtFooter,
}
internal sealed class ReplayFooter
{
public const uint Terminator = 0x7FFFFFFF;
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
private readonly byte[] _data;
public uint FinalTimeCode { get; }
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / Replay.FrameRate);
public ReplayFooter(BinaryReader reader, ReplayFooterOption option)
{
var currentPosition = reader.BaseStream.Position;
reader.BaseStream.Seek(-4, SeekOrigin.End);
var footerLength = reader.ReadInt32();
if (option == ReplayFooterOption.SeekToFooter)
{
currentPosition = reader.BaseStream.Length - footerLength;
}
reader.BaseStream.Seek(currentPosition, SeekOrigin.Begin);
var footer = reader.ReadBytes(footerLength);
if (footer.Length != footerLength || reader.BaseStream.Position != reader.BaseStream.Length)
{
throw new InvalidDataException("Invalid footer");
}
using var footerStream = new MemoryStream(footer);
using var footerReader = new BinaryReader(footerStream);
var footerString = footerReader.ReadBytes(17);
if (!footerString.SequenceEqual(FooterString))
{
throw new InvalidDataException("Invalid footer, no footer string");
}
FinalTimeCode = footerReader.ReadUInt32();
_data = footerReader.ReadBytes(footer.Length - 25);
if (footerReader.ReadInt32() != footerLength)
{
throw new InvalidDataException();
}
}
public ReplayFooter(uint finalTimeCode)
{
FinalTimeCode = finalTimeCode;
_data = new byte[] { 0x02, 0x1A, 0x00, 0x00, 0x00 };
}
public float[]? TryGetKillDeathRatios()
{
if (_data.Length < 24)
{
return null;
}
var ratios = new float[6];
using var stream = new MemoryStream(_data, _data.Length - 24, 24);
using var reader = new BinaryReader(stream);
for (var i = 0; i < ratios.Length; ++i)
{
ratios[i] = reader.ReadSingle();
}
return ratios;
}
public void WriteTo(BinaryWriter writer)
{
writer.Write(Terminator);
writer.Write(FooterString);
writer.Write(FinalTimeCode);
writer.Write(_data);
writer.Write(FooterString.Length + _data.Length + 8);
}
}
}

Binary file not shown.

90
UpdateChecker.cs Normal file
View 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);
}
}
}

48
Utils/CancelManager.cs Normal file
View File

@ -0,0 +1,48 @@
using System;
using System.Threading;
namespace AnotherReplayReader.Utils
{
internal class CancelManager : IDisposable
{
private CancellationToken _linkedToken;
private CancellationTokenSource? _source;
public CancellationToken Token => Materialize().Token;
public void Reset(CancellationToken linked)
{
_linkedToken = linked;
if (_source is { } source)
{
_source = null;
try
{
source.Cancel();
}
catch (AggregateException e)
{
Debug.Instance.DebugMessage += $"Cancellation failed: {e}";
}
source.Dispose();
}
}
public CancellationToken ResetAndGetToken(CancellationToken linked)
{
Reset(linked);
return Token;
}
public void Dispose() => Reset(default);
private CancellationTokenSource Materialize()
{
if (_source is null)
{
_source = CancellationTokenSource.CreateLinkedTokenSource(_linkedToken);
}
return _source;
}
}
}

View File

@ -0,0 +1,23 @@
using System;
using System.Threading.Tasks;
namespace AnotherReplayReader.Utils
{
internal static class CancellableTaskExtensions
{
public static async Task IgnoreCancel(this Task task)
{
try
{
await task.ConfigureAwait(false);
}
catch (OperationCanceledException) { }
}
public static void Forget(this Task task)
{
const TaskContinuationOptions flags = TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously;
task.ContinueWith(t => t.Exception?.Handle(_ => true), flags);
}
}
}

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Immutable;
namespace AnotherReplayReader.Utils
{
static class ImmutableArrayExtensions
{
public static int? FindIndex<T>(this in ImmutableArray<T> a, Predicate<T> p)
{
for (var i = 0; i < a.Length; ++i)
{
if (p(a[i]))
{
return i;
}
}
return null;
}
}
}

32
Utils/Lock.cs Normal file
View File

@ -0,0 +1,32 @@
using System;
using System.Threading;
namespace AnotherReplayReader.Utils
{
internal class Lock : IDisposable
{
public readonly object LockObject;
private bool _disposed = false;
public Lock(object lockObject)
{
LockObject = lockObject;
Monitor.Enter(LockObject);
}
public void Dispose()
{
if (!_disposed)
{
Monitor.Exit(LockObject);
_disposed = true;
}
}
public static T Run<T>(object @lock, Func<T> action)
{
using var locker = new Lock(@lock);
return action();
}
}
}

27
Utils/Network.cs Normal file
View 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);
}
}
}

27
Utils/PinyinExtensions.cs Normal file
View File

@ -0,0 +1,27 @@
using NPinyin;
using System;
namespace AnotherReplayReader.Utils
{
static class PinyinExtensions
{
public static bool ContainsIgnoreCase(this string self, string? s)
{
return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1;
}
public static string? ToPinyin(this string self)
{
string pinyin;
try
{
pinyin = Pinyin.GetPinyin(self);
}
catch
{
return null;
}
return pinyin.Replace(" ", "");
}
}
}

43
Utils/RegistryUtils.cs Normal file
View File

@ -0,0 +1,43 @@
using Microsoft.Win32;
using System;
namespace AnotherReplayReader.Utils
{
public static class RegistryUtils
{
public static string? Retrieve32(RegistryHive hive, string path, string value)
{
try
{
using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
using var key = view32.OpenSubKey(path, false);
return key?.GetValue(value) as string;
}
catch (Exception e)
{
Debug.Instance.DebugMessage += $"Failed to retrieve registy {hive}:{path}:{value}: {e}";
return null;
}
}
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 Retrieve32(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value);
}
}
}

56
Utils/ReplayPinyinList.cs Normal file
View File

@ -0,0 +1,56 @@
using AnotherReplayReader.ReplayFile;
using System.Collections.Immutable;
using System.Linq;
namespace AnotherReplayReader.Utils
{
internal class ReplayPinyinList
{
public ImmutableArray<Replay> Replays { get; } = ImmutableArray<Replay>.Empty;
public ImmutableArray<ReplayPinyinData> Pinyins { get; } = ImmutableArray<ReplayPinyinData>.Empty;
public ReplayPinyinList() :
this(ImmutableArray<Replay>.Empty)
{
}
public ReplayPinyinList(ImmutableArray<Replay> replay) :
this(replay,
replay.Select(replay => new ReplayPinyinData(replay)).ToImmutableArray())
{
}
private ReplayPinyinList(ImmutableArray<Replay> replay,
ImmutableArray<ReplayPinyinData> pinyins)
{
Replays = replay;
Pinyins = pinyins;
}
public ReplayPinyinList SetItem(int index, Replay replay)
{
return new(Replays.SetItem(index, replay),
Pinyins.SetItem(index, new(replay)));
}
}
class ReplayPinyinData
{
public Replay Replay { get; }
public string? PinyinDetails { get; }
public string? PinyinMod { get; }
public ReplayPinyinData(Replay replay)
{
Replay = replay;
PinyinDetails = replay.GetDetails().ToPinyin();
PinyinMod = replay.Mod.ModName.ToPinyin();
}
public bool MatchPinyin(string? pinyin)
{
return PinyinDetails?.ContainsIgnoreCase(pinyin) is true
|| PinyinMod?.ContainsIgnoreCase(pinyin) is true;
}
}
}

28
Utils/ShortTimeSpan.cs Normal file
View File

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader.Utils
{
public readonly struct ShortTimeSpan : IEquatable<ShortTimeSpan>, IComparable<ShortTimeSpan>, IComparable
{
public readonly TimeSpan Value;
public ShortTimeSpan(TimeSpan value) => Value = value;
public static implicit operator TimeSpan(ShortTimeSpan span) => span.Value;
public static implicit operator ShortTimeSpan(TimeSpan value) => new(value);
public override string ToString() => $"{(int)Value.TotalMinutes:00}:{Value.Seconds:00}";
public int CompareTo(ShortTimeSpan other) => Value.CompareTo(other.Value);
public int CompareTo(object obj) => obj is ShortTimeSpan span ? CompareTo(span) : 1;
public override bool Equals(object? obj) => obj is ShortTimeSpan span && Equals(span);
public bool Equals(ShortTimeSpan other) => Value.Equals(other.Value);
public override int GetHashCode() => Value.GetHashCode();
public static bool operator ==(ShortTimeSpan left, ShortTimeSpan right) => left.Equals(right);
public static bool operator !=(ShortTimeSpan left, ShortTimeSpan right) => !(left == right);
public static bool operator <(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) < 0;
public static bool operator >(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) > 0;
}
}

31
Utils/TaskQueue.cs Normal file
View File

@ -0,0 +1,31 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace AnotherReplayReader.Utils
{
internal class TaskQueue
{
private readonly object _lock = new();
private readonly Dispatcher _dispatcher;
private Task _current = Task.CompletedTask;
public TaskQueue(Dispatcher dispatcher)
{
_dispatcher = dispatcher;
}
public Task Enqueue(Func<Task> getTask, CancellationToken cancelToken)
{
using var locker = new Lock(_lock);
_current = _current.ContinueWith(async t =>
{
var task = await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
await task.ConfigureAwait(false);
}, cancelToken).Unwrap();
return _current.IgnoreCancel();
}
}
}

115
Utils/Verifier.cs Normal file
View 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
View 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;
}
}
}
}
}

View File

@ -8,15 +8,19 @@
mc:Ignorable="d"
Title="Window1" Height="450" Width="800">
<Grid>
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIPFieldChanged" />
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIpFieldChanged" />
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="29,28,0,0" TextWrapping="Wrap" Text="IP" VerticalAlignment="Top"/>
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" />
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" TextChanged="OnIpFieldChanged" />
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="205,28,0,0" TextWrapping="Wrap" Text="玩家名称 / 说明" VerticalAlignment="Top"/>
<Button x:Name="_setIPButton" Content="上传" Margin="705,26,12,0" VerticalAlignment="Top" Click="OnClick"/>
<DataGrid x:Name="_dataGrid" Margin="20,60,12,19">
<DataGrid x:Name="_dataGrid"
Margin="20,60,12,19"
MouseDoubleClick="OnDataGridMouseDoubleClick"
IsReadOnly="True"
AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="IP" Binding="{Binding Path=IPString}"/>
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=ID}"/>
<DataGridTextColumn Header="IP" Binding="{Binding Path=IpString}"/>
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=Id}"/>
</DataGrid.Columns>
</DataGrid>
</Grid>

View File

@ -1,12 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using AnotherReplayReader.Utils;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Script.Serialization;
using System.Windows;
namespace AnotherReplayReader
@ -16,113 +13,97 @@ namespace AnotherReplayReader
/// </summary>
internal partial class Window1 : Window
{
private PlayerIdentity _identity;
public Window1(PlayerIdentity identity)
public Window1()
{
InitializeComponent();
_identity = identity;
Refresh();
Refresh(true);
}
private async void Refresh()
private async void Refresh(bool showCached)
{
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
_setIPButton.IsEnabled = false;
try
{
Dispatcher.Invoke(() =>
var loading = new IpAndPlayer[] { new() { Ip = 0, Id = "正在加载..." } };
_dataGrid.ItemsSource = loading;
if (showCached)
{
_dataGrid.Items.Clear();
_dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
});
await _identity.Fetch();
Display();
await Display();
_dataGrid.ItemsSource = loading.Concat(_dataGrid.ItemsSource.Cast<IpAndPlayer>());
}
await Display();
}
catch(Exception e)
catch (Exception e)
{
Dispatcher.Invoke(() => MessageBox.Show(this, $"无法加载IP表{e}"));
MessageBox.Show(this, $"无法加载IP表{e}");
}
finally
{
_setIPButton.IsEnabled = true;
}
Dispatcher.Invoke(() => _setIPButton.IsEnabled = true);
}
private void Display(string filter = "")
private async Task Display(string filter = "", string nameFilter = "")
{
var newList = _identity.AsSortedList().Where(x => x.IPString.StartsWith(filter));
Dispatcher.Invoke(() =>
{
_dataGrid.Items.Clear();
foreach (var item in newList)
{
_dataGrid.Items.Add(item);
}
});
}
private async void OnClick(object sender, RoutedEventArgs e)
{
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
await Task.Run(() =>
_setIPButton.IsEnabled = false;
try
{
var ipText = Dispatcher.Invoke(() => _ipField.Text);
var ipText = _ipField.Text;
if (!IPAddress.TryParse(ipText, out var ip))
{
Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
MessageBox.Show(this, "IP 格式不正确");
return;
}
var idText = Dispatcher.Invoke(() => _idField.Text);
var idText = _idField.Text;
if (string.IsNullOrWhiteSpace(idText))
{
var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
if(result != MessageBoxResult.OK)
var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
if (choice != MessageBoxResult.OK)
{
return;
}
}
try
var result = await UpdateIpTable(ip, idText);
if (!result)
{
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 stream = request.GetResponse().GetResponseStream())
using (var reader = new StreamReader(stream))
{
var response = reader.ReadToEnd();
var serializer = new JavaScriptSerializer();
var result = serializer.Deserialize<bool>(response);
if(!result)
{
Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
}
}
}
catch (Exception exception)
{
Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
MessageBox.Show(this, "设置 IP 表失败");
}
});
Refresh();
Refresh(false);
}
catch (Exception exception)
{
MessageBox.Show(this, $"设置 IP 表时发生错误。\r\n{exception}");
}
finally
{
_setIPButton.IsEnabled = true;
}
}
private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
private async Task<bool> UpdateIpTable(IPAddress ip, string idText)
{
await Task.Run(() =>
{
var fieldText = Dispatcher.Invoke(() => _ipField.Text);
Display(fieldText);
});
var bytes = ip.GetAddressBytes();
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
return false;
}
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
{
var ipText = _ipField.Text;
var idText = _idField.Text;
await Display(ipText, idText);
}
private void OnDataGridMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
}
}
}

76
app.manifest Normal file
View 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>

View File

@ -1,95 +0,0 @@
private async Task AutoSaveReplays()
{
const string ourPrefix = "自动保存";
// filename and last write time
Dictionary<string, DateTime> previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
// filename and file size
Dictionary<string, long> lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
while(true)
{
try
{
var changed = from fileName in Directory.GetFiles(_properties.ReplayFolderPath, "*.RA3Replay")
let info = new FileInfo(fileName)
where !info.Name.StartsWith(ourPrefix)
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
select info;
foreach (var info in changed)
{
previousFiles[info.FullName] = info.LastWriteTimeUtc;
}
var replays = changed.Select(info =>
{
try
{
return new Replay(info.FullName);
}
catch (Exception)
{
return null;
}
}).Where(replay => replay != null);
var newLastReplays = from replay in replays
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
where threshold < 20
select replay;
var toBeChecked = newLastReplays.ToDictionary(replay => replay.FileName, StringComparer.OrdinalIgnoreCase);
foreach (var savedLastReplay in lastReplays)
{
if (!toBeChecked.ContainsKey(savedLastReplay.Key))
{
try
{
toBeChecked.Add(savedLastReplay.Key, new Replay(savedLastReplay.Key));
}
catch(Exception)
{
}
}
}
foreach (var kv in toBeChecked)
{
var replay = kv.Value;
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
{
if (fileSize == replay.Size)
{
// skip if size is not changed
continue;
}
}
lastReplays[kv.Key] = replay.Size;
var date = replay.Date;
var numberOfPlayers = replay.NumberOfPlayingPlayers;
var playerString = $"{numberOfPlayers}名玩家";
if (replay.NumberOfPlayingPlayers <= 2)
{
var playingPlayers = from player in replay.Players
let faction = ModData.GetFaction(replay.Mod, player.FactionID)
where faction.Kind != FactionKind.Observer
select $"{player}({faction.Name})";
playerString = playingPlayers.Aggregate((x, y) => x + y);
}
var dateString = $"{date.Year}-{date.Month}-{date.Day}_{date.Hour}:{date.Minute}";
File.Copy(replay.FileName, $"{_properties.ReplayFolderPath}/{ourPrefix}-{playerString}{dateString}.RA3Replay");
}
}
catch(Exception e)
{
_ = Dispatcher.InvokeAsync(() => MessageBox.Show($"自动保存录像时出现错误:\r\n{e}"));
}
await Task.Delay(10 * 1000);
}
}