Add project files.

This commit is contained in:
lanyi 2021-04-23 00:56:08 +02:00
parent 97cf4333ea
commit 1c19badeb5
31 changed files with 3612 additions and 0 deletions

63
APM.xaml Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

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

121
ILMergeConfig.json Normal file
View 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
View 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
View 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
View 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
View 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
View 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);
}
}
}

View 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
View 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
View 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
View 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;
}
}
}
}

View 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
View 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
View 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
View 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
View 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
View 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);
}
}