19 Commits
Author SHA1 Message Date
lanyi a3b4cc530d wip 2026-06-21 14:42:04 +02:00
lanyi 22cfb1f0a9 完善日志 2025-04-08 09:41:56 +02:00
lanyi a942768c8d Merge pull request '干掉了浩方查马甲代码' (#1) from 干掉了浩方查马甲代码 into master
Reviewed-on: #1
2025-01-30 19:54:11 +08:00
lanyi e16eefa166 fix 2025-01-30 12:53:12 +01:00
lanyi 66458e831c 干掉了浩方查马甲代码 2024-08-24 01:54:11 +02:00
lanyi 08700abd4f 分离玩家信息插件,以及更新检测 2021-10-22 08:54:04 +02:00
lanyi 2a96f8efac 击杀阵亡比 2021-10-20 22:40:03 +02:00
lanyi 0863b2fd71 APM! 2021-10-20 22:22:19 +02:00
lanyi 78a7310a3b 修未知指令的十六进制代码 2021-10-19 23:20:09 +02:00
lanyi 23207b9085 更改命名 2021-10-19 23:17:30 +02:00
lanyi 6f5c21aa8f 修离线缓存 2021-10-19 23:13:28 +02:00
lanyi 888ddce4ef 改了一亿个东西,修了一亿个 BUG 2021-10-19 17:42:18 +02:00
lanyi 5b907309e0 修 2021-10-14 14:22:34 +02:00
lanyi c3a41c0d4e 尝试切换到 SDK 风格的项目文件 2021-10-14 13:22:49 +02:00
lanyi b92d1d90ad 支持筛选录像 2021-10-13 20:17:32 +02:00
lanyi 3f5070df77 修 2021-10-13 16:47:59 +02:00
lanyi 6fd0bf0046 修了一堆东西 2021-10-13 16:42:38 +02:00
lanyi 1f5f1c8e6c 增加按照名字搜索 2021-10-09 12:13:44 +02:00
lanyi 79d9bf4cba 修复部分录像解析错误 2021-10-08 14:10:13 +02:00
62 changed files with 5861 additions and 2590 deletions
-63
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>
-230
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();
}
}
}
+70 -21
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>
+48 -14
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();
}
}
}
+39 -169
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>
<Target Name="CustomAfterBuild" AfterTargets="Build">
<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>
<_FilesToMove Include="$(OutputPath)*.dll" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<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>
+3 -3
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
+306
View File
@@ -0,0 +1,306 @@
using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using OxyPlot;
using OxyPlot.Axes;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Windows;
using System.Windows.Threading;
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(int 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<int, byte> UnknownCommands;
private static readonly ConcurrentDictionary<int, byte> AutoCommands;
private readonly ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> _commands;
public Replay Replay { get; }
public ImmutableSortedDictionary<int, Player> PlayersMap { get; }
public ImmutableArray<Player> PlayersArray { get; }
public ImmutableArray<TimeSpan> PlayerLifes { get; }
public ImmutableArray<TimeSpan> StricterPlayerLifes { get; }
public TimeSpan ReplayLength { get; }
public ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> Commands => _commands;
static ApmPlotter()
{
static KeyValuePair<int, byte> Create(int 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;
var queryFull = from chunk in body
where chunk.Type is 1
from command in CommandChunk.Parse(chunk)
select command;
var playersIndices = queryFull
.Select(x => x.PlayerIndex)
.Distinct()
.OrderBy(x => x)
.ToArray();
// if length mismatch and last is post commentator, remove post commentator from replay players
var replayPlayers = replay.Players;
var postCommentatorIndex = replayPlayers.FindIndex(p => p.PlayerName == Replay.PostCommentator);
if (playersIndices.Length + 1 == replayPlayers.Length && postCommentatorIndex is int index)
{
replayPlayers = replayPlayers.RemoveAt(index);
}
if (playersIndices.Length != replayPlayers.Length)
{
throw new InvalidOperationException(
$"Player count mismatch. Indices={playersIndices.Length}, Players={replayPlayers.Length}");
}
PlayersMap = playersIndices
.Zip(replayPlayers, (index, player) => new KeyValuePair<int, Player>(index, player))
.ToImmutableSortedDictionary();
PlayersArray = PlayersMap.Values.ToImmutableArray();
//var queryFull = from chunk in body
// where chunk.Type is 1
// from command in CommandChunk.Parse(chunk)
// select (chunk.Time, command);
//var sb = new System.Text.StringBuilder();
//var stringHashes = System.IO.File.ReadAllText(@"C:\Apps\RA3-MODSDK-X\builtmods\StringHashes.xml");
//XDocument doc = XDocument.Parse(stringHashes);
//XNamespace ns = "uri:ea.com:eala:asset";
//var table = doc
// .Descendants(ns + "StringHashTable")
// .FirstOrDefault(x => (string?)x.Attribute("id") == "StringHashBin_INSTANCEID");
//if (table == null)
// throw new InvalidOperationException("StringHashBin_INSTANCEID not found.");
//Dictionary<uint, string> hashTable = table
// .Elements(ns + "StringAndHash")
// .ToDictionary(
// x => uint.Parse(x.Attribute("Hash")!.Value),
// x => x.Attribute("Text")!.Value);
//foreach (var (time, command) in queryFull)
//{
// if (IsUnknown(command.CommandId) || IsAuto(command.CommandId))
// {
// continue;
// }
// var name = RA3Commands.GetCommandName(command.CommandId);
// sb.Append($"[{time}] 玩家 {command.PlayerIndex},{name}\r\n");
// foreach (var kv in command.Data)
// {
// // try to convert int32 or uint32 to hashes and retrieve text
// var textFromHash = string.Empty;
// uint hashValue = kv.Key switch
// {
// CommandArgumentType.Int32 => (uint)(int)kv.Value,
// CommandArgumentType.UInt32 => (uint)kv.Value,
// CommandArgumentType.UInt32_2 => (uint)kv.Value,
// _ => 0
// };
// if (hashTable.TryGetValue(hashValue, out var text))
// {
// sb.Append($" {text}\r\n");
// }
// else
// {
// sb.Append($" {kv.Key}: {kv.Value}\r\n");
// }
// }
//}
//流水账 = sb.ToString();
//System.IO.File.WriteAllText("流水账.txt", 流水账);
// get all commands
var query = from chunk in body
where chunk.Type is 1
select (chunk.Time, CommandChunk.Parse(chunk).ToImmutableArray());
_commands = query.ToImmutableArray();
var playerLifes = new SortedDictionary<int, TimeSpan>(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero));
var threeSeconds = TimeSpan.FromSeconds(3);
var stricterLifes = new SortedDictionary<int, TimeSpan>(PlayersMap.Keys.ToDictionary(x => x, _ => TimeSpan.Zero));
foreach (var (time, commands) in _commands)
{
var estimatedTime = time + threeSeconds;
foreach (var command in commands)
{
var commandId = command.CommandId;
var playerIndex = command.PlayerIndex;
if (playerIndex != -1)
{
if (commandId == 0x221)
{
if (playerLifes[playerIndex] < estimatedTime)
{
playerLifes[playerIndex] = estimatedTime;
}
}
else if (!IsUnknown(commandId) && !IsAuto(commandId))
{
if (stricterLifes[playerIndex] < estimatedTime)
{
stricterLifes[playerIndex] = estimatedTime;
}
}
}
}
}
PlayerLifes = playerLifes.Values.ToImmutableArray();
StricterPlayerLifes = stricterLifes.Values.ToImmutableArray();
ReplayLength = replay.Footer?.ReplayLength ?? PlayerLifes.Max();
}
public DataPoint[][] GetPoints(TimeSpan resolution, ApmPlotterFilterOptions options)
{
var lists = new SortedDictionary<int, List<int>>(PlayersMap.Keys.ToDictionary(x => x, _ => new List<int>()));
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))
{
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.Value.Select(Create).ToArray())];
}
public double[] CalculateAverageApm(ApmPlotterFilterOptions options)
{
var apm = new SortedDictionary<int, double>(PlayersMap.Keys.ToDictionary(x => x, _ => 0.0));
foreach (var (_, commands) in _commands)
{
foreach (var command in commands)
{
if (options.ShouldSkip(command.CommandId))
{
continue;
}
++apm[command.PlayerIndex];
}
}
return [.. apm.Select((kv, i) => kv.Value / PlayerLifes[i].TotalMinutes)];
}
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;
})];
}
public Dictionary<int, int[]> GetCommandCounts(TimeSpan begin, TimeSpan end)
{
var playerCommands = new Dictionary<int, SortedDictionary<int, 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(PlayersMap.Keys.ToDictionary(x => x, _ => 0));
}
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
}
}
return playerCommands.ToDictionary(kv => kv.Key, kv => kv.Value.Values.ToArray());
}
public static bool IsUnknown(int commandId)
{
if (UnknownCommands.ContainsKey(commandId))
{
return true;
}
if (RA3Commands.IsUnknownCommand(commandId))
{
UnknownCommands.TryAdd(commandId, default);
return true;
}
return false;
}
public static bool IsAuto(int commandId) => AutoCommands.ContainsKey(commandId);
public static bool IsClick(int commandId) => commandId is 0x1F8 or 0x1F5;
}
}
+168
View File
@@ -0,0 +1,168 @@
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<int>();
orderedCommands.AddRange(
[
0x1F5, // 选择
0x1F6,
0x1F8,
0x1F9,
0x22A,
0x1FA, // 编队
0x1FB,
0x1FC,
0x207, // 生产/建造
0x208,
0x205,
0x206,
0x209, // 摆放
0x20A, // 出售
0x203, // 升级
0x204,
0x20D, // 右键攻击
0x20E, // 强A
0x20F, // 强A
0x215, // 移动攻击
0x214, // 移动
0x236, // 倒车
0x216, // 碾压
0x22C, // 队形
0x21A, // 停止
0x21B, // 散开
0x24E, // 选择协议
0x1FE, // 释放特殊能力
0x1FF,
0x200,
0x201,
0x232,
0x22E, // 姿态
0x22F, // 计划模式
0x228, // 维修
0x229,
0x202, // 集结点
0x210, // 进驻建筑
0x20C, // 从建筑中撤出
0x248, // 采矿交矿
0x212,
0x24B, // 信标
0x24C,
0x24D,
0x1, // 退出地图
]);
orderedCommands.AddRange(RA3Commands.AutoCommands);
orderedCommands.AddRange(RA3Commands.UnknownCommands);
var dataList = new List<DataRow>
{
new("ID", plotter.PlayersArray.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.PlayersArray.Select(GetStatus)));
}
apmRowIndex = dataList.Count;
// kill-death ratio
if (plotter.Replay.Footer?.TryGetKillDeathRatios() is { } kdRatios)
{
var texts = kdRatios
.Take(plotter.PlayersArray.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.PlayersArray.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
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();
}
}
}
+142
View File
@@ -0,0 +1,142 @@
<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">
<Grid DockPanel.Dock="Bottom"
Margin="16,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LEFT SIDE CONTROLS -->
<StackPanel Grid.Column="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>
<!-- RIGHT SIDE BUTTON -->
<Button x:Name="_openEventDump"
Grid.Column="1"
Padding="8,2"
Content="打开流水账"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Click="OnOpenEventDumpClicked"/>
</Grid>
<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>
+396
View File
@@ -0,0 +1,396 @@
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.PlayersArray[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 = _plotter.Result.PlayersArray[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);
}
}
private async void OnOpenEventDumpClicked(object sender, RoutedEventArgs e)
{
var plotter = await _plotter;
var dump = new EventDump();
dump.LoadStringHashes();
dump.SetDumpData(_replay.Mod, plotter);
await dump.ShowPlainText();
}
}
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);
}
}
}
+2 -1
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>
+63 -4
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
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());
}
}
}
+76 -187
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()
public BigMinimapCache(string? ra3Directory)
{
Bigs = new List<string>();
MapsToBigs = new Dictionary<string, string>();
}
Task.Run(() => Initialize(ra3Directory));
}
//private Cache _cache;
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = null;
public bool TryGetEntry(string path, out Stream? bigEntry)
{
bigEntry = null;
public BigMinimapCache(Cache cache, string ra3Directory)
{
//_cache = cache;
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;
}
public byte[] TryReadBytesFromBig(string path)
private void Initialize(string? ra3Directory)
{
if(_mapsToBigs == null)
{
return null;
}
if(!_mapsToBigs.ContainsKey(path))
{
return null;
}
try
{
var bigPath = _mapsToBigs[path];
using (var big = new BigArchive(path))
if (ra3Directory is null || !Directory.Exists(ra3Directory))
{
var entry = big.GetEntry(path);
using (var stream = entry.Open())
using (var reader = new BinaryReader(stream))
{
return reader.ReadBytes((int)entry.Length);
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
return;
}
var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
var currentLanguage_ = $"{currentLanguage}_";
double SkudefVersionSelector(string fullPath)
{
var value = -1.0;
try
{
const string skudefPrefix = "RA3";
const int prefix = 1;
const int language = 2;
const int majorVersion = 3;
const int minorVersion = 4;
const int majorVersionMultiplier = 10000;
const int correctLanguageBonus = 1000_0000;
value = 0;
var stem = Path.GetFileNameWithoutExtension(fullPath);
var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
if (!match.Success || match.Groups.Cast<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;
}
}
}
+61 -22
View File
@@ -3,21 +3,24 @@ 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()
{
Initialization = Task.Run(async () =>
{
try
{
@@ -26,36 +29,73 @@ namespace AnotherReplayReader
Directory.CreateDirectory(CacheDirectory);
}
var serializer = new JavaScriptSerializer();
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
}
catch
try
{
_storage = new ConcurrentDictionary<string, string>();
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
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;
}
}
}
-303
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;
}
}
}
}
+16 -4
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>
+83 -32
View File
@@ -1,52 +1,104 @@
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)
public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog());
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(() =>
{
var saveFileDialog = new SaveFileDialog
{
@@ -54,15 +106,14 @@ namespace AnotherReplayReader
OverwritePrompt = true,
};
var result = saveFileDialog.ShowDialog(this);
var result = saveFileDialog.ShowDialog(owner);
if (result == true)
{
using (var file = saveFileDialog.OpenFile())
using (var writer = new StreamWriter(file))
{
writer.Write(Instance.DebugMessage);
using var file = saveFileDialog.OpenFile();
using var writer = new StreamWriter(file);
writer.Write(_textBox.Text);
}
}
});
}
}
}
+25
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;
}
}
}
+127
View File
@@ -0,0 +1,127 @@
<Window x:Class="AnotherReplayReader.EventDump"
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="流水账" Height="450" Width="800">
<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" />
<Label Content="压缩程度"
Target="_compactLevelComboBox"
VerticalAlignment="Center"
Margin="0,0,4,0" />
<ComboBox x:Name="_compactLevelComboBox"
Width="120"
SelectionChanged="OnCompactLevelComboBoxSelectionChanged" />
<Label x:Name="_tokenUsageLabel"
VerticalAlignment="Center"
Margin="4,0,0,0" />
<Button Padding="8,2"
Margin="0,0,10,0"
Content="AI分析"
Click="OnAIAnalyzeClick" />
</StackPanel>
<TabControl>
<TabItem x:Name="_eventTab"
Header="流水账">
<TextBox x:Name="_textBox"
TextWrapping="Wrap"
IsReadOnly="True"
IsUndoEnabled="False"
UndoLimit="0"
ScrollViewer.VerticalScrollBarVisibility="Auto" />
</TabItem>
<TabItem x:Name="_aiTab"
Header="AI分析结果">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="60*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="40*" />
</Grid.ColumnDefinitions>
<!-- 左侧 -->
<TextBox x:Name="_aiTextBox"
Grid.Row="0"
Grid.Column="0"
TextWrapping="Wrap"
IsReadOnly="True"
IsUndoEnabled="False"
UndoLimit="0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"/>
<!-- splitter -->
<GridSplitter Grid.Row="0"
Grid.Column="1"
Width="5"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"/>
<!-- 右侧 -->
<TextBox x:Name="_aiReasoningTextBox"
Grid.Row="0"
Grid.Column="2"
TextWrapping="Wrap"
IsReadOnly="True"
IsUndoEnabled="False"
UndoLimit="0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto"/>
<StatusBar Grid.Row="1" Grid.ColumnSpan="3">
<StatusBarItem>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="80"/>
<ColumnDefinition Width="140"/>
<ColumnDefinition Width="160"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="180"/>
</Grid.ColumnDefinitions>
<!-- elapsed time -->
<TextBlock x:Name="_elapsedTimeTextBlock"
Grid.Column="0"
TextTrimming="CharacterEllipsis"/>
<!-- total content -->
<TextBlock x:Name="_contentCountTextBlock"
Grid.Column="1"
TextTrimming="CharacterEllipsis"/>
<!-- speed -->
<TextBlock x:Name="_rateTextBlock"
Grid.Column="2"
TextTrimming="CharacterEllipsis"/>
<!-- tokens details -->
<TextBlock x:Name="_tokensDetailsTextBlock"
Grid.Column="3"
TextTrimming="CharacterEllipsis"/>
<!-- extra status -->
<TextBlock x:Name="_extraStatusTextBlock"
Grid.Column="4"
TextTrimming="CharacterEllipsis"
HorizontalAlignment="Right"
TextAlignment="Right"/>
</Grid>
</StatusBarItem>
</StatusBar>
</Grid>
</TabItem>
</TabControl>
</DockPanel>
</Window>
+575
View File
@@ -0,0 +1,575 @@
using AnotherReplayReader.Apm;
using AnotherReplayReader.ReplayFile;
using AnotherReplayReader.Utils;
using Microsoft.Win32;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Threading;
using System.Xml.Linq;
namespace AnotherReplayReader
{
/// <summary>
/// EventDump.xaml 的交互逻辑
/// </summary>
public sealed partial class EventDump : Window
{
public enum CompactLevel
{
NoCompact,
ForAI,
VeryCompactedForAI,
}
private class Model(
Mod mod,
ImmutableSortedDictionary<int, Player> players,
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> commands,
CompactLevel level
)
{
public Mod Mod { get; } = mod;
public ImmutableSortedDictionary<int, Player> Players { get; } = players;
public ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)> Commands { get; } = commands;
public CompactLevel Level { get; } = level;
public ImmutableSortedDictionary<int, string>? PlayersNamesForAI { get; } = level <= CompactLevel.NoCompact
? null
: AIAnalyze.PlayerNamesForAI(mod, players);
public bool IsDefault => Players.IsEmpty && Commands.IsEmpty;
public Model() : this(
new("RA3"),
ImmutableSortedDictionary<int, Player>.Empty,
ImmutableArray<(TimeSpan, ImmutableArray<CommandChunk>)>.Empty, CompactLevel.NoCompact
)
{
}
public string PlayerNameByPlayerListIndex(int playerId)
{
var playerRawName = $"玩家#{playerId}";
var playerName = Players.TryGetValue(playerId, out var player)
? player.PlayerName
: playerRawName;
if (PlayersNamesForAI?.TryGetValue(playerId, out var playerNameForAI) is true)
{
playerName = playerNameForAI;
}
return playerName;
}
public string PlayerNameByGameSlotIndex(int index) => PlayerNameByPlayerListIndex(Players.ElementAt(index).Key);
}
private delegate string? ToStringHook(int argumentIndex, CommandArgumentType type, int elementIndex, object value, string currentTextValue);
private static readonly Regex _matchHotkey = new("^(.*)((左右键|[A-Za-z]+))$");
private static ImmutableDictionary<uint, string> _stringHashes = ImmutableDictionary<uint, string>.Empty;
private readonly CancellationTokenSource _cancellation = new();
private Model _model = new();
private string? _cached;
private DateTimeOffset _aiStartTime;
public EventDump()
{
InitializeComponent();
Closing += EventDump_Closing;
Closed += EventDump_Closed;
// add enum values of CompactLevel to _playerComboBox
var values = Enum.GetValues(typeof(CompactLevel)).Cast<CompactLevel>();
foreach (var value in values)
{
_compactLevelComboBox.Items.Add(value.ToString());
}
_compactLevelComboBox.SelectedIndex = (int)CompactLevel.VeryCompactedForAI;
}
private void EventDump_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
try
{
_cancellation.Cancel();
}
catch (Exception ex)
{
Debug.Instance.DebugMessage += $"EventDump_Closing: {ex}\r\n";
}
}
private void EventDump_Closed(object sender, EventArgs e)
{
try
{
_cancellation.Cancel();
}
catch (Exception ex)
{
Debug.Instance.DebugMessage += $"EventDump_Closed: {ex}\r\n";
}
}
public void LoadStringHashes()
{
var stringHashes = File.ReadAllText(@"C:\Apps\RA3-MODSDK-X\builtmods\StringHashes.xml");
XDocument doc = XDocument.Parse(stringHashes);
XNamespace ns = "uri:ea.com:eala:asset";
var table = doc
.Descendants(ns + "StringHashTable")
.FirstOrDefault(x => (string?)x.Attribute("id") == "StringHashBin_INSTANCEID");
if (table == null)
throw new InvalidOperationException("StringHashBin_INSTANCEID not found.");
_stringHashes = table
.Elements(ns + "StringAndHash")
.ToImmutableDictionary(
x => uint.Parse(x.Attribute("Hash")!.Value),
x => x.Attribute("Text")!.Value);
}
internal void SetDumpData(Mod mod, ApmPlotter plotter)
{
var level = (CompactLevel)_compactLevelComboBox.SelectedIndex;
_model = new Model(mod, plotter.PlayersMap, plotter.Commands, level);
}
public async Task ShowPlainText()
{
_cached = null;
if (_model.IsDefault)
{
_textBox.Text = "";
_tokenUsageLabel.Content = "";
return;
}
var playerData = _model.Players.Values.Select(x =>
{
var factionName = ModData.GetFaction(_model.Mod, x.FactionId).Name;
return $"{x.PlayerName},队伍{x.Team},{factionName}";
});
//await analyzer.AnalyzeAsync("deepseek-v4-flash",
// AIAnalyze.GetSystemPrompt(_model.Mod, _model.Players),
// AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, text),
// deepSeekExtraParams);
_textBox.Text = "正在加载,请稍候";
_tokenUsageLabel.Content = "";
Show();
var text = await Task.Run(() => GeneratePlainText(_model));
var (bytesCount, estimatedTokenCount) = AIAnalyze.EstimateTokenCount(text);
_textBox.Text = text;
_cached = text;
// display KB and K tokens in _tokenUsageLabel
_tokenUsageLabel.Content = $"大小: {bytesCount / 1024.0:0.00} KiB,估计Token数: {estimatedTokenCount / 1000.0:0.00} K";
}
private static string GeneratePlainText(Model model)
{
var sb = new StringBuilder();
for (int chunkIndex = 0; chunkIndex < model.Commands.Length; ++chunkIndex)
{
var (time, commands) = model.Commands[chunkIndex];
var filtered = commands
.Where((c, i) => ShouldDisplay(model.Level, commands, i))
.ToList();
if (filtered.Count == 0)
{
continue;
}
sb.AppendLine($"[{TimeStampToString(time, model.Level)}]");
foreach (var command in filtered)
{
var commandName = RA3Commands.GetCommandName(command.CommandId);
if (model.Level > CompactLevel.NoCompact)
{
commandName = command.CommandId switch
{
0x1F5 => ((bool[])command.Data[0].Value)[0] switch
{
true when command.Data.Length <= 1 || command.Data[1].Count == 0 => "取消选择",
true => "重新选择单位",
false => "追加选择单位"
},
0x22E => "切换姿态",
_ => CommandNameRemoveDescription(commandName),
};
}
var playerName = model.PlayerNameByPlayerListIndex(command.PlayerIndex);
sb.AppendLine($"{playerName}: {commandName}");
AppendCommandArguments(sb, command, (i, type, j, value, text) => command.CommandId switch
{
0x1F5 when i == 0 => j switch
{
0 when model.Level > CompactLevel.NoCompact => null,
0 => (bool)value ? "替换现有选择" : "加入到当前选择",
1 => null,
_ => throw new NotImplementedException(),
},
0x1F8 when i == 0 && model.Level > CompactLevel.NoCompact => null,
0x205 or 0x206 when i == 2 => (bool)value ? "连续5个" : null,
0x205 or 0x206 when i == 3 => $"序列:{ProductionQueueTypeToString((int)value)}",
0x207 when i == 1 && j == 1 => $"序列:{ProductionQueueTypeToString((int)value)}",
0x207 or 0x208 or 0x209 when i == 0 => $"{text}(建造者)",
0x517 or 0x518 when i == 0 => $"{text}(出兵建筑)",
0x252 => $"{model.PlayerNameByGameSlotIndex((int)command.Data[0].Value)}已主动退出游戏",
0x22E when i == 0 => j == 0 ? StanceToString((int)value) : null,
_ => text,
});
sb.AppendLine();
}
}
return sb.ToString();
}
private static bool ShouldDisplay(CompactLevel level, ImmutableArray<CommandChunk> commands, int i)
{
var chunk = commands[i];
var commandId = chunk.CommandId;
if (commandId is 0x1 or 0x252)
{
return true;
}
if (ApmPlotter.IsUnknown(commandId))
{
return false;
}
if (ApmPlotter.IsAuto(commandId))
{
return false;
}
if (commandId is 0x1F5) // 选择单位
{
var isNewSelection = ((bool[])chunk.Data[0].Value)[0];
if (chunk.Data.Length <= 1 || chunk.Data[1].Count == 0)
{
// 空选择:
// 假如是追加到当前选择,那么等于无操作,没什么意义,可以过滤掉
// 假如是新建选择,那么等于取消当前选择,在最高 compact level 下也可以过滤掉
return level switch
{
<= CompactLevel.NoCompact => true,
CompactLevel.ForAI => isNewSelection,
>= CompactLevel.VeryCompactedForAI => false,
};
}
}
if (level >= CompactLevel.ForAI && commandId is 0x1F8) // 取消选择
{
if (level >= CompactLevel.VeryCompactedForAI)
{
return false;
}
var hasSelectGroupAfterThis = commands
.Skip(i + 1)
.Any(c => c.PlayerIndex == chunk.PlayerIndex && c.CommandId == 0x1FB);
if (hasSelectGroupAfterThis)
{
return false;
}
}
return true;
}
private static string TimeStampToString(TimeSpan t, CompactLevel level) => level switch
{
<= CompactLevel.NoCompact => $"{t:hh\\:mm\\:ss\\.ff}",
_ => $"{(int)t.TotalMinutes}:{t:ss\\.ff}",
};
private static string CommandNameRemoveDescription(string commandName)
{
var match = _matchHotkey.Match(commandName);
if (match.Success)
{
commandName = match.Groups[1].Value;
}
return commandName;
}
private static string ProductionQueueTypeToString(int value) => value switch
{
0 => "主要建筑",
1 => "其他建筑",
2 => "步兵",
3 => "载具",
4 => "飞行器",
5 => "升级",
6 => "舰船",
_ => $"无效({value})"
};
private static string StanceToString(int value) => value switch
{
0 => "Guard",
1 => "Aggressive",
2 => "HoldPosition",
3 => "HoldFire",
_ => $"Unknown({value})"
};
private static void AppendCommandArguments(StringBuilder sb, CommandChunk command, ToStringHook hook)
{
var count = 0;
foreach (var (argType, argValue, argCount) in command.Data)
{
++count;
var prefix = argType is CommandArgumentType.ObjectId or CommandArgumentType.ObjectId_2
? "[UnitId]"
: string.Empty;
if (argCount == 1)
{
var textValue = CommandArgumentToString(argType, argValue);
textValue = hook(count - 1, argType, 0, argValue, textValue);
if (textValue is not null)
{
sb.AppendLine($" {prefix}{textValue}");
}
}
else
{
var textValues = ((Array)argValue).Cast<object>().Select((x, i) =>
{
var textValue = CommandArgumentToString(argType, x);
textValue = hook(count - 1, argType, i, x, textValue);
return textValue;
});
if (textValues.All(x => x is null))
{
continue;
}
sb.AppendLine($" {prefix}{string.Join(",", textValues.Where(x => x is not null))}");
}
}
}
private static string CommandArgumentToString(CommandArgumentType type, object value)
{
return type switch
{
CommandArgumentType.Int32 => Int32BitToString((int)value),
CommandArgumentType.UInt32 or CommandArgumentType.UInt32_2 => Int32BitToString((uint)value),
CommandArgumentType.Float32 => ((float)value).ToString("0.##"),
CommandArgumentType.Bool
or CommandArgumentType.UInt16
or CommandArgumentType.ObjectId
or CommandArgumentType.ObjectId_2
or CommandArgumentType.AsciiString
or CommandArgumentType.UnicodeString
or CommandArgumentType.AssetId
or CommandArgumentType.Vector3 => value.ToString(),
_ => throw new InvalidOperationException($"Unknown argument type {value}"),
};
}
private static string Int32BitToString<T>(T value) where T : struct
{
// try to convert int32 or uint32 to hashes and retrieve text
uint hashValue = value switch
{
int intValue => unchecked((uint)intValue),
uint uintValue => uintValue,
_ => throw new InvalidOperationException($"Unexpected type {typeof(T)}"),
};
return _stringHashes.TryGetValue(hashValue, out var text) ? text : hashValue.ToString();
}
private void OnExportButtonClick(object sender, RoutedEventArgs e) => Export(this);
private void Export(Window owner)
{
Dispatcher.Invoke(() =>
{
var saveFileDialog = new SaveFileDialog
{
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);
}
});
}
private async void OnCompactLevelComboBoxSelectionChanged(object sender, System.Windows.Controls.SelectionChangedEventArgs e)
{
var selectedLevel = (CompactLevel)_compactLevelComboBox.SelectedIndex;
_model = new Model(_model.Mod, _model.Players, _model.Commands, selectedLevel);
await ShowPlainText();
}
private async void OnAIAnalyzeClick(object sender, RoutedEventArgs e)
{
var cached = _cached;
if (cached is null)
{
MessageBox.Show(this, "请先生成文本");
return;
}
_aiStartTime = DateTimeOffset.MaxValue;
_elapsedTimeTextBlock.Text = "";
_contentCountTextBlock.Text = "";
_rateTextBlock.Text = "";
_tokensDetailsTextBlock.Text = "";
_extraStatusTextBlock.Text = "";
var pending = new ConcurrentQueue<AIAnalyzeUI.AIAnalyzeProgressData>();
var emaSpeedCalculator = new AIAnalyzeUI.EmaSpeed();
var totalCharacters = 0;
void TimerUpdateStatus(object sender, EventArgs ea)
{
if (_aiStartTime == DateTimeOffset.MaxValue)
{
_elapsedTimeTextBlock.Text = "";
return;
}
var buffer = new Dictionary<AIAnalyze.AIChunkType, (TextBox Target, StringBuilder Text)>
{
[AIAnalyze.AIChunkType.Content] = (_aiTextBox, new StringBuilder()),
[AIAnalyze.AIChunkType.Reasoning] = (_aiReasoningTextBox, new StringBuilder()),
};
while (pending.TryDequeue(out var result))
{
var (delta, timestamp, isExtra) = result;
buffer[delta.Type].Text.Append(delta.Text);
if (!isExtra)
{
totalCharacters += delta.Text.Length;
}
emaSpeedCalculator.ProcessEvent(result);
}
var now = DateTimeOffset.UtcNow;
var elapsed = now - _aiStartTime;
_elapsedTimeTextBlock.Text = $"{elapsed:mm\\:ss}";
_contentCountTextBlock.Text = $"内容字数: {totalCharacters}";
var display = emaSpeedCalculator.GetDisplaySpeed(now);
_rateTextBlock.Text = $"{display:0.00} 字/秒;平均{totalCharacters / elapsed.TotalSeconds:0.00} 字/秒";
foreach (var kv in buffer)
{
var (target, text) = kv.Value;
if (text.Length > 0)
{
target.AppendText(text.ToString());
text.Clear();
}
}
}
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromSeconds(0.1),
};
timer.Tick += TimerUpdateStatus;
timer.Start();
try
{
await LaunchAIAnalyze(cached, pending.Enqueue);
TimerUpdateStatus(this, EventArgs.Empty);
}
catch (Exception ex)
{
MessageBox.Show(this, $"AI分析失败: {ex}");
}
finally
{
_aiStartTime = DateTimeOffset.MaxValue;
timer.Stop();
}
}
private async Task LaunchAIAnalyze(string replayData, Action<AIAnalyzeUI.AIAnalyzeProgressData> newContent)
{
void AddExtraContent(string message)
{
var delta = new AIAnalyze.AIChunk
{
Type = AIAnalyze.AIChunkType.Reasoning,
Text = message,
};
newContent(new(Delta: delta, TimeStamp: null, IsExtra: true));
delta.Type = AIAnalyze.AIChunkType.Content;
newContent(new(Delta: delta, TimeStamp: null, IsExtra: true));
}
// var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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");
var analyzer = new AIAnalyze("https://integrate.api.nvidia.com/v1/", "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");
var deepSeekExtraParams = new Dictionary<string, object>
{
["temperature"] = 0.75,
["top_p"] = 0.95,
["max_tokens"] = 16384,
["chat_template_kwargs"] = new
{
thinking = true,
reasoning_effort = "high"
},
["reasoning_effort"] = "high",
["thinking"] = new { type = "enabled" }
};
var systemPrompt = AIAnalyze.GetSystemPrompt(_model.Mod, _model.Players);
var userPrompt = AIAnalyze.BuildUserPrompt(_model.Mod, _model.Players, replayData);
#region info
var (systemPromptSize, systemPromptTokenCount) = AIAnalyze.EstimateTokenCount(systemPrompt);
var (userPromptSize, userPromptTokenCount) = AIAnalyze.EstimateTokenCount(userPrompt);
var totalPromptSize = systemPromptSize + userPromptSize;
var totalPromptTokenCount = systemPromptTokenCount + userPromptTokenCount;
var estimateText = $"系统提示大小: {systemPromptSize / 1024.0:0.00}KiB,估计Token数: {systemPromptTokenCount / 1000.0:0.00}K\r\n" +
$"用户提示大小: {userPromptSize / 1024.0:0.00} KiB,估计Token数: {userPromptTokenCount / 1000.0:0.00} K\r\n" +
$"总大小: {totalPromptSize / 1024.0:0.00} KiB,估计总Token数: {totalPromptTokenCount / 1000.0:0.00} K\r\n";
MessageBox.Show(this, estimateText, "提示", MessageBoxButton.OK, MessageBoxImage.Information);
#endregion
_extraStatusTextBlock.Text = "AI正在了解录像……";
_aiStartTime = DateTimeOffset.UtcNow;
var firstReader = AIAnalyzeUI.BuildAIChunkReader(newContent);
var result = await Task.Run(() => analyzer.AnalyzeAsync("deepseek-ai/deepseek-v4-flash",
systemPrompt,
userPrompt,
deepSeekExtraParams,
firstReader,
_cancellation.Token
));
_tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens})";
AddExtraContent("\r\n--- // 开始分段分析\r\n");
while (result.CurrentSegment < result.Segments.Count)
{
var currentSegmentName = $"{result.CurrentSegment + 1}";
_extraStatusTextBlock.Text = $"AI正在分析录像{currentSegmentName}/{result.Segments.Count}";
var segmentReader = AIAnalyzeUI.BuildAIChunkReader(newContent);
result = await Task.Run(() => analyzer.ContinueAnalyzeAsync(
segmentReader,
_cancellation.Token
));
_tokensDetailsTextBlock.Text = $"Token: {result.TotalTokens}(输入{result.PromptTokens})";
AddExtraContent($"\r\n--- // 第{currentSegmentName}段已分析完毕\r\n");
}
}
}
}
-1
View File
@@ -1 +0,0 @@
Veldrid\.SDL2\.dll
-121
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
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;
}
}
+43 -10
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>
+283 -353
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 = "")
{
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)
_cache.Save().Wait();
Application.Current.Shutdown();
};
}
private async void OnMainWindowLoaded(object sender, EventArgs eventArgs)
{
Debug.Initialize();
await _cache.Initialization;
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
_ = _taskQueue.Enqueue(() => LoadReplays(null, token), token);
const string permissionKey = "questionAsked";
if (_cache.GetOrDefault(permissionKey, false) is not true)
{
_cache.Set(permissionKey, true);
var sb = new StringWriter();
sb.WriteLine("要不要自动检查更新呢?");
sb.WriteLine("之后也可以在“关于”窗口里,设置自动更新的选项");
var choice = MessageBox.Show(this, sb.ToString(), App.Name, MessageBoxButton.YesNo);
_cache.Set(UpdateChecker.CheckForUpdatesKey, choice is MessageBoxResult.Yes);
await _cache.Save();
}
_ = UpdateChecker.CheckForUpdates(_cache).ContinueWith(t => Dispatcher.InvokeAsync(() =>
{
var updateData = t.Result;
if (updateData.IsNewVersion())
{
var about = new About(_cache, updateData)
{
Owner = this
};
about.ShowDialog();
}
}));
}
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
{
if (!IsLoaded)
{
return;
}
handling[0] = true;
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
};
cancelToken.ThrowIfCancellationRequested();
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
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()
{
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);
while (true)
{
try
{
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)
{
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(_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);
}
}
}
catch (Exception e)
{
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);
}
});
}
await Task.Delay(10 * 1000);
}
}
private async void LoadReplays(string nextSelected = null)
{
const string loadingString = "正在加载录像列表,请稍候";
Dispatcher.Invoke(() =>
{
if (_image != null)
{
_properties.CurrentReplay = null;
_image.Source = null;
}
if (_dataGrid != null)
if (_dataGrid.Items.Count > 0)
{
_dataGrid.Items.Clear();
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
});
_loadReplaysToken?.Cancel();
_loadReplaysToken = new CancellationTokenSource();
cancelToken.ThrowIfCancellationRequested();
_replayList = new();
var cancelToken = _loadReplaysToken.Token;
var path = _properties.ReplayFolderPath;
var task = Task.Run(async () =>
{
var messages = "";
var replayList = new List<Replay>();
if (!Directory.Exists(path))
{
messages = "这个文件夹并不存在。";
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
return;
}
else
var result = await Task.Run(() =>
{
var replays = Directory.EnumerateFiles(path, "*.RA3Replay");
foreach (var replayPath in replays)
var list = new List<Replay>();
var clock = new Stopwatch();
clock.Start();
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
{
cancelToken.ThrowIfCancellationRequested();
try
{
var replay = await Task.Run(() => new Replay(replayPath));
replayList.Add(replay);
_properties.ReplayDetails = loadingString + $"\n已加载 {replayList.Count} 个录像";
var replay = new Replay(replayPath);
list.Add(replay);
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
continue;
}
if (clock.ElapsedMilliseconds > 300)
{
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
clock.Restart();
}
}
return new ReplayPinyinList(list.ToImmutableArray());
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
}
_replayList = result;
await FilterReplays(string.Empty, nextSelected, filterToken);
}
return new { Replays = replayList, Messages = messages };
});
try
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
{
var result = await task;
_replayList = result.Replays;
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
DisplayReplays(result.Messages, nextSelected);
}
catch (OperationCanceledException) { }
}
_cancelDisplayReplays.Reset(cancelToken);
_filteredReplays = _replayList.Replays;
private void DisplayReplays(string message = null, string nextSelected = null)
{
var filtered = _replayList;
Dispatcher.Invoke(() =>
{
_properties.CurrentReplay = null;
_dataGrid.Items.Clear();
filtered.ForEach(x => _dataGrid.Items.Add(x));
_dataGrid.SelectedItem = null;
_image.Source = null;
if (_filterStrings.Any())
{
_properties.ReplayDetails = "正在筛选符合条件的录像…";
if (_dataGrid.Items.Count > 0)
{
_dataGrid.ItemsSource = Array.Empty<Replay>();
_dataGrid.Items.Refresh();
}
var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
var result = await Task.Run(() =>
{
var query = from replay in list.AsParallel().WithCancellation(cancelToken)
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
select replay.Replay;
return query.ToImmutableArray();
}, cancelToken);
cancelToken.ThrowIfCancellationRequested();
_filteredReplays = result;
}
_properties.CurrentReplay = null;
_dataGrid.SelectedItem = null;
_dataGrid.ItemsSource = _filteredReplays;
_dataGrid.Items.Refresh();
_properties.ReplayDetails = message;
if (nextSelected != null)
if (nextSelected is not 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))
if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
{
_dataGrid.SelectedIndex = i;
OnReplaySelectionChanged(null, null);
break;
}
}
}
});
}
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
{
LoadReplays();
if (!IsLoaded)
{
return;
}
cancelToken.ThrowIfCancellationRequested();
_properties.CurrentReplay = null;
_image.Source = null;
_properties.ReplayDetails = replayDetails;
// 开始获取小地图
var mapPath = replay.MapPath;
var minimapTask = _minimapReader.TryReadTargaAsync(replay);
// 解析录像内容
try
{
replay = await Task.Run(() => new Replay(replay.Path, true));
cancelToken.ThrowIfCancellationRequested();
}
catch (Exception e) when (e is not OperationCanceledException)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{e}\r\n";
return;
}
private void OnReplaySelectionChanged(object sender, EventArgs e)
// 假如小地图变了(这……),那么重新加载小地图
if (replay.MapPath != mapPath)
{
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
if (_properties.CurrentReplay == null)
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)
{
_replayList = _replayList.SetItem(index, replay.CloneHeader());
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
await FilterReplays(newDetails, replay.Path, token);
return;
}
}
_properties.CurrentReplay = replay;
_properties.ReplayDetails = newDetails;
try
{
var newSource = await minimapTask;
cancelToken.ThrowIfCancellationRequested();
_image.Source = newSource;
/* _image.Width = source.Width;
_image.Height = source.Height; */
}
catch (Exception e) when (e is not OperationCanceledException)
{
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
}
}
private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
{
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
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 async void OnReplaySelectionChanged(object sender, EventArgs e)
{
if (_dataGrid.SelectedItem is not Replay replay)
{
return;
}
Dispatcher.Invoke(() => { _image.Source = null; });
string GetSizeString(double size)
{
if (size > 1024 * 1024)
{
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
}
return $"{Math.Round(size / 1024)}KB";
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
_properties.ReplayDetails = replay.GetDetails();
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
}
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)
private void OnAboutButtonClick(object sender, RoutedEventArgs e)
{
lengthString = $"{replay.Length}";
}
var replaySaver = "[无法获取保存录像的玩家]";
try
var aboutWindow = new About(_cache)
{
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";
}
}
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
{
var aboutWindow = new About();
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);
}
}
}
+80 -44
View File
@@ -1,13 +1,14 @@
using System;
using AnotherReplayReader.ReplayFile;
using Pfim;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices.ComTypes;
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,22 +25,49 @@ 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))
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));
}
public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
{
using var memoryStream = new MemoryStream();
{
using var stream = TryGetStream(path, mod);
if (stream is null)
{
return null;
}
stream.CopyTo(memoryStream);
}
bool isJpgPng = IsJpgPng(memoryStream);
memoryStream.Position = 0;
if (isJpgPng)
{
try
{
var decoder = BitmapDecoder.Create(memoryStream, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.OnLoad);
return decoder.Frames[0];
}
catch { }
}
memoryStream.Position = 0;
using var targa = Targa.Create(memoryStream, new PfimConfig());
if (targa == null)
{
return null;
@@ -47,7 +75,9 @@ namespace AnotherReplayReader
try
{
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
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)
{
@@ -55,27 +85,20 @@ namespace AnotherReplayReader
return null;
}
}
}
private Targa TryGetTarga(string path, Mod mod)
private Stream? TryGetStream(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());
return File.OpenRead(minimapPath);
}
}
// now, normalize paths
path = path.Replace('/', '\\');
if (!mod.IsRA3)
{
try
{
var modSkudefPaths = Enumerable.Empty<string>();
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
@@ -83,46 +106,59 @@ namespace AnotherReplayReader
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
}
DronePlatform.BuildTechnologyAssembler();
foreach (var modSkudefPath in modSkudefPaths)
{
var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
foreach (var modBigPath in modBigPaths)
try
{
using (var modBig = new BigArchive(modBigPath))
using var fs = new SkuDefFileSystemProvider("modConfig", modSkudefPath);
if (!fs.FileExists(path))
{
var entry = modBig.GetEntry(path);
if (entry != null)
{
return Targa.Create(entry.Open(), new PfimConfig());
}
}
}
continue;
}
return fs.OpenStream(path, VirtualFileModeType.Open);
}
catch (Exception exception)
{
Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
Debug.Instance.DebugMessage += $"Exception when reading minimap from mod bigs:\r\n {exception}\r\n";
}
}
}
if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
if (_cache != null && _cache.TryGetEntry(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());
}
return big;
}
return null;
}
private static bool IsJpgPng(Stream stream)
{
var header = new byte[4];
var pos = stream.Position;
stream.Read(header, 0, header.Length);
stream.Position = pos;
// PNG
if (header[0] == 0x89 &&
header[1] == 0x50 &&
header[2] == 0x4E &&
header[3] == 0x47)
{
return true;
}
// JPEG
if (header[0] == 0xFF &&
header[1] == 0xD8)
{
return true;
}
// TGA(很粗略判断:通常无统一magic,只能 fallback)
return false;
}
}
}
+30 -70
View File
@@ -1,8 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader
{
@@ -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, "观察员") },
@@ -107,7 +97,7 @@ namespace AnotherReplayReader
{ 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;
}
}
-159
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);
}
}
}
-26
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
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
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);
}
}
}
}
+83
View File
@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
namespace AnotherReplayReader.ReplayFile
{
public enum CommandArgumentType
{
Int32 = 0,
Float32 = 1,
Bool = 2,
AssetId = 3,
ObjectId = 4,
ObjectId_2 = 5,
UInt32 = 6,
Vector3 = 7,
UInt32_2 = 8,
UInt16 = 9,
AsciiString = 10,
UnicodeString = 11,
}
public record struct CommandArgumentEntry(CommandArgumentType Type, object Value, int Count);
public record struct Vector3(float X, float Y, float Z)
{
public override readonly string ToString() => $"(X={Math.Round(X)},Y={Math.Round(Y)},Z={Math.Round(Z)})";
}
public record struct AssetId(uint TypeId, uint InstanceId)
{
public override readonly string ToString() => $"(TypeId={TypeId:X},InstanceId={InstanceId:X})";
}
public sealed class CommandChunk
{
public int CommandId { get; private set; }
public int PlayerIndex { get; private set; }
public ImmutableArray<CommandArgumentEntry> Data { get; private set; }
public static List<CommandChunk> Parse(ReplayChunk chunk)
{
if (chunk.Type != 1)
{
throw new NotImplementedException();
}
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 commandIdAndPlayerId = reader.ReadUInt16();
var commandId = commandIdAndPlayerId & 0x7FF;
var playerId = commandIdAndPlayerId >> 11;
var data = reader.ReadCommandData();
list.Add(new CommandChunk
{
CommandId = commandId,
PlayerIndex = playerId,
Data = data.ToImmutableArray(),
});
}
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
View File
@@ -0,0 +1,47 @@
using System.Collections.Generic;
namespace AnotherReplayReader.ReplayFile
{
public 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);
}
}
}
}
+221
View File
@@ -0,0 +1,221 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Text;
namespace AnotherReplayReader.ReplayFile
{
internal static class RA3Commands
{
private interface IArgumentReader
{
object Read(BinaryReader reader, int count);
}
private sealed class ArgumentReader<T> : IArgumentReader
{
private readonly Func<BinaryReader, T> _reader;
public ArgumentReader(Func<BinaryReader, T> reader) => _reader = reader;
public object Read(BinaryReader reader, int count)
{
if (count == 1)
{
return _reader(reader)!;
}
var result = new T[count];
for (int i = 0; i < count; i++)
{
result[i] = _reader(reader);
}
return result;
}
}
private static readonly Dictionary<CommandArgumentType, IArgumentReader> _readers = new()
{
[CommandArgumentType.Int32] = new ArgumentReader<int>(r => r.ReadInt32()),
[CommandArgumentType.Float32] = new ArgumentReader<float>(r => r.ReadSingle()),
[CommandArgumentType.Bool] = new ArgumentReader<bool>(r => r.ReadByte() != 0),
[CommandArgumentType.UInt16] = new ArgumentReader<ushort>(r => r.ReadUInt16()),
[CommandArgumentType.UInt32] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.UInt32_2] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.ObjectId] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.ObjectId_2] = new ArgumentReader<uint>(r => r.ReadUInt32()),
[CommandArgumentType.Vector3] = new ArgumentReader<Vector3>(ReadVector3),
[CommandArgumentType.AssetId] = new ArgumentReader<AssetId>(ReadAssetId),
[CommandArgumentType.AsciiString] = new ArgumentReader<string>(r => ReadString(r, CommandArgumentType.AsciiString)),
[CommandArgumentType.UnicodeString] = new ArgumentReader<string>(r => ReadString(r, CommandArgumentType.UnicodeString)),
};
private static readonly Dictionary<int, string> _commandNames;
public static ImmutableArray<int> UnknownCommands { get; } = new[]
{
0x1FD, // unk
0x25F, // unk
}.ToImmutableArray();
public static ImmutableArray<int> AutoCommands { get; } = new[]
{
0x1,
0x221, // 3 seconds heartbeat
0x233, // uuid
0x234, // uuid
0x235, // player info
0x237, // indeterminate autogen
0x247, // 5th frame auto gen
0x252, // another player quits
}.ToImmutableArray();
static RA3Commands()
{
_commandNames = new()
{
[0x1] = "[游戏结束]", // 1
[0x1F5] = "选择单位", // 501
[0x1F6] = "选择相同单位(W)", // 502
[0x1F8] = "取消选择", // 504
[0x1F9] = "从选择中移除单位", // 505
[0x1FA] = "创建编队", // 506
[0x1FB] = "选择编队", // 507
[0x1FC] = "将编队加入选择", // 508
[0x1FD] = "(未知指令 0x1FD)", // 509
[0x1FE] = "释放特殊能力(无目标)", // 510
[0x1FF] = "释放特殊能力(指定位置)", // 511
[0x200] = "释放特殊能力(指定位置和角度)", // 512
[0x201] = "释放特殊能力(指定目标)", // 513
[0x202] = "设置集结点", // 514
[0x203] = "开始升级", // 515
[0x204] = "暂停/中止升级", // 516
[0x205] = "开始出兵", // 517
[0x206] = "暂停/取消出兵", // 518
[0x207] = "开始建造", // 519
[0x208] = "暂停/取消建造", // 520
[0x209] = "摆放建筑", // 521
[0x20A] = "出售建筑", // 522
// 523
[0x20C] = "从进驻的建筑撤出(?)", // 524
[0x20D] = "集火攻击", // 525
[0x20E] = "强制攻击单位(Ctrl)", // 526
[0x20F] = "强制攻击地板(Ctrl)", // 527
[0x210] = "进驻建筑", // 528
// 529
[0x212] = "命令矿车交矿", // 530
// 531
[0x214] = "移动", // 532
[0x215] = "行进攻击(A)", // 533
[0x216] = "强制移动/碾压(G)", // 534
// 535
// 536
// 537
[0x21A] = "停止(S)", // 538
[0x21B] = "散开(X)", // 539
// 0x21E 542 有可能是 AI 信标相关的?
[0x221] = "[游戏每3秒自动产生的检测不同步指令]", // 545
[0x228] = "开始维修建筑", // 552
[0x229] = "停止维修建筑", // 553
[0x22A] = "选择所有单位(Q)", // 554
// 555
[0x22C] = "队形操作(左右键)", // 556
// 557
[0x22E] = "切换姿态(警戒/侵略/固守/停火模式)", // 558
[0x22F] = "路径点模式/计划模式(Alt)", // 559
// 560
// 561
[0x232] = "释放特殊能力(一个或多个目标)", // 562
[0x233] = "[游戏自动生成的UUID指令]", // 563
[0x234] = "[游戏自动产生的UUID]", // 564
[0x235] = "[玩家信息(?)]", // 565
[0x236] = "倒车(D)", // 566
[0x237] = "[游戏不定期自动产生的指令]", // 567
[0x247] = "[游戏在第五帧自动产生的指令]", // 583
[0x248] = "让矿车去采矿", // 584
[0x24B] = "信标", // 587
[0x24C] = "删除信标", // 588
[0x24D] = "在信标里输入文字", // 589
[0x24E] = "选择协议", // 590
[0x252] = "[其他玩家主动退出游戏]", // 594
[0x25F] = "(未知指令 0x25F)", // 607
};
}
public static List<CommandArgumentEntry> ReadCommandData(this BinaryReader reader)
{
var list = new List<CommandArgumentEntry>();
while (true)
{
byte head = reader.ReadByte();
if (head == 0xFF)
break;
int count = (head >> 4) + 1;
var type = (CommandArgumentType)(head & 0xF);
var value = _readers[type].Read(reader, count);
list.Add(new CommandArgumentEntry(type, value, count));
}
return list;
}
public static bool IsUnknownCommand(int commandId)
{
return !_commandNames.ContainsKey(commandId);
}
public static string GetCommandName(int commandId)
{
return _commandNames.TryGetValue(commandId, out var storedName)
? storedName
: $"(未知指令 0x{commandId:X2})";
}
private static AssetId ReadAssetId(BinaryReader reader)
{
var version = reader.ReadByte();
var typeId = reader.ReadUInt32();
var instanceId = reader.ReadUInt32();
return new(TypeId: typeId, InstanceId: instanceId);
}
private static Vector3 ReadVector3(BinaryReader reader)
{
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
return new(X: x, Y: y, Z: z);
}
private static string ReadString(BinaryReader current, CommandArgumentType type)
{
var length = (int)current.ReadByte();
if (length == 0xFF)
{
length = current.ReadInt32();
}
// read byte string or wchar_t string based on T type
switch (type)
{
case CommandArgumentType.AsciiString:
return Encoding.UTF8.GetString(current.ReadBytes(length));
case CommandArgumentType.UnicodeString:
return Encoding.Unicode.GetString(current.ReadBytes(length * 2));
}
throw new InvalidDataException($"Invalid string type {type}");
}
}
}
+315
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));
}
}
}
}
+38
View File
@@ -0,0 +1,38 @@
using System;
using System.IO;
namespace AnotherReplayReader.ReplayFile
{
public 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);
}
}
}
+40
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);
}
}
+92
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
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);
}
}
}
+1115
View File
File diff suppressed because it is too large Load Diff
+89
View File
@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace AnotherReplayReader.Utils
{
internal static class AIAnalyzeUI
{
public record AIAnalyzeProgressData(AIAnalyze.AIChunk Delta, DateTimeOffset? TimeStamp, bool IsExtra);
public class EmaSpeed
{
const double Tau = 5;
private DateTimeOffset lastEventTime = DateTimeOffset.UtcNow;
private DateTimeOffset timeSinceLastSpeedMeasure = DateTimeOffset.UtcNow;
private int bufferedCharactersSinceLastSpeedMeasure = 0;
private double emaSpeed = double.NaN;
public void ProcessEvent(AIAnalyzeProgressData data)
{
if (data.IsExtra)
{
return;
}
if (data.TimeStamp is { } timestamp)
{
lastEventTime = timestamp;
}
bufferedCharactersSinceLastSpeedMeasure += data.Delta.Text.Length;
}
public double GetDisplaySpeed(DateTimeOffset now)
{
// =========================
// 2. 计算 instant speed(真实时间)
// =========================
double instant = 0;
double dt = (now - timeSinceLastSpeedMeasure).TotalSeconds;
if (bufferedCharactersSinceLastSpeedMeasure > 0 && dt > 0.05)
{
instant = bufferedCharactersSinceLastSpeedMeasure / dt;
// reset window
bufferedCharactersSinceLastSpeedMeasure = 0;
timeSinceLastSpeedMeasure = now;
}
double alpha = 1 - Math.Exp(-dt / Tau);
if (double.IsNaN(emaSpeed))
{
emaSpeed = instant;
}
else
{
emaSpeed += alpha * (instant - emaSpeed);
}
double idle = (now - lastEventTime).TotalSeconds;
double display = emaSpeed;
if (idle > 0.5)
{
double decay = Math.Exp(-(idle - 0.5) / Tau);
display *= decay;
}
return display;
}
}
public static Action<AIAnalyze.AIChunk> BuildAIChunkReader(Action<AIAnalyzeProgressData> newContent)
{
void OnAIChunk(AIAnalyze.AIChunk c)
{
if (c.Type == AIAnalyze.AIChunkType.Json)
{
return;
}
newContent(new(Delta: c, TimeStamp: DateTimeOffset.UtcNow, IsExtra: false));
}
return OnAIChunk;
}
}
}
+48
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;
}
}
}
+23
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);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
namespace System.Runtime.CompilerServices
{
internal static class IsExternalInit
{
}
}
+20
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
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
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
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
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
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
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
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
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
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;
}
}
}
}
}
+9 -5
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>
+63 -81
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,98 @@ 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)
{
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 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);
}
});
return Task.CompletedTask;
}
private async void OnClick(object sender, RoutedEventArgs e)
{
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
await Task.Run(() =>
{
var ipText = Dispatcher.Invoke(() => _ipField.Text);
if (!IPAddress.TryParse(ipText, out var ip))
{
Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
return;
}
var idText = Dispatcher.Invoke(() => _idField.Text);
if (string.IsNullOrWhiteSpace(idText))
{
var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
if(result != MessageBoxResult.OK)
{
return;
}
}
_setIPButton.IsEnabled = false;
try
{
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 ipText = _ipField.Text;
if (!IPAddress.TryParse(ipText, out var ip))
{
var response = reader.ReadToEnd();
var serializer = new JavaScriptSerializer();
var result = serializer.Deserialize<bool>(response);
MessageBox.Show(this, "IP 格式不正确");
return;
}
var idText = _idField.Text;
if (string.IsNullOrWhiteSpace(idText))
{
var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
if (choice != MessageBoxResult.OK)
{
return;
}
}
var result = await UpdateIpTable(ip, idText);
if (!result)
{
Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
}
MessageBox.Show(this, "设置 IP 表失败");
}
Refresh(false);
}
catch (Exception exception)
{
Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
MessageBox.Show(this, $"设置 IP 表时发生错误。\r\n{exception}");
}
});
Refresh();
}
private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
finally
{
await Task.Run(() =>
_setIPButton.IsEnabled = true;
}
}
private Task<bool> UpdateIpTable(IPAddress ip, string idText)
{
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 Task.FromResult(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
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>
-95
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);
}
}