Add project files.
This commit is contained in:
parent
97cf4333ea
commit
1c19badeb5
63
APM.xaml
Normal file
63
APM.xaml
Normal file
@ -0,0 +1,63 @@
|
||||
<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
APM.xaml.cs
Normal file
230
APM.xaml.cs
Normal file
@ -0,0 +1,230 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
33
About.xaml
Normal file
33
About.xaml
Normal file
@ -0,0 +1,33 @@
|
||||
<Window x:Class="AnotherReplayReader.About"
|
||||
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="About"
|
||||
Height="301.893"
|
||||
Width="429">
|
||||
<Grid Margin="0,0,-8,-59">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="280"/>
|
||||
<RowDefinition Height="50"/>
|
||||
</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>
|
||||
</Grid>
|
||||
</Window>
|
34
About.xaml.cs
Normal file
34
About.xaml.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
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
|
||||
{
|
||||
public About()
|
||||
{
|
||||
InitializeComponent();
|
||||
_idBox.Text = Auth.ID;
|
||||
}
|
||||
|
||||
private void Hyperlink_RequestNavigate(object sender, System.Windows.Navigation.RequestNavigateEventArgs e)
|
||||
{
|
||||
Process.Start(e.Uri.ToString());
|
||||
}
|
||||
}
|
||||
}
|
193
AnotherReplayReader.csproj
Normal file
193
AnotherReplayReader.csproj
Normal file
@ -0,0 +1,193 @@
|
||||
<?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')" />
|
||||
<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>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<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>
|
||||
</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" />
|
||||
<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" />
|
||||
</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" />
|
||||
</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">
|
||||
<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">
|
||||
<Generator>SettingsSingleFileGenerator</Generator>
|
||||
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<BootstrapperPackage Include=".NETFramework,Version=v4.6.1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>Microsoft .NET Framework 4.6.1 %28x86 和 x64%29</ProductName>
|
||||
<Install>true</Install>
|
||||
</BootstrapperPackage>
|
||||
<BootstrapperPackage Include="Microsoft.Net.Framework.3.5.SP1">
|
||||
<Visible>False</Visible>
|
||||
<ProductName>.NET Framework 3.5 SP1</ProductName>
|
||||
<Install>false</Install>
|
||||
</BootstrapperPackage>
|
||||
</ItemGroup>
|
||||
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
|
||||
</Project>
|
25
AnotherReplayReader.sln
Normal file
25
AnotherReplayReader.sln
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30907.101
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnotherReplayReader", "AnotherReplayReader.csproj", "{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{A54AEAB3-D99C-4E29-8C47-3DFD5B1A0FDE}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {352AD5BB-C7E0-49AC-8DDA-A9564C31AB8B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
9
App.xaml
Normal file
9
App.xaml
Normal file
@ -0,0 +1,9 @@
|
||||
<Application x:Class="AnotherReplayReader.App"
|
||||
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">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
16
App.xaml.cs
Normal file
16
App.xaml.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
/// <summary>
|
||||
/// App.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
}
|
93
Auth.cs
Normal file
93
Auth.cs
Normal file
@ -0,0 +1,93 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
231
BigMinimapCache.cs
Normal file
231
BigMinimapCache.cs
Normal file
@ -0,0 +1,231 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using OpenSage.FileFormats.Big;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class BigMinimapCache
|
||||
{
|
||||
private sealed class CacheAdapter
|
||||
{
|
||||
public List<string> Bigs { get; set; }
|
||||
public Dictionary<string, string> MapsToBigs { get; set; }
|
||||
|
||||
public CacheAdapter()
|
||||
{
|
||||
Bigs = new List<string>();
|
||||
MapsToBigs = new Dictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
//private Cache _cache;
|
||||
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = 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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_mapsToBigs.ContainsKey(path))
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bigPath = _mapsToBigs[path];
|
||||
big = new BigArchive(bigPath);
|
||||
if(big.GetEntry(path) == null)
|
||||
{
|
||||
//_cache.Remove("bigsCache");
|
||||
big.Dispose();
|
||||
big = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
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)
|
||||
{
|
||||
if(_mapsToBigs == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!_mapsToBigs.ContainsKey(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bigPath = _mapsToBigs[path];
|
||||
using (var big = new BigArchive(path))
|
||||
{
|
||||
var entry = big.GetEntry(path);
|
||||
using (var stream = entry.Open())
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
return reader.ReadBytes((int)entry.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
|
||||
//_cache.Remove("bigsCache");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
79
Cache.cs
Normal file
79
Cache.cs
Normal file
@ -0,0 +1,79 @@
|
||||
using System;
|
||||
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;
|
||||
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class Cache
|
||||
{
|
||||
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
||||
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
||||
|
||||
private ConcurrentDictionary<string, string> _storage;
|
||||
|
||||
public Cache()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(CacheDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
}
|
||||
|
||||
var serializer = new JavaScriptSerializer();
|
||||
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_storage = new ConcurrentDictionary<string, string>();
|
||||
}
|
||||
}
|
||||
|
||||
public T GetOrDefault<T>(string key, in T defaultValue)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_storage.TryGetValue(key, out var valueString))
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
return serializer.Deserialize<T>(valueString);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public void Set<T>(string key, in T value)
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
_storage[key] = serializer.Serialize(value);
|
||||
Save();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public void Remove(string key)
|
||||
{
|
||||
_storage.TryRemove(key, out _);
|
||||
Save();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
303
CommandChunk.cs
Normal file
303
CommandChunk.cs
Normal file
@ -0,0 +1,303 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
13
Debug.xaml
Normal file
13
Debug.xaml
Normal file
@ -0,0 +1,13 @@
|
||||
<Window x:Class="AnotherReplayReader.Debug"
|
||||
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="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>
|
||||
</Window>
|
68
Debug.xaml.cs
Normal file
68
Debug.xaml.cs
Normal file
@ -0,0 +1,68 @@
|
||||
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 event PropertyChangedEventHandler PropertyChanged;
|
||||
public string DebugMessage
|
||||
{
|
||||
get => _debugMessage;
|
||||
set
|
||||
{
|
||||
_debugMessage = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("DebugMessage"));
|
||||
}
|
||||
}
|
||||
|
||||
private string _debugMessage;
|
||||
}
|
||||
/// <summary>
|
||||
/// Debug.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
public sealed partial class Debug : Window
|
||||
{
|
||||
public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
|
||||
|
||||
public Debug()
|
||||
{
|
||||
DataContext = Instance;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void OnExport_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var saveFileDialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "文本文档 (*.txt)|*.txt|所有文件 (*.*)|*.*",
|
||||
OverwritePrompt = true,
|
||||
};
|
||||
|
||||
var result = saveFileDialog.ShowDialog(this);
|
||||
if (result == true)
|
||||
{
|
||||
using (var file = saveFileDialog.OpenFile())
|
||||
using (var writer = new StreamWriter(file))
|
||||
{
|
||||
writer.Write(Instance.DebugMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
Excludes.txt
Normal file
1
Excludes.txt
Normal file
@ -0,0 +1 @@
|
||||
Veldrid\.SDL2\.dll
|
121
ILMergeConfig.json
Normal file
121
ILMergeConfig.json
Normal file
@ -0,0 +1,121 @@
|
||||
{
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
70
MainWindow.xaml
Normal file
70
MainWindow.xaml
Normal file
@ -0,0 +1,70 @@
|
||||
<Window x:Class="AnotherReplayReader.MainWindow"
|
||||
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="MainWindow" Width="800" Height="600">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20"/>
|
||||
<RowDefinition Height="30"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" MinWidth="85"/>
|
||||
<ColumnDefinition Width="66*"/>
|
||||
<ColumnDefinition Width="449*"/>
|
||||
<ColumnDefinition Width="95"/>
|
||||
<ColumnDefinition Width="97"/>
|
||||
</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"/>
|
||||
</Grid>
|
||||
<Grid Grid.Row="2">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition/>
|
||||
<RowDefinition Height="156"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="175*"/>
|
||||
<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" />
|
||||
<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.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="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"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<Button x:Name="_detailsButton"
|
||||
Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Content="详细信息"
|
||||
Margin="10,0,20,16"
|
||||
Height="35"
|
||||
VerticalAlignment="Bottom"
|
||||
IsEnabled="{Binding Path=ReplaySelected}"
|
||||
Click="OnDetailsButton_Click" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Window>
|
537
MainWindow.xaml.cs
Normal file
537
MainWindow.xaml.cs
Normal file
@ -0,0 +1,537 @@
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class MainWindowProperties : INotifyPropertyChanged
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
||||
{
|
||||
userDataLeafName = "Red Alert 3";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(replayFolderName))
|
||||
{
|
||||
replayFolderName = "Replays";
|
||||
}
|
||||
|
||||
RA3ReplayFolderPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, replayFolderName);
|
||||
ReplayFolderPath = RA3ReplayFolderPath;
|
||||
CustomMapsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), userDataLeafName, "Maps");
|
||||
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
||||
}
|
||||
|
||||
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 = "")
|
||||
{
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
public string RA3Directory { get; }
|
||||
public string RA3ReplayFolderPath { get; }
|
||||
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
||||
public string CustomMapsDirectory { get; }
|
||||
public string ModsDirectory { get; }
|
||||
|
||||
public string ReplayFolderPath
|
||||
{
|
||||
get { return _replayFolderPath; }
|
||||
set { _replayFolderPath = value; NotifyPropertyChanged(_replayFolderPath); }
|
||||
}
|
||||
|
||||
public string ReplayFilterString
|
||||
{
|
||||
get { return _replayFilterString; }
|
||||
set { _replayFilterString = value; NotifyPropertyChanged(_replayFilterString); }
|
||||
}
|
||||
|
||||
public string ReplayDetails
|
||||
{
|
||||
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; }
|
||||
set
|
||||
{
|
||||
_currentReplay = value;
|
||||
NotifyPropertyChanged(_currentReplay);
|
||||
ReplayDetails = "";
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayPlayable"));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplaySelected"));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("ReplayDamaged"));
|
||||
}
|
||||
}
|
||||
|
||||
private volatile string _replayFolderPath;
|
||||
private volatile string _replayDetails;
|
||||
private volatile string _replayFilterString;
|
||||
private volatile Replay _currentReplay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MainWindow.xaml 的交互逻辑
|
||||
/// </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;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
DataContext = _properties;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
var handling = new bool[1] { false };
|
||||
Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
|
||||
{
|
||||
if (handling == null || handling[0] == true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
handling[0] = true;
|
||||
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
_image.Source = null;
|
||||
}
|
||||
|
||||
if (_dataGrid != null)
|
||||
{
|
||||
_dataGrid.Items.Clear();
|
||||
}
|
||||
});
|
||||
|
||||
_loadReplaysToken?.Cancel();
|
||||
_loadReplaysToken = new CancellationTokenSource();
|
||||
|
||||
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 = "这个文件夹并不存在。";
|
||||
}
|
||||
else
|
||||
{
|
||||
var replays = Directory.EnumerateFiles(path, "*.RA3Replay");
|
||||
foreach (var replayPath in replays)
|
||||
{
|
||||
try
|
||||
{
|
||||
var replay = await Task.Run(() => new Replay(replayPath));
|
||||
replayList.Add(replay);
|
||||
_properties.ReplayDetails = loadingString + $"\n已加载 {replayList.Count} 个录像";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
||||
}
|
||||
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
}
|
||||
|
||||
return new { Replays = replayList, Messages = messages };
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
var result = await task;
|
||||
_replayList = result.Replays;
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
DisplayReplays(result.Messages, nextSelected);
|
||||
}
|
||||
catch (OperationCanceledException) { }
|
||||
}
|
||||
|
||||
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));
|
||||
_properties.ReplayDetails = message;
|
||||
|
||||
if (nextSelected != null)
|
||||
{
|
||||
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
||||
{
|
||||
var replay = _dataGrid.Items[i] as Replay;
|
||||
if (replay == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_dataGrid.SelectedIndex = i;
|
||||
OnReplaySelectionChanged(null, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
||||
{
|
||||
LoadReplays();
|
||||
}
|
||||
|
||||
private void OnReplaySelectionChanged(object sender, EventArgs e)
|
||||
{
|
||||
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
|
||||
if (_properties.CurrentReplay == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() => { _image.Source = null; });
|
||||
|
||||
string GetSizeString(double size)
|
||||
{
|
||||
if (size > 1024 * 1024)
|
||||
{
|
||||
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
||||
}
|
||||
return $"{Math.Round(size / 1024)}KB";
|
||||
}
|
||||
|
||||
const string formatA = "文件名:{0}\n大小:{1}\n";
|
||||
const string formatB = "地图:{0}\n日期:{1}\n长度:{2}\n";
|
||||
const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n";
|
||||
const string playerListTitle = "玩家列表:\n";
|
||||
var replay = _properties.CurrentReplay;
|
||||
var sizeString = GetSizeString(replay.Size);
|
||||
var lengthString = "录像已损坏,请先修复录像";
|
||||
if (replay.HasFooter)
|
||||
{
|
||||
lengthString = $"{replay.Length}";
|
||||
}
|
||||
|
||||
var replaySaver = "[无法获取保存录像的玩家]";
|
||||
try
|
||||
{
|
||||
replaySaver = replay.ReplaySaver.PlayerName;
|
||||
}
|
||||
catch { }
|
||||
|
||||
_properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString);
|
||||
_properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString);
|
||||
_properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver);
|
||||
_properties.ReplayDetails += playerListTitle;
|
||||
foreach (var player in replay.Players)
|
||||
{
|
||||
if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name;
|
||||
var realName = replay.Type == ReplayType.Lan ? _playerIdentity.QueryRealName(player.PlayerIP) : string.Empty;
|
||||
_properties.ReplayDetails += $"{player.PlayerName + realName},{factionName}\n";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var mapPath = replay.MapPath.TrimEnd('/');
|
||||
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
|
||||
var minimapPath = $"{mapPath}/{mapName}_art.tga";
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
var source = _minimapReader.TryReadTarga(minimapPath, replay.Mod);
|
||||
_image.Source = source;
|
||||
/*_image.Width = source.Width;
|
||||
_image.Height = source.Height;*/
|
||||
});
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{exception}\r\n";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
replay.ParseBody();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var aboutWindow = new About();
|
||||
aboutWindow.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnBrowseButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var openFileDialog = new OpenFileDialog
|
||||
{
|
||||
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
||||
InitialDirectory = _properties.ReplayFolderPath,
|
||||
};
|
||||
|
||||
var result = openFileDialog.ShowDialog();
|
||||
if (result == true)
|
||||
{
|
||||
var fileName = openFileDialog.FileName;
|
||||
var directoryName = Path.GetDirectoryName(fileName);
|
||||
_properties.ReplayFolderPath = directoryName;
|
||||
LoadReplays(fileName);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Cannot set replay folder: \r\n{exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
|
||||
detailsWindow.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnDebugButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var debug = new Debug();
|
||||
debug.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnPlayReplayButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
|
||||
}
|
||||
|
||||
private void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
var replay = _properties.CurrentReplay;
|
||||
|
||||
var saveFileDialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
||||
InitialDirectory = Path.GetDirectoryName(replay.Path),
|
||||
OverwritePrompt = true,
|
||||
Title = "保存已被修复的录像"
|
||||
};
|
||||
|
||||
var result = saveFileDialog.ShowDialog(this);
|
||||
if (result == true)
|
||||
{
|
||||
using (var file = saveFileDialog.OpenFile())
|
||||
using (var writer = new BinaryWriter(file))
|
||||
{
|
||||
writer.WriteReplay(replay);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
MessageBox.Show($"无法修复录像:\r\n{exception}");
|
||||
}
|
||||
|
||||
LoadReplays();
|
||||
}
|
||||
}
|
||||
}
|
128
MinimapReader.cs
Normal file
128
MinimapReader.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using OpenSage.FileFormats.Big;
|
||||
using Pfim;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class MinimapReader
|
||||
{
|
||||
public static readonly IReadOnlyDictionary<ImageFormat, PixelFormat> FormatMapper = new Dictionary<ImageFormat, PixelFormat>
|
||||
{
|
||||
{ ImageFormat.Rgb24, PixelFormats.Bgr24 },
|
||||
{ ImageFormat.Rgba32, PixelFormats.Bgr32 },
|
||||
{ ImageFormat.Rgb8, PixelFormats.Gray8 },
|
||||
{ ImageFormat.R5g5b5a1, PixelFormats.Bgr555 },
|
||||
{ ImageFormat.R5g5b5, PixelFormats.Bgr555 },
|
||||
{ ImageFormat.R5g6b5, PixelFormats.Bgr565 },
|
||||
};
|
||||
|
||||
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)
|
||||
{
|
||||
_cache = cache;
|
||||
_ra3InstallPath = ra3InstallPath;
|
||||
_mapFolderPath = mapFolderPath;
|
||||
_modFolderPath = modFolderPath;
|
||||
}
|
||||
|
||||
public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
||||
{
|
||||
using (var targa = TryGetTarga(path, mod))
|
||||
{
|
||||
if(targa == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Targa TryGetTarga(string path, Mod mod)
|
||||
{
|
||||
var tga = null as Targa;
|
||||
const string customMapPrefix = "data/maps/internal/";
|
||||
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
||||
{
|
||||
var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length));
|
||||
if(File.Exists(minimapPath))
|
||||
{
|
||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
// now, normalize paths
|
||||
path = path.Replace('/', '\\');
|
||||
|
||||
if(!mod.IsRA3)
|
||||
{
|
||||
try
|
||||
{
|
||||
var modSkudefPaths = Enumerable.Empty<string>();
|
||||
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
|
||||
{
|
||||
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
|
||||
}
|
||||
|
||||
foreach (var modSkudefPath in modSkudefPaths)
|
||||
{
|
||||
var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
|
||||
foreach (var modBigPath in modBigPaths)
|
||||
{
|
||||
using (var modBig = new BigArchive(modBigPath))
|
||||
{
|
||||
var entry = modBig.GetEntry(path);
|
||||
if (entry != null)
|
||||
{
|
||||
return Targa.Create(entry.Open(), new PfimConfig());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
|
||||
{
|
||||
using (big)
|
||||
{
|
||||
return Targa.Create(big.GetEntry(path).Open(), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
if (Directory.Exists(_ra3InstallPath))
|
||||
{
|
||||
var minimapPath = Path.Combine(_mapFolderPath, path);
|
||||
if (File.Exists(minimapPath))
|
||||
{
|
||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
258
ModData.cs
Normal file
258
ModData.cs
Normal file
@ -0,0 +1,258 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
enum FactionKind
|
||||
{
|
||||
Player,
|
||||
Observer,
|
||||
Unknown
|
||||
}
|
||||
|
||||
internal sealed class Mod : IComparable
|
||||
{
|
||||
public readonly string ModName;
|
||||
public readonly string ModVersion;
|
||||
public bool IsRA3 => ModName.Equals("RA3");
|
||||
|
||||
public Mod(string modInfo)
|
||||
{
|
||||
var splitted = modInfo.Split(new char[] { '\0' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
ModName = splitted[0];
|
||||
ModVersion = string.Empty;
|
||||
if (IsRA3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
ModVersion = splitted[1];
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
if (IsRA3)
|
||||
{
|
||||
return "原版";
|
||||
}
|
||||
return ModName + ' ' + ModVersion;
|
||||
}
|
||||
|
||||
public int CompareTo(object other)
|
||||
{
|
||||
if(!(other is Mod))
|
||||
{
|
||||
return GetType().FullName.CompareTo(other.GetType().FullName);
|
||||
}
|
||||
|
||||
var otherMod = (Mod)other;
|
||||
if(IsRA3 != otherMod.IsRA3)
|
||||
{
|
||||
if (IsRA3)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
var modCompare = ModName.CompareTo(otherMod.ModName);
|
||||
if (modCompare != 0)
|
||||
{
|
||||
return modCompare;
|
||||
}
|
||||
|
||||
return ModVersion.CompareTo(otherMod.ModVersion);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class Faction
|
||||
{
|
||||
public readonly FactionKind Kind;
|
||||
public readonly string Name;
|
||||
|
||||
public Faction(FactionKind kind, string name)
|
||||
{
|
||||
Kind = kind;
|
||||
Name = name;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
static ModData()
|
||||
{
|
||||
_ra3Factions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3ARFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
||||
{ 2, new Faction(FactionKind.Player, "天琼") },
|
||||
{ 3, new Faction(FactionKind.Player, "OB") },
|
||||
{ 4, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 5, new Faction(FactionKind.Player, "鹰眼") },
|
||||
{ 6, new Faction(FactionKind.Player, "解说员") },
|
||||
{ 7, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 8, new Faction(FactionKind.Player, "克格勃") },
|
||||
{ 11, new Faction(FactionKind.Player, "血月") },
|
||||
{ 12, new Faction(FactionKind.Player, "随机") },
|
||||
{ 13, new Faction(FactionKind.Player, "苏联") },
|
||||
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
};
|
||||
_ra3CoronaFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
{ 9, new Faction(FactionKind.Player, "神州") },
|
||||
};
|
||||
_ra3DawnFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "禁卫军") },
|
||||
{ 5, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 9, new Faction(FactionKind.Player, "革命军") },
|
||||
{ 10, new Faction(FactionKind.Player, "德法同盟") },
|
||||
{ 11, new Faction(FactionKind.Player, "随机") },
|
||||
{ 12, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3INSFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3FSFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3EisenreichFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "德国") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3TNWFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "帝国") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3WOPFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
{ 2, new Faction(FactionKind.Player, "日本") },
|
||||
{ 3, new Faction(FactionKind.Observer, "解说员") },
|
||||
{ 4, new Faction(FactionKind.Player, "美国") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_unknown = new Faction(FactionKind.Unknown, "未知阵营");
|
||||
}
|
||||
|
||||
public static Faction GetFaction(Mod mod, int factionID)
|
||||
{
|
||||
new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
|
||||
|
||||
if
|
||||
(mod.ModName.Equals("RA3"))
|
||||
{
|
||||
return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
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
PlayerIdentity.cs
Normal file
159
PlayerIdentity.cs
Normal file
@ -0,0 +1,159 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
55
Properties/AssemblyInfo.cs
Normal file
55
Properties/AssemblyInfo.cs
Normal file
@ -0,0 +1,55 @@
|
||||
using System.Reflection;
|
||||
using System.Resources;
|
||||
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。
|
||||
[assembly: ComVisible(false)]
|
||||
|
||||
//若要开始生成可本地化的应用程序,请设置
|
||||
//.csproj 文件中的 <UICulture>CultureYouAreCodingWith</UICulture>
|
||||
//例如,如果您在源文件中使用的是美国英语,
|
||||
//使用的是美国英语,请将 <UICulture> 设置为 en-US。 然后取消
|
||||
//对以下 NeutralResourceLanguage 特性的注释。 更新
|
||||
//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
|
||||
|
||||
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
|
||||
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //主题特定资源词典所处位置
|
||||
//(未在页面中找到资源时使用,
|
||||
//或应用程序资源字典中找到时使用)
|
||||
ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
|
||||
//(未在页面中找到资源时使用,
|
||||
//、应用程序或任何主题专用资源字典中找到时使用)
|
||||
)]
|
||||
|
||||
|
||||
// 程序集的版本信息由下列四个值组成:
|
||||
//
|
||||
// 主版本
|
||||
// 次版本
|
||||
// 生成号
|
||||
// 修订号
|
||||
//
|
||||
// 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号
|
||||
// 方法是按如下所示使用“*”: :
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("0.0.3.0")]
|
||||
[assembly: AssemblyFileVersion("0.0.3.0")]
|
63
Properties/Resources.Designer.cs
generated
Normal file
63
Properties/Resources.Designer.cs
generated
Normal file
@ -0,0 +1,63 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace AnotherReplayReader.Properties {
|
||||
using System;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 一个强类型的资源类,用于查找本地化的字符串等。
|
||||
/// </summary>
|
||||
// 此类是由 StronglyTypedResourceBuilder
|
||||
// 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
|
||||
// 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
|
||||
// (以 /str 作为命令选项),或重新生成 VS 项目。
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")]
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
internal class Resources {
|
||||
|
||||
private static global::System.Resources.ResourceManager resourceMan;
|
||||
|
||||
private static global::System.Globalization.CultureInfo resourceCulture;
|
||||
|
||||
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
|
||||
internal Resources() {
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 返回此类使用的缓存的 ResourceManager 实例。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Resources.ResourceManager ResourceManager {
|
||||
get {
|
||||
if (object.ReferenceEquals(resourceMan, null)) {
|
||||
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AnotherReplayReader.Properties.Resources", typeof(Resources).Assembly);
|
||||
resourceMan = temp;
|
||||
}
|
||||
return resourceMan;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 重写当前线程的 CurrentUICulture 属性
|
||||
/// 重写当前线程的 CurrentUICulture 属性。
|
||||
/// </summary>
|
||||
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
|
||||
internal static global::System.Globalization.CultureInfo Culture {
|
||||
get {
|
||||
return resourceCulture;
|
||||
}
|
||||
set {
|
||||
resourceCulture = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
117
Properties/Resources.resx
Normal file
117
Properties/Resources.resx
Normal file
@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
<xsd:complexType>
|
||||
<xsd:choice maxOccurs="unbounded">
|
||||
<xsd:element name="metadata">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
<xsd:attribute name="type" type="xsd:string" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="assembly">
|
||||
<xsd:complexType>
|
||||
<xsd:attribute name="alias" type="xsd:string" />
|
||||
<xsd:attribute name="name" type="xsd:string" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="data">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" msdata:Ordinal="1" />
|
||||
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
|
||||
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
<xsd:element name="resheader">
|
||||
<xsd:complexType>
|
||||
<xsd:sequence>
|
||||
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
|
||||
</xsd:sequence>
|
||||
<xsd:attribute name="name" type="xsd:string" use="required" />
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:choice>
|
||||
</xsd:complexType>
|
||||
</xsd:element>
|
||||
</xsd:schema>
|
||||
<resheader name="resmimetype">
|
||||
<value>text/microsoft-resx</value>
|
||||
</resheader>
|
||||
<resheader name="version">
|
||||
<value>2.0</value>
|
||||
</resheader>
|
||||
<resheader name="reader">
|
||||
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
</root>
|
26
Properties/Settings.Designer.cs
generated
Normal file
26
Properties/Settings.Designer.cs
generated
Normal file
@ -0,0 +1,26 @@
|
||||
//------------------------------------------------------------------------------
|
||||
// <auto-generated>
|
||||
// 此代码由工具生成。
|
||||
// 运行时版本:4.0.30319.42000
|
||||
//
|
||||
// 对此文件的更改可能会导致不正确的行为,并且如果
|
||||
// 重新生成代码,这些更改将会丢失。
|
||||
// </auto-generated>
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
namespace AnotherReplayReader.Properties {
|
||||
|
||||
|
||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.9.0.0")]
|
||||
internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase {
|
||||
|
||||
private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
|
||||
|
||||
public static Settings Default {
|
||||
get {
|
||||
return defaultInstance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
7
Properties/Settings.settings
Normal file
7
Properties/Settings.settings
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<SettingsFile xmlns="uri:settings" CurrentProfile="(Default)">
|
||||
<Profiles>
|
||||
<Profile Name="(Default)" />
|
||||
</Profiles>
|
||||
<Settings />
|
||||
</SettingsFile>
|
431
Replay.cs
Normal file
431
Replay.cs
Normal file
@ -0,0 +1,431 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
23
Window1.xaml
Normal file
23
Window1.xaml
Normal file
@ -0,0 +1,23 @@
|
||||
<Window x:Class="AnotherReplayReader.Window1"
|
||||
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="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" />
|
||||
<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" />
|
||||
<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.Columns>
|
||||
<DataGridTextColumn Header="IP" Binding="{Binding Path=IPString}"/>
|
||||
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=ID}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Window>
|
128
Window1.xaml.cs
Normal file
128
Window1.xaml.cs
Normal file
@ -0,0 +1,128 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
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
|
||||
{
|
||||
/// <summary>
|
||||
/// Window1.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
internal partial class Window1 : Window
|
||||
{
|
||||
private PlayerIdentity _identity;
|
||||
|
||||
public Window1(PlayerIdentity identity)
|
||||
{
|
||||
InitializeComponent();
|
||||
_identity = identity;
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private async void Refresh()
|
||||
{
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_dataGrid.Items.Clear();
|
||||
_dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
|
||||
});
|
||||
|
||||
await _identity.Fetch();
|
||||
|
||||
Display();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"无法加载IP表:{e}"));
|
||||
}
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = true);
|
||||
}
|
||||
|
||||
private void Display(string filter = "")
|
||||
{
|
||||
var newList = _identity.AsSortedList().Where(x => x.IPString.StartsWith(filter));
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_dataGrid.Items.Clear();
|
||||
foreach (var item in newList)
|
||||
{
|
||||
_dataGrid.Items.Add(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async void OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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 response = reader.ReadToEnd();
|
||||
var serializer = new JavaScriptSerializer();
|
||||
var result = serializer.Deserialize<bool>(response);
|
||||
if(!result)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
await Task.Run(() =>
|
||||
{
|
||||
var fieldText = Dispatcher.Invoke(() => _ipField.Text);
|
||||
Display(fieldText);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
3
app.config
Normal file
3
app.config
Normal file
@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1"/></startup></configuration>
|
95
autosave.txt
Normal file
95
autosave.txt
Normal file
@ -0,0 +1,95 @@
|
||||
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);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user