改了一亿个东西,修了一亿个 BUG
This commit is contained in:
parent
5b907309e0
commit
888ddce4ef
17
About.xaml
17
About.xaml
@ -14,20 +14,21 @@
|
|||||||
<RowDefinition Height="50"/>
|
<RowDefinition Height="50"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBox x:Name="_idBox" HorizontalAlignment="Left" Margin="54,23,0,10" TextWrapping="Wrap" Text="TextBox" Width="300" BorderBrush="White" RenderTransformOrigin="0.497,0.438" Grid.Row="1"/>
|
<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">
|
<TextBlock x:Name="textBlock" Margin="36,10,36,32" TextWrapping="Wrap" Grid.RowSpan="2">
|
||||||
<Run Text="【自动录像机0.6】"/><LineBreak/>
|
<Run Text="【自动录像机0.7】"/><LineBreak/>
|
||||||
<Run Text="本工具目前额外支持以下mod的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
<Run Text="本工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
||||||
<Run Text="当mod增加新的阵营时,会出现未知阵营和阵营错乱现象"/><LineBreak/>
|
<Run Text="当 Mod 增加新的阵营时,会出现未知阵营和阵营错乱现象"/><LineBreak/>
|
||||||
<Run Text="有任何问题可以先去找苏醒或节操"/><LineBreak/>
|
<Run Text="有任何问题可以先去找苏醒或节操"/><LineBreak/>
|
||||||
<Run Text="解析录像的代码主要来源于 louisdx 的研究:"/><LineBreak/>
|
<Run Text="解析录像的代码主要来源于 louisdx 的研究:"/><LineBreak/>
|
||||||
<Hyperlink NavigateUri="https://github.com/louisdx/cnc-replayreaders"><Run Text="https://github.com/louisdx/cnc-replayreaders"/></Hyperlink><LineBreak/>
|
<Hyperlink NavigateUri="https://github.com/louisdx/cnc-replayreaders"><Run Text="https://github.com/louisdx/cnc-replayreaders"/></Hyperlink><LineBreak/>
|
||||||
<Run Text="解析Big的代码来源于OpenSage:"/><LineBreak/>
|
<Run Text="解析 Big 的代码来源于" />
|
||||||
<Hyperlink NavigateUri="https://github.com/OpenSAGE/OpenSAGE"><Run Text="https://github.com/OpenSAGE/OpenSAGE"/></Hyperlink><LineBreak/>
|
<Hyperlink NavigateUri="https://github.com/Qibbi"><Run Text="Jana Mohn" /></Hyperlink>
|
||||||
|
<Run Text="的 TechnologyAssembler" /><LineBreak />
|
||||||
<Run Text="解析 Tga 的代码来源于Pfim:"/><LineBreak/>
|
<Run Text="解析 Tga 的代码来源于Pfim:"/><LineBreak/>
|
||||||
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
|
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
|
||||||
<Run Text="RA3吧:"/><LineBreak/>
|
<Run Text="欢迎来到红警3吧:"/><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/>
|
<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>
|
<Run Text="ARMod 群号:161660710"/></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>
|
<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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
@ -1,17 +1,5 @@
|
|||||||
using System;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Windows;
|
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
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
<PublishUrl>publish\</PublishUrl>
|
<PublishUrl>publish\</PublishUrl>
|
||||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
|
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||||
@ -20,19 +21,34 @@
|
|||||||
<Page Remove="publish\**" />
|
<Page Remove="publish\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Reference Include="System.Web" />
|
<None Remove="ReplayAutoSaver.cs~RF188cd1d1.TMP" />
|
||||||
<Reference Include="System.Web.Extensions" />
|
|
||||||
<Reference Include="System.Xaml" />
|
|
||||||
<Reference Include="WindowsBase" />
|
|
||||||
<Reference Include="PresentationCore" />
|
|
||||||
<Reference Include="PresentationFramework" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ILMerge" Version="3.0.29" />
|
<Reference Include="System.Web" />
|
||||||
|
<Reference Include="TechnologyAssembler.Core">
|
||||||
|
<HintPath>TechnologyAssembler.Core.dll</HintPath>
|
||||||
|
<Private>true</Private>
|
||||||
|
</Reference>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="ILMerge" Version="3.0.41" />
|
||||||
<PackageReference Include="ILMerge.MSBuild.Task" Version="1.0.7" />
|
<PackageReference Include="ILMerge.MSBuild.Task" Version="1.0.7" />
|
||||||
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
|
<PackageReference Include="NPinyin.Core" Version="3.0.0" />
|
||||||
<PackageReference Include="OpenSage.FileFormats.Big" Version="1.0.0" />
|
<PackageReference Include="Pfim" Version="0.10.1" />
|
||||||
<PackageReference Include="Pfim" Version="0.7.0" />
|
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
|
||||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
<PackageReference Include="System.Text.Json" Version="5.0.2" />
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Properties\Settings.Designer.cs">
|
||||||
|
<DesignTimeSharedInput>True</DesignTimeSharedInput>
|
||||||
|
<AutoGen>True</AutoGen>
|
||||||
|
<DependentUpon>Settings.settings</DependentUpon>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="Properties\Settings.settings">
|
||||||
|
<Generator>SettingsSingleFileGenerator</Generator>
|
||||||
|
<LastGenOutput>Settings.Designer.cs</LastGenOutput>
|
||||||
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
@ -1,4 +1,4 @@
|
|||||||
<Window x:Class="AnotherReplayReader.APM"
|
<Window x:Class="AnotherReplayReader.ApmWindow"
|
||||||
x:ClassModifier="internal"
|
x:ClassModifier="internal"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
@ -21,7 +21,7 @@
|
|||||||
Margin="0,19,22,0"
|
Margin="0,19,22,0"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Padding="5,2"
|
Padding="5,2"
|
||||||
Visibility="Visible"
|
Visibility="Hidden"
|
||||||
Click="OnSetPlayerButtonClick"
|
Click="OnSetPlayerButtonClick"
|
||||||
HorizontalAlignment="Right"/>
|
HorizontalAlignment="Right"/>
|
||||||
<DataGrid x:Name="_table"
|
<DataGrid x:Name="_table"
|
@ -1,8 +1,10 @@
|
|||||||
using System;
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -30,9 +32,8 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
return Value.CompareTo(other.Value);
|
return Value.CompareTo(other.Value);
|
||||||
}
|
}
|
||||||
return NumberValue.Value.CompareTo(other.NumberValue.Value);
|
return NumberValue.Value.CompareTo(other.NumberValue!.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
return NumberValue.HasValue ? 1 : -1;
|
return NumberValue.HasValue ? 1 : -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -68,11 +69,8 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
_values = values.Select(x => new DataValue(x)).ToArray();
|
_values = values.Select(x => new DataValue(x)).ToArray();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
internal static class DataTableFactory
|
public static List<DataRow> GetList(Replay replay, PlayerIdentity identity)
|
||||||
{
|
|
||||||
static public List<DataRow> Get(Replay replay, PlayerIdentity identity)
|
|
||||||
{
|
{
|
||||||
var list = new List<byte>
|
var list = new List<byte>
|
||||||
{
|
{
|
||||||
@ -136,7 +134,7 @@ namespace AnotherReplayReader
|
|||||||
};
|
};
|
||||||
if (replay.Type == ReplayType.Lan && identity.IsUsable)
|
if (replay.Type == ReplayType.Lan && identity.IsUsable)
|
||||||
{
|
{
|
||||||
dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIP))));
|
dataList.Add(new DataRow("局域网IP", replay.Players.Select(x => identity.QueryRealNameAndIP(x.PlayerIp))));
|
||||||
}
|
}
|
||||||
|
|
||||||
var commandCounts = replay.GetCommandCounts();
|
var commandCounts = replay.GetCommandCounts();
|
||||||
@ -155,43 +153,37 @@ namespace AnotherReplayReader
|
|||||||
return dataList;
|
return dataList;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// APM.xaml 的交互逻辑
|
/// APM.xaml 的交互逻辑
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal partial class APM : Window
|
internal partial class ApmWindow : Window
|
||||||
{
|
{
|
||||||
private PlayerIdentity _identity;
|
private readonly PlayerIdentity _identity;
|
||||||
|
|
||||||
public APM(Replay replay, PlayerIdentity identity)
|
public ApmWindow(Replay replay, PlayerIdentity identity)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
_identity = identity;
|
_identity = identity;
|
||||||
|
InitializeComponent();
|
||||||
|
InitializeApmWindowData(replay);
|
||||||
|
}
|
||||||
|
|
||||||
if (_identity.IsUsable)
|
private async void InitializeApmWindowData(Replay replay)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
if (_identity.IsUsable)
|
||||||
{
|
{
|
||||||
_setPlayerButton.IsEnabled = true;
|
_setPlayerButton.IsEnabled = true;
|
||||||
_setPlayerButton.Visibility = Visibility.Visible;
|
_setPlayerButton.Visibility = Visibility.Visible;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
{
|
||||||
_setPlayerButton.IsEnabled = false;
|
_setPlayerButton.IsEnabled = false;
|
||||||
_setPlayerButton.Visibility = Visibility.Hidden;
|
_setPlayerButton.Visibility = Visibility.Hidden;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Task.Run(() =>
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dataList = DataTableFactory.Get(replay, _identity);
|
var dataList = await Task.Run(() => DataRow.GetList(replay, _identity));
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_table.Items.Clear();
|
_table.Items.Clear();
|
||||||
foreach (var row in dataList)
|
foreach (var row in dataList)
|
||||||
{
|
{
|
||||||
@ -204,16 +196,11 @@ namespace AnotherReplayReader
|
|||||||
_table.Columns[4].Header = dataList[0].Player4Value;
|
_table.Columns[4].Header = dataList[0].Player4Value;
|
||||||
_table.Columns[5].Header = dataList[0].Player5Value;
|
_table.Columns[5].Header = dataList[0].Player5Value;
|
||||||
_table.Columns[6].Header = dataList[0].Player6Value;
|
_table.Columns[6].Header = dataList[0].Player6Value;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
{
|
||||||
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
|
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
|
||||||
@ -222,7 +209,7 @@ namespace AnotherReplayReader
|
|||||||
window1.ShowDialog();
|
window1.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||||
{
|
{
|
||||||
_table.SelectAll();
|
_table.SelectAll();
|
||||||
}
|
}
|
3
App.xaml
3
App.xaml
@ -2,7 +2,8 @@
|
|||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
StartupUri="MainWindow.xaml">
|
StartupUri="MainWindow.xaml"
|
||||||
|
ShutdownMode="OnMainWindowClose">
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
25
App.xaml.cs
25
App.xaml.cs
@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Threading;
|
||||||
using System.Configuration;
|
|
||||||
using System.Data;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
@ -12,5 +9,25 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
private int _isInException = 0;
|
||||||
|
public App()
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) =>
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref _isInException) > 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
const string message = "哎呀呀,出现了一些无法处理的问题,只能退出了。要不要尝试保存一下日志文件呢?";
|
||||||
|
var choice = MessageBox.Show($"{message}\r\n{eventArgs.ExceptionObject}", "自动录像机", MessageBoxButton.YesNo);
|
||||||
|
if (choice == MessageBoxResult.Yes)
|
||||||
|
{
|
||||||
|
Debug.Instance.RequestSave();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
29
Auth.cs
29
Auth.cs
@ -1,37 +1,33 @@
|
|||||||
using System;
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using Microsoft.Win32;
|
using System.Text;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal static class Auth
|
internal static class Auth
|
||||||
{
|
{
|
||||||
public static string ID { get; private set; }
|
public static string? ID { get; }
|
||||||
|
|
||||||
static Auth()
|
static Auth()
|
||||||
{
|
{
|
||||||
ID = null;
|
ID = null;
|
||||||
|
|
||||||
var windowsID = null as string;
|
string? windowsID;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using (var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
|
using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
||||||
using (var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false))
|
using var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false);
|
||||||
{
|
|
||||||
windowsID = winNt?.GetValue("ProductId") as string;
|
windowsID = winNt?.GetValue("ProductId") as string;
|
||||||
}
|
}
|
||||||
}
|
catch
|
||||||
catch(Exception)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var randomKey = null as string;
|
string? randomKey;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var folderPath = Cache.CacheDirectory;
|
var folderPath = Cache.CacheDirectory;
|
||||||
@ -48,7 +44,7 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
randomKey = File.ReadAllText(keyPath);
|
randomKey = File.ReadAllText(keyPath);
|
||||||
}
|
}
|
||||||
catch(Exception)
|
catch
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -58,13 +54,10 @@ namespace AnotherReplayReader
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using var sha = SHA256.Create();
|
||||||
using (var sha = SHA256.Create())
|
|
||||||
{
|
|
||||||
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
|
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
|
||||||
ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
|
ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public static string GetKey()
|
public static string GetKey()
|
||||||
{
|
{
|
||||||
|
@ -1,172 +1,35 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
using System.Collections.Generic;
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Win32;
|
using TechnologyAssembler.Core.IO;
|
||||||
using OpenSage.FileFormats.Big;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal sealed class BigMinimapCache
|
internal sealed class BigMinimapCache
|
||||||
{
|
{
|
||||||
private sealed class CacheAdapter
|
private readonly object _lock = new();
|
||||||
{
|
private SkuDefFileSystemProvider? _skudefFileSystem = null;
|
||||||
public List<string> Bigs { get; set; }
|
|
||||||
public Dictionary<string, string> MapsToBigs { get; set; }
|
|
||||||
|
|
||||||
public CacheAdapter()
|
public BigMinimapCache(string? ra3Directory)
|
||||||
{
|
{
|
||||||
Bigs = new List<string>();
|
Task.Run(() => Initialize(ra3Directory));
|
||||||
MapsToBigs = new Dictionary<string, string>();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//private Cache _cache;
|
public bool TryGetEntry(string path, out Stream? bigEntry)
|
||||||
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = null;
|
{
|
||||||
|
bigEntry = null;
|
||||||
|
|
||||||
public BigMinimapCache(Cache cache, string ra3Directory)
|
using var locker = new Lock(_lock);
|
||||||
{
|
if (_skudefFileSystem is not { } fs)
|
||||||
//_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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_mapsToBigs.ContainsKey(path))
|
if (!fs.FileExists(path))
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
||||||
return false;
|
return false;
|
||||||
@ -174,58 +37,75 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bigPath = _mapsToBigs[path];
|
bigEntry = fs.OpenStream(path, VirtualFileModeType.Open);
|
||||||
big = new BigArchive(bigPath);
|
return true;
|
||||||
if(big.GetEntry(path) == null)
|
|
||||||
{
|
|
||||||
//_cache.Remove("bigsCache");
|
|
||||||
big.Dispose();
|
|
||||||
big = null;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
|
||||||
big = null;
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
private void Initialize(string? ra3Directory)
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] TryReadBytesFromBig(string path)
|
|
||||||
{
|
{
|
||||||
if(_mapsToBigs == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!_mapsToBigs.ContainsKey(path))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var bigPath = _mapsToBigs[path];
|
if (ra3Directory is null || !Directory.Exists(ra3Directory))
|
||||||
using (var big = new BigArchive(path))
|
|
||||||
{
|
{
|
||||||
var entry = big.GetEntry(path);
|
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
|
||||||
using (var stream = entry.Open())
|
return;
|
||||||
using (var reader = new BinaryReader(stream))
|
}
|
||||||
|
|
||||||
|
var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
|
||||||
|
var currentLanguage_ = $"{currentLanguage}_";
|
||||||
|
double SkudefVersionSelector(string fullPath)
|
||||||
{
|
{
|
||||||
return reader.ReadBytes((int)entry.Length);
|
var value = -1.0;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const string skudefPrefix = "RA3";
|
||||||
|
const int prefix = 1;
|
||||||
|
const int language = 2;
|
||||||
|
const int majorVersion = 3;
|
||||||
|
const int minorVersion = 4;
|
||||||
|
const int majorVersionMultiplier = 10000;
|
||||||
|
const int correctLanguageBonus = 1000_0000;
|
||||||
|
|
||||||
|
value = 0;
|
||||||
|
var stem = Path.GetFileNameWithoutExtension(fullPath);
|
||||||
|
var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
|
||||||
|
if (!match.Success || match.Groups.Cast<Group>().Any(g => !g.Success))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0;
|
||||||
|
value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0;
|
||||||
|
value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier;
|
||||||
|
value += int.Parse(match.Groups[minorVersion].Value);
|
||||||
|
return value;
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n";
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef")
|
||||||
|
orderby SkudefVersionSelector(p) descending
|
||||||
|
select p).First();
|
||||||
|
Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n";
|
||||||
|
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
DronePlatform.BuildTechnologyAssembler();
|
||||||
|
_skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
||||||
//_cache.Remove("bigsCache");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
50
Cache.cs
50
Cache.cs
@ -2,11 +2,8 @@
|
|||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -15,9 +12,11 @@ namespace AnotherReplayReader
|
|||||||
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
||||||
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
||||||
|
|
||||||
private ConcurrentDictionary<string, string> _storage;
|
private readonly ConcurrentDictionary<string, string> _storage = new();
|
||||||
|
|
||||||
public Cache()
|
public Cache()
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -26,13 +25,19 @@ namespace AnotherReplayReader
|
|||||||
Directory.CreateDirectory(CacheDirectory);
|
Directory.CreateDirectory(CacheDirectory);
|
||||||
}
|
}
|
||||||
|
|
||||||
var serializer = new JavaScriptSerializer();
|
using var cacheStream = File.OpenRead(CacheFilePath);
|
||||||
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
|
var futureCache = DeserializeAsync<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
|
||||||
}
|
if (await futureCache is not { } cached)
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
_storage = new ConcurrentDictionary<string, string>();
|
return;
|
||||||
}
|
}
|
||||||
|
foreach (var kv in cached)
|
||||||
|
{
|
||||||
|
_storage.TryAdd(kv.Key, kv.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetOrDefault<T>(string key, in T defaultValue)
|
public T GetOrDefault<T>(string key, in T defaultValue)
|
||||||
@ -41,8 +46,7 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
if (_storage.TryGetValue(key, out var valueString))
|
if (_storage.TryGetValue(key, out var valueString))
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
return Deserialize<T>(valueString) ?? defaultValue;
|
||||||
return serializer.Deserialize<T>(valueString);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@ -53,8 +57,20 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
_storage[key] = Serialize(value);
|
||||||
_storage[key] = serializer.Serialize(value);
|
Save();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetValues(params (string Key, object Value)[] values)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var (key, value) in values)
|
||||||
|
{
|
||||||
|
_storage[key] = Serialize(value);
|
||||||
|
}
|
||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
@ -66,12 +82,12 @@ namespace AnotherReplayReader
|
|||||||
Save();
|
Save();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
public async void Save()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var serializer = new JavaScriptSerializer();
|
using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write);
|
||||||
File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
|
await SerializeAsync(cacheStream, _storage).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
using Microsoft.Win32;
|
using AnotherReplayReader.Utils;
|
||||||
|
using Microsoft.Win32;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
@ -21,11 +23,33 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly List<string> _list = new();
|
||||||
|
|
||||||
public event Action<string>? NewText;
|
public event Action<string>? NewText;
|
||||||
public Proxy DebugMessage
|
public Proxy DebugMessage
|
||||||
{
|
{
|
||||||
get => new Proxy();
|
get => new();
|
||||||
set => NewText?.Invoke(value.Payload);
|
set
|
||||||
|
{
|
||||||
|
var text = value.Payload;
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
if (NewText is null || _list.Count > 0)
|
||||||
|
{
|
||||||
|
_list.Add(text);
|
||||||
|
if (NewText is not null)
|
||||||
|
{
|
||||||
|
text = string.Join(string.Empty, _list);
|
||||||
|
_list.Clear();
|
||||||
|
_list.Capacity = 0;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NewText(text);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public event Action? RequestedSave;
|
public event Action? RequestedSave;
|
||||||
@ -36,18 +60,31 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed partial class Debug : Window
|
public sealed partial class Debug : Window
|
||||||
{
|
{
|
||||||
public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
|
public static readonly DebugMessageWrapper Instance = new();
|
||||||
|
private static readonly object _lock = new();
|
||||||
private static Debug? _window = null;
|
private static Debug? _window = null;
|
||||||
|
|
||||||
public static void Initialize()
|
public static void Initialize()
|
||||||
{
|
{
|
||||||
Interlocked.CompareExchange(ref _window, new(), null);
|
using var locker = new Lock(_lock);
|
||||||
_window.InitializeComponent();
|
if (_window is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var window = new Debug();
|
||||||
|
window.InitializeComponent();
|
||||||
|
_window = window;
|
||||||
Instance.NewText += _window.AppendText;
|
Instance.NewText += _window.AppendText;
|
||||||
Instance.RequestedSave += _window.ExportInMainWindow;
|
Instance.RequestedSave += _window.ExportInMainWindow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static new void ShowDialog() => (_window as Window)?.ShowDialog();
|
public static new void ShowDialog() => Lock.Run(_lock, () => (_window as Window)?.ShowDialog());
|
||||||
|
|
||||||
|
protected override void OnClosing(CancelEventArgs e)
|
||||||
|
{
|
||||||
|
e.Cancel = true;
|
||||||
|
Hide();
|
||||||
|
}
|
||||||
|
|
||||||
private Debug() { }
|
private Debug() { }
|
||||||
|
|
||||||
@ -55,7 +92,7 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
private void OnClear_Click(object sender, RoutedEventArgs e) => _textBox.Clear();
|
private void OnClear_Click(object sender, RoutedEventArgs e) => _textBox.Clear();
|
||||||
|
|
||||||
private void AppendText(string s) => Dispatcher.Invoke(() => _textBox.AppendText(s));
|
private void AppendText(string s) => Dispatcher.InvokeAsync(() => _textBox.AppendText(s));
|
||||||
|
|
||||||
private void ExportInMainWindow() => Export(Application.Current.MainWindow);
|
private void ExportInMainWindow() => Export(Application.Current.MainWindow);
|
||||||
|
|
||||||
@ -72,12 +109,10 @@ namespace AnotherReplayReader
|
|||||||
var result = saveFileDialog.ShowDialog(owner);
|
var result = saveFileDialog.ShowDialog(owner);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
using (var file = saveFileDialog.OpenFile())
|
using var file = saveFileDialog.OpenFile();
|
||||||
using (var writer = new StreamWriter(file))
|
using var writer = new StreamWriter(file);
|
||||||
{
|
|
||||||
writer.Write(_textBox.Text);
|
writer.Write(_textBox.Text);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
25
DronePlatform.cs
Normal file
25
DronePlatform.cs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using TechnologyAssembler;
|
||||||
|
using TechnologyAssembler.Core.Diagnostics;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader
|
||||||
|
{
|
||||||
|
public static class DronePlatform
|
||||||
|
{
|
||||||
|
private static readonly object _lock = new();
|
||||||
|
private static bool _built = false;
|
||||||
|
|
||||||
|
public static void BuildTechnologyAssembler()
|
||||||
|
{
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
if (_built)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new TechnologyAssemblerCoreModule().Initialize();
|
||||||
|
Tracer.SetTraceLevel(7);
|
||||||
|
Tracer.TraceWrite += (source, type, message) => Debug.Instance.DebugMessage += $"[{source}][{type}] {message}\r\n";
|
||||||
|
_built = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +0,0 @@
|
|||||||
Veldrid\.SDL2\.dll
|
|
@ -1,121 +1,36 @@
|
|||||||
{
|
{
|
||||||
"General": {
|
"General": {
|
||||||
|
"OutputFile": null,
|
||||||
|
"TargetPlatform": null,
|
||||||
|
"KeyFile": null,
|
||||||
|
"AlternativeILMergePath": null,
|
||||||
"InputAssemblies": [
|
"InputAssemblies": [
|
||||||
"$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll",
|
"NPinyin.Core.dll",
|
||||||
"$(TargetDir)Microsoft.Extensions.DependencyModel.dll",
|
"Pfim.dll",
|
||||||
"$(TargetDir)Microsoft.Win32.Primitives.dll",
|
"TechnologyAssembler.Core.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": {
|
"Advanced": {
|
||||||
"AllowWildCards": true
|
"AllowDuplicateType": null,
|
||||||
|
"AllowMultipleAssemblyLevelAttributes": false,
|
||||||
|
"AllowWildCards": false,
|
||||||
|
"AllowZeroPeKind": false,
|
||||||
|
"AttributeFile": null,
|
||||||
|
"Closed": false,
|
||||||
|
"CopyAttributes": false,
|
||||||
|
"DebugInfo": true,
|
||||||
|
"DelaySign": false,
|
||||||
|
"DeleteCopiesOverwriteTarget": false,
|
||||||
|
"ExcludeFile": "",
|
||||||
|
"FileAlignment": 512,
|
||||||
|
"Internalize": false,
|
||||||
|
"Log": false,
|
||||||
|
"LogFile": null,
|
||||||
|
"PublicKeyTokens": true,
|
||||||
|
"SearchDirectories": [],
|
||||||
|
"TargetKind": null,
|
||||||
|
"UnionMerge": false,
|
||||||
|
"Version": null,
|
||||||
|
"XmlDocumentation": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,7 +5,8 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="MainWindow" Width="800" Height="600">
|
Title="MainWindow" Width="800" Height="600"
|
||||||
|
Loaded="OnMainWindowLoaded">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="20"/>
|
<RowDefinition Height="20"/>
|
||||||
@ -71,7 +72,8 @@
|
|||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="文件名" Binding="{Binding Path=FileName}" Width="4*" IsReadOnly="True"/>
|
<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=NumberOfPlayingPlayers}" Width="1.5*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="录像时长" Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="录像时长"
|
||||||
|
Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
||||||
<DataGridTextColumn Header="Mod" Binding="{Binding Path=Mod}" Width="1.5*" IsReadOnly="True"/>
|
<DataGridTextColumn Header="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" />
|
<DataGridTextColumn Header="录像日期" Binding="{Binding Path=Date, StringFormat='{}{0:yyyy/MM/dd HH:mm:SS}'}" Width="3*" IsReadOnly="True" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
using Microsoft.Win32;
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using Microsoft.Win32;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Immutable;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@ -17,28 +20,14 @@ namespace AnotherReplayReader
|
|||||||
{
|
{
|
||||||
public MainWindowProperties()
|
public MainWindowProperties()
|
||||||
{
|
{
|
||||||
string userDataLeafName = null;
|
const RegistryHive hklm = RegistryHive.LocalMachine;
|
||||||
string replayFolderName = null;
|
RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
|
||||||
try
|
string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
|
||||||
{
|
string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"获取注册表项时出现错误:{e}\r\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
||||||
{
|
{
|
||||||
userDataLeafName = "Red Alert 3";
|
userDataLeafName = "Red Alert 3";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(replayFolderName))
|
if (string.IsNullOrWhiteSpace(replayFolderName))
|
||||||
{
|
{
|
||||||
replayFolderName = "Replays";
|
replayFolderName = "Replays";
|
||||||
@ -50,23 +39,21 @@ namespace AnotherReplayReader
|
|||||||
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
||||||
}
|
}
|
||||||
|
|
||||||
public event PropertyChangedEventHandler PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
// This method is called by the Set accessor of each property.
|
// This method is called by the Set accessor of each property.
|
||||||
// The CallerMemberName attribute that is applied to the optional propertyName
|
// The CallerMemberName attribute that is applied to the optional propertyName
|
||||||
// parameter causes the property name of the caller to be substituted as an argument.
|
// parameter causes the property name of the caller to be substituted as an argument.
|
||||||
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
|
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
|
||||||
{
|
{
|
||||||
var equals = Equals(target, newValue);
|
if (!Equals(target, newValue))
|
||||||
target = newValue;
|
|
||||||
if (!equals)
|
|
||||||
{
|
{
|
||||||
|
target = newValue;
|
||||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string RA3Directory { get; }
|
public string? RA3Directory { get; }
|
||||||
public string RA3ReplayFolderPath { get; }
|
public string RA3ReplayFolderPath { get; }
|
||||||
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
||||||
public string CustomMapsDirectory { get; }
|
public string CustomMapsDirectory { get; }
|
||||||
@ -78,19 +65,19 @@ namespace AnotherReplayReader
|
|||||||
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
|
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReplayDetails
|
public string? ReplayDetails
|
||||||
{
|
{
|
||||||
get => _replayDetails;
|
get => _replayDetails;
|
||||||
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
|
set => SetAndNotifyPropertyChanged(ref _replayDetails, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool ReplaySelected => _currentReplay != null;
|
public bool ReplaySelected => CurrentReplay != null;
|
||||||
|
|
||||||
public bool ReplayPlayable => _currentReplay?.HasFooter == true && _currentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
|
public bool ReplayPlayable => CurrentReplay?.HasFooter == true && CurrentReplay?.HasCommentator == true && (RA3Directory != null) && File.Exists(RA3Exe);
|
||||||
|
|
||||||
public bool ReplayDamaged => _currentReplay?.HasFooter == false;
|
public bool ReplayDamaged => CurrentReplay?.HasFooter == false;
|
||||||
|
|
||||||
public Replay CurrentReplay
|
public Replay? CurrentReplay
|
||||||
{
|
{
|
||||||
get => _currentReplay;
|
get => _currentReplay;
|
||||||
set
|
set
|
||||||
@ -103,9 +90,9 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _replayFolderPath;
|
private string _replayFolderPath = null!;
|
||||||
private string _replayDetails;
|
private string? _replayDetails;
|
||||||
private Replay _currentReplay;
|
private Replay? _currentReplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -113,346 +100,236 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class MainWindow : Window
|
public partial class MainWindow : Window
|
||||||
{
|
{
|
||||||
private readonly MainWindowProperties _properties = new MainWindowProperties();
|
private readonly TaskQueue _taskQueue;
|
||||||
private readonly Cache _cache = new Cache();
|
private readonly MainWindowProperties _properties = new();
|
||||||
|
private readonly Cache _cache = new();
|
||||||
private readonly PlayerIdentity _playerIdentity;
|
private readonly PlayerIdentity _playerIdentity;
|
||||||
private readonly BigMinimapCache _minimapCache;
|
private readonly BigMinimapCache _minimapCache;
|
||||||
private readonly MinimapReader _minimapReader;
|
private readonly MinimapReader _minimapReader;
|
||||||
private List<Replay> _replayList = new List<Replay>();
|
private readonly CancelManager _cancelLoadReplays = new();
|
||||||
private List<PinyinReplayData> _pinyinList = new List<PinyinReplayData>();
|
private readonly CancelManager _cancelFilterReplays = new();
|
||||||
private CancellationTokenSource _loadReplaysToken;
|
private readonly CancelManager _cancelDisplayReplays = new();
|
||||||
|
private ReplayPinyinList _replayList;
|
||||||
|
private ImmutableArray<string> _filterStrings = ImmutableArray<string>.Empty;
|
||||||
|
private ImmutableArray<Replay> _filteredReplays = ImmutableArray<Replay>.Empty;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
DataContext = _properties;
|
_taskQueue = new(Dispatcher);
|
||||||
|
_playerIdentity = new PlayerIdentity(_cache);
|
||||||
|
_minimapCache = new BigMinimapCache(_properties.RA3Directory);
|
||||||
|
_minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
||||||
|
_replayList = new(_playerIdentity);
|
||||||
|
|
||||||
var handling = new bool[1] { false };
|
DataContext = _properties;
|
||||||
Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
|
InitializeComponent();
|
||||||
|
Closing += (sender, eventArgs) =>
|
||||||
{
|
{
|
||||||
if (handling == null || handling[0])
|
_cache.Save();
|
||||||
|
Application.Current.Shutdown();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnMainWindowLoaded(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
Debug.Initialize();
|
||||||
|
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
|
||||||
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
if (!IsLoaded)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handling[0] = true;
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
|
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
|
||||||
};
|
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
|
||||||
|
|
||||||
_playerIdentity = new PlayerIdentity(_cache);
|
_properties.CurrentReplay = null;
|
||||||
_minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory);
|
|
||||||
_minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
|
||||||
InitializeComponent();
|
|
||||||
Closing += (sender, eventArgs) => _cache.Save();
|
|
||||||
|
|
||||||
LoadReplays();
|
|
||||||
Task.Run(() => AutoSaveReplays(Dispatcher, _properties.RA3ReplayFolderPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static async Task AutoSaveReplays(Dispatcher dispatcher, string replayFolderPath)
|
|
||||||
{
|
|
||||||
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(replayFolderPath, "*.RA3Replay")
|
|
||||||
let info = new FileInfo(fileName)
|
|
||||||
where !info.Name.StartsWith(ourPrefix)
|
|
||||||
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
|
|
||||||
select info).ToList();
|
|
||||||
|
|
||||||
foreach (var info in changed)
|
|
||||||
{
|
|
||||||
previousFiles[info.FullName] = info.LastWriteTimeUtc;
|
|
||||||
}
|
|
||||||
|
|
||||||
var replays = changed.Select(info =>
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
|
||||||
{
|
|
||||||
return new Replay(info.FullName, stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}).Where(replay => replay != null);
|
|
||||||
|
|
||||||
var newLastReplays = from replay in replays
|
|
||||||
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
|
|
||||||
let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero)
|
|
||||||
let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds)
|
|
||||||
where threshold < 40 || endThreshold < 40
|
|
||||||
select replay;
|
|
||||||
|
|
||||||
var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase);
|
|
||||||
foreach (var savedLastReplay in lastReplays.Keys)
|
|
||||||
{
|
|
||||||
if (!toBeChecked.ContainsKey(savedLastReplay))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
|
||||||
{
|
|
||||||
toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var kv in toBeChecked)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
|
|
||||||
var replay = kv.Value;
|
|
||||||
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
|
|
||||||
{
|
|
||||||
if (fileSize == replay.Size)
|
|
||||||
{
|
|
||||||
// skip if size is not changed
|
|
||||||
Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
|
|
||||||
lastReplays[kv.Key] = replay.Size;
|
|
||||||
|
|
||||||
var date = replay.Date;
|
|
||||||
|
|
||||||
var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
|
|
||||||
if (replay.NumberOfPlayingPlayers <= 2)
|
|
||||||
{
|
|
||||||
var playingPlayers = from player in replay.Players
|
|
||||||
let faction = ModData.GetFaction(replay.Mod, player.FactionID)
|
|
||||||
where faction.Kind != FactionKind.Observer
|
|
||||||
select $"{player.PlayerName}({faction.Name})";
|
|
||||||
playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
|
|
||||||
}
|
|
||||||
|
|
||||||
var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
|
|
||||||
var destinationPath = Path.Combine(replayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
File.Copy(replay.Path, destinationPath, true);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
|
|
||||||
Debug.Instance.DebugMessage += errorString;
|
|
||||||
if (Interlocked.Increment(ref errorMessageCount) == 1)
|
|
||||||
{
|
|
||||||
_ = dispatcher.InvokeAsync(() =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
MessageBox.Show(errorString);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
Interlocked.Decrement(ref errorMessageCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.Delay(10 * 1000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void LoadReplays(string nextSelected = null)
|
|
||||||
{
|
|
||||||
const string loadingString = "正在加载录像列表,请稍候… 已加载 {0} 个录像";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_loadReplaysToken?.Cancel();
|
|
||||||
}
|
|
||||||
catch (AggregateException e)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Cancellation failed: {e}";
|
|
||||||
}
|
|
||||||
_loadReplaysToken?.Dispose();
|
|
||||||
_loadReplaysToken = new CancellationTokenSource();
|
|
||||||
var cancelToken = _loadReplaysToken.Token;
|
|
||||||
var path = _properties.ReplayFolderPath;
|
|
||||||
|
|
||||||
if (_image != null)
|
|
||||||
{
|
|
||||||
_image.Source = null;
|
_image.Source = null;
|
||||||
}
|
if (_dataGrid.Items.Count > 0)
|
||||||
if (_dataGrid != null)
|
|
||||||
{
|
{
|
||||||
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||||
_dataGrid.Items.Refresh();
|
_dataGrid.Items.Refresh();
|
||||||
}
|
}
|
||||||
_replayList.Clear();
|
|
||||||
_pinyinList.Clear();
|
|
||||||
|
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_replayList = new(_playerIdentity);
|
||||||
|
|
||||||
|
var path = _properties.ReplayFolderPath;
|
||||||
if (!Directory.Exists(path))
|
if (!Directory.Exists(path))
|
||||||
{
|
{
|
||||||
DisplayReplays("这个文件夹并不存在。", nextSelected);
|
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (newList, newPinyinList) = await Task.Run(() =>
|
var result = await Task.Run(() =>
|
||||||
{
|
{
|
||||||
var list = new List<Replay>();
|
var list = new List<Replay>();
|
||||||
var pinyinList = new List<PinyinReplayData>();
|
var clock = new Stopwatch();
|
||||||
|
clock.Start();
|
||||||
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
|
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
|
||||||
{
|
{
|
||||||
if (cancelToken.IsCancellationRequested)
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var replay = new Replay(replayPath);
|
var replay = new Replay(replayPath);
|
||||||
list.Add(replay);
|
list.Add(replay);
|
||||||
pinyinList.Add(replay.ToPinyin(_playerIdentity));
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
_ = Dispatcher.Invoke(() => _properties.ReplayDetails = string.Format(loadingString, _replayList.Count));
|
if (clock.ElapsedMilliseconds > 300)
|
||||||
|
{
|
||||||
|
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
|
||||||
|
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
|
||||||
|
clock.Restart();
|
||||||
}
|
}
|
||||||
return (list, pinyinList);
|
}
|
||||||
});
|
return new ReplayPinyinList(list.ToImmutableArray(), _playerIdentity);
|
||||||
|
}, cancelToken);
|
||||||
_replayList = newList;
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
_pinyinList = newPinyinList;
|
_replayList = result;
|
||||||
DisplayReplays(string.Empty, nextSelected);
|
await FilterReplays(string.Empty, nextSelected, filterToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisplayReplays(string message = null, string nextSelected = null, Replay[] filtered = null)
|
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() =>
|
if (!IsLoaded)
|
||||||
{
|
{
|
||||||
filtered = filtered ?? _replayList.ToArray();
|
return;
|
||||||
|
}
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_cancelDisplayReplays.Reset(cancelToken);
|
||||||
|
_filteredReplays = _replayList.Replays;
|
||||||
|
|
||||||
_properties.CurrentReplay = null;
|
_properties.CurrentReplay = null;
|
||||||
_dataGrid.ItemsSource = filtered;
|
_dataGrid.SelectedItem = null;
|
||||||
|
_image.Source = null;
|
||||||
|
|
||||||
|
if (_filterStrings.Any())
|
||||||
|
{
|
||||||
|
_properties.ReplayDetails = "正在筛选符合条件的录像…";
|
||||||
|
if (_dataGrid.Items.Count > 0)
|
||||||
|
{
|
||||||
|
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||||
|
_dataGrid.Items.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
|
||||||
|
var result = await Task.Run(() =>
|
||||||
|
{
|
||||||
|
var query = from replay in list.AsParallel().WithCancellation(cancelToken)
|
||||||
|
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
|
||||||
|
select replay.Replay;
|
||||||
|
return query.ToImmutableArray();
|
||||||
|
}, cancelToken);
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_filteredReplays = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
_properties.CurrentReplay = null;
|
||||||
|
_dataGrid.SelectedItem = null;
|
||||||
|
_dataGrid.ItemsSource = _filteredReplays;
|
||||||
_dataGrid.Items.Refresh();
|
_dataGrid.Items.Refresh();
|
||||||
_properties.ReplayDetails = message;
|
_properties.ReplayDetails = message;
|
||||||
|
|
||||||
if (nextSelected != null)
|
if (nextSelected is not null)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
||||||
{
|
{
|
||||||
if (_dataGrid.Items[i] is Replay replay && replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
|
|
||||||
|
if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
|
||||||
{
|
{
|
||||||
_dataGrid.SelectedIndex = i;
|
_dataGrid.SelectedIndex = i;
|
||||||
OnReplaySelectionChanged(null, null);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
|
||||||
{
|
{
|
||||||
LoadReplays();
|
if (!IsLoaded)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_properties.CurrentReplay = null;
|
||||||
|
_image.Source = null;
|
||||||
|
_properties.ReplayDetails = replayDetails;
|
||||||
|
|
||||||
|
// 开始获取小地图
|
||||||
|
var mapPath = replay.MapPath;
|
||||||
|
var minimapTask = _minimapReader.TryReadTargaAsync(replay);
|
||||||
|
|
||||||
|
// 解析录像内容
|
||||||
|
try
|
||||||
|
{
|
||||||
|
replay = await Task.Run(() => new Replay(replay.Path, true));
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay body: \r\n{e}\r\n";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnReplaySelectionChanged(object sender, EventArgs e)
|
// 假如小地图变了(这……),那么重新加载小地图
|
||||||
|
if (replay.MapPath != mapPath)
|
||||||
{
|
{
|
||||||
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
|
minimapTask.Forget();
|
||||||
if (_properties.CurrentReplay == null)
|
minimapTask = _minimapReader.TryReadTargaAsync(replay);
|
||||||
|
}
|
||||||
|
var newDetails = replay.GetDetails(_playerIdentity);
|
||||||
|
if (_properties.ReplayDetails != newDetails)
|
||||||
|
{
|
||||||
|
if (_replayList.Replays.FindIndex(r => r.PathEquals(replay)) is int index)
|
||||||
|
{
|
||||||
|
_replayList = _replayList.SetItem(index, replay.CloneHeader());
|
||||||
|
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
|
||||||
|
await FilterReplays(newDetails, replay.Path, token);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_properties.CurrentReplay = replay;
|
||||||
|
_properties.ReplayDetails = newDetails;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var newSource = await minimapTask;
|
||||||
|
cancelToken.ThrowIfCancellationRequested();
|
||||||
|
_image.Source = newSource;
|
||||||
|
/* _image.Width = source.Width;
|
||||||
|
_image.Height = source.Height; */
|
||||||
|
}
|
||||||
|
catch (Exception e) when (e is not OperationCanceledException)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Uncaught exception when loading minimap: \r\n{e}\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnReplaySelectionChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (_dataGrid.SelectedItem is not Replay replay)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Dispatcher.Invoke(() => { _image.Source = null; });
|
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
|
||||||
|
_properties.ReplayDetails = replay.GetDetails(_playerIdentity);
|
||||||
string GetSizeString(double size)
|
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
|
||||||
{
|
|
||||||
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.FormatRealName(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)
|
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
|
||||||
@ -477,7 +354,6 @@ namespace AnotherReplayReader
|
|||||||
var fileName = openFileDialog.FileName;
|
var fileName = openFileDialog.FileName;
|
||||||
var directoryName = Path.GetDirectoryName(fileName);
|
var directoryName = Path.GetDirectoryName(fileName);
|
||||||
_properties.ReplayFolderPath = directoryName;
|
_properties.ReplayFolderPath = directoryName;
|
||||||
LoadReplays(fileName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@ -488,7 +364,7 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
|
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
|
var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity);
|
||||||
detailsWindow.ShowDialog();
|
detailsWindow.ShowDialog();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -499,12 +375,12 @@ namespace AnotherReplayReader
|
|||||||
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
|
Process.Start(_properties.RA3Exe, $" -replayGame \"{_properties.CurrentReplay}\" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
|
private async void OnFixReplayButton_Click(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
|
var replay = _properties.CurrentReplay ?? throw new InvalidOperationException("Trying to fix a null replay");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var replay = _properties.CurrentReplay;
|
|
||||||
|
|
||||||
var saveFileDialog = new SaveFileDialog
|
var saveFileDialog = new SaveFileDialog
|
||||||
{
|
{
|
||||||
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
||||||
@ -516,11 +392,9 @@ namespace AnotherReplayReader
|
|||||||
var result = saveFileDialog.ShowDialog(this);
|
var result = saveFileDialog.ShowDialog(this);
|
||||||
if (result == true)
|
if (result == true)
|
||||||
{
|
{
|
||||||
using (var file = saveFileDialog.OpenFile())
|
using var file = saveFileDialog.OpenFile();
|
||||||
using (var writer = new BinaryWriter(file))
|
using var writer = new BinaryWriter(file);
|
||||||
{
|
writer.Write(replay);
|
||||||
writer.WriteReplay(replay);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@ -528,42 +402,21 @@ namespace AnotherReplayReader
|
|||||||
MessageBox.Show($"无法修复录像:\r\n{exception}");
|
MessageBox.Show($"无法修复录像:\r\n{exception}");
|
||||||
}
|
}
|
||||||
|
|
||||||
LoadReplays();
|
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||||
|
await _taskQueue.Enqueue(() => LoadReplays(replay.Path, token), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void ReplayFilterBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
private async void ReplayFilterBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
{
|
{
|
||||||
try
|
_filterStrings = _replayFilterBox.Text
|
||||||
{
|
.Split(',', ' ', ',')
|
||||||
var pinyins = _replayFilterBox.Text.Split(',', ' ', ',')
|
|
||||||
.Select(x => x.ToPinyin())
|
.Select(x => x.ToPinyin())
|
||||||
.Where(x => !string.IsNullOrEmpty(x))
|
.Where(x => !string.IsNullOrEmpty(x))
|
||||||
.ToArray();
|
.ToImmutableArray()!;
|
||||||
if (!pinyins.Any())
|
var currentReplayPath = _properties?.CurrentReplay?.Path;
|
||||||
{
|
|
||||||
DisplayReplays();
|
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
|
||||||
return;
|
await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
|
||||||
}
|
|
||||||
var tokenSource = _loadReplaysToken;
|
|
||||||
var token = tokenSource.Token;
|
|
||||||
var list = _pinyinList.ToArray();
|
|
||||||
var result = await Task.Run(() =>
|
|
||||||
{
|
|
||||||
var query = from replay in list.AsParallel()
|
|
||||||
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
|
|
||||||
select replay.Replay;
|
|
||||||
return query.ToArray();
|
|
||||||
});
|
|
||||||
if (token.IsCancellationRequested || _loadReplaysToken != tokenSource)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
DisplayReplays(null, null, result);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Debug.Instance.DebugMessage += $"Exception when filtering replays: {ex}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
using System;
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using Pfim;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using OpenSage.FileFormats.Big;
|
using TechnologyAssembler.Core.IO;
|
||||||
using Pfim;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -24,22 +24,27 @@ namespace AnotherReplayReader
|
|||||||
};
|
};
|
||||||
|
|
||||||
private readonly BigMinimapCache _cache;
|
private readonly BigMinimapCache _cache;
|
||||||
private readonly string _ra3InstallPath;
|
|
||||||
private readonly string _mapFolderPath;
|
private readonly string _mapFolderPath;
|
||||||
private readonly string _modFolderPath;
|
private readonly string _modFolderPath;
|
||||||
|
|
||||||
public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath)
|
public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
_ra3InstallPath = ra3InstallPath;
|
|
||||||
_mapFolderPath = mapFolderPath;
|
_mapFolderPath = mapFolderPath;
|
||||||
_modFolderPath = modFolderPath;
|
_modFolderPath = modFolderPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
public Task<BitmapSource?> TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0)
|
||||||
{
|
{
|
||||||
using (var targa = TryGetTarga(path, mod))
|
var mapPath = replay.MapPath.TrimEnd('/');
|
||||||
|
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
|
||||||
|
var minimapPath = $"{mapPath}/{mapName}_art.tga";
|
||||||
|
return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY));
|
||||||
|
}
|
||||||
|
|
||||||
|
public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
||||||
{
|
{
|
||||||
|
using var targa = TryGetTarga(path, mod);
|
||||||
if (targa == null)
|
if (targa == null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@ -47,7 +52,9 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
var bitmap = BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
||||||
|
bitmap.Freeze();
|
||||||
|
return bitmap;
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@ -55,11 +62,9 @@ namespace AnotherReplayReader
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private Targa TryGetTarga(string path, Mod mod)
|
private Targa? TryGetTarga(string path, Mod mod)
|
||||||
{
|
{
|
||||||
var tga = null as Targa;
|
|
||||||
const string customMapPrefix = "data/maps/internal/";
|
const string customMapPrefix = "data/maps/internal/";
|
||||||
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
||||||
{
|
{
|
||||||
@ -70,12 +75,7 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// now, normalize paths
|
|
||||||
path = path.Replace('/', '\\');
|
|
||||||
|
|
||||||
if (!mod.IsRA3)
|
if (!mod.IsRA3)
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
{
|
||||||
var modSkudefPaths = Enumerable.Empty<string>();
|
var modSkudefPaths = Enumerable.Empty<string>();
|
||||||
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
|
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
|
||||||
@ -83,42 +83,31 @@ namespace AnotherReplayReader
|
|||||||
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
|
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DronePlatform.BuildTechnologyAssembler();
|
||||||
foreach (var modSkudefPath in modSkudefPaths)
|
foreach (var modSkudefPath in modSkudefPaths)
|
||||||
{
|
{
|
||||||
var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
|
try
|
||||||
foreach (var modBigPath in modBigPaths)
|
|
||||||
{
|
{
|
||||||
using (var modBig = new BigArchive(modBigPath))
|
using var fs = new SkuDefFileSystemProvider("modConfig", modSkudefPath);
|
||||||
|
if (!fs.FileExists(path))
|
||||||
{
|
{
|
||||||
var entry = modBig.GetEntry(path);
|
continue;
|
||||||
if (entry != null)
|
|
||||||
{
|
|
||||||
return Targa.Create(entry.Open(), new PfimConfig());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
using var stream = fs.OpenStream(path, VirtualFileModeType.Open);
|
||||||
|
return Targa.Create(stream, new PfimConfig());
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
|
Debug.Instance.DebugMessage += $"Exception when reading minimap from mod bigs:\r\n {exception}\r\n";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
|
if (_cache != null && _cache.TryGetEntry(path, out var big))
|
||||||
{
|
{
|
||||||
using (big)
|
using (big)
|
||||||
{
|
{
|
||||||
return Targa.Create(big.GetEntry(path).Open(), new PfimConfig());
|
return Targa.Create(big, new PfimConfig());
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Directory.Exists(_ra3InstallPath))
|
|
||||||
{
|
|
||||||
var minimapPath = Path.Combine(_mapFolderPath, path);
|
|
||||||
if (File.Exists(minimapPath))
|
|
||||||
{
|
|
||||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
100
ModData.cs
100
ModData.cs
@ -1,8 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -84,20 +81,13 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
internal static class ModData
|
internal static class ModData
|
||||||
{
|
{
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3Factions;
|
private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营");
|
||||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3ARFactions;
|
private static readonly IReadOnlyDictionary<string, IReadOnlyDictionary<int, Faction>> _factions;
|
||||||
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()
|
static ModData()
|
||||||
{
|
{
|
||||||
_ra3Factions = new Dictionary<int, Faction>
|
var ra3Factions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -107,7 +97,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3ARFactions = new Dictionary<int, Faction>
|
var arFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
||||||
{ 2, new Faction(FactionKind.Player, "天琼") },
|
{ 2, new Faction(FactionKind.Player, "天琼") },
|
||||||
@ -123,7 +113,7 @@ namespace AnotherReplayReader
|
|||||||
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
};
|
};
|
||||||
_ra3CoronaFactions = new Dictionary<int, Faction>
|
var coronaFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -134,7 +124,7 @@ namespace AnotherReplayReader
|
|||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
{ 9, new Faction(FactionKind.Player, "神州") },
|
{ 9, new Faction(FactionKind.Player, "神州") },
|
||||||
};
|
};
|
||||||
_ra3DawnFactions = new Dictionary<int, Faction>
|
var dawnFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -147,7 +137,7 @@ namespace AnotherReplayReader
|
|||||||
{ 11, new Faction(FactionKind.Player, "随机") },
|
{ 11, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 12, new Faction(FactionKind.Player, "苏联") },
|
{ 12, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3INSFactions = new Dictionary<int, Faction>
|
var insFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -157,7 +147,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3FSFactions = new Dictionary<int, Faction>
|
var fsFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -167,7 +157,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3EisenreichFactions = new Dictionary<int, Faction>
|
var eisenreichFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -177,7 +167,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3TNWFactions = new Dictionary<int, Faction>
|
var tnwFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -187,7 +177,7 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_ra3WOPFactions = new Dictionary<int, Faction>
|
var wopFactions = new Dictionary<int, Faction>
|
||||||
{
|
{
|
||||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||||
@ -197,61 +187,31 @@ namespace AnotherReplayReader
|
|||||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||||
};
|
};
|
||||||
_unknown = new Faction(FactionKind.Unknown, "未知阵营");
|
_factions = new Dictionary<string, IReadOnlyDictionary<int, Faction>>(StringComparer.CurrentCultureIgnoreCase)
|
||||||
|
{
|
||||||
|
["RA3"] = ra3Factions,
|
||||||
|
["Armor Rush"] = arFactions,
|
||||||
|
["ART"] = arFactions,
|
||||||
|
["corona"] = coronaFactions,
|
||||||
|
["Dawn"] = dawnFactions,
|
||||||
|
["Insurrection"] = insFactions,
|
||||||
|
["1.12+FS"] = fsFactions,
|
||||||
|
["Eisenreich"] = eisenreichFactions,
|
||||||
|
["The New World"] = tnwFactions,
|
||||||
|
["War Of Powers"] = wopFactions
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Faction GetFaction(Mod mod, int factionID)
|
public static Faction GetFaction(Mod mod, int factionId)
|
||||||
{
|
{
|
||||||
new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
|
if (_factions.TryGetValue(mod.ModName, out var table))
|
||||||
|
|
||||||
if
|
|
||||||
(mod.ModName.Equals("RA3"))
|
|
||||||
{
|
{
|
||||||
return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
if (table.TryGetValue(factionId, out var result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
if (mod.ModName.Equals("Armor Rush"))
|
|
||||||
{
|
|
||||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
}
|
||||||
if (mod.ModName.Equals("ART"))
|
|
||||||
{
|
|
||||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("corona"))
|
|
||||||
{
|
|
||||||
return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Dawn"))
|
|
||||||
{
|
|
||||||
return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Insurrection"))
|
|
||||||
{
|
|
||||||
return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("1.12+FS"))
|
|
||||||
{
|
|
||||||
return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("Eisenreich"))
|
|
||||||
{
|
|
||||||
return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("The New World"))
|
|
||||||
{
|
|
||||||
return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
if (mod.ModName.Equals("War Of Powers"))
|
|
||||||
{
|
|
||||||
return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return _unknown;
|
return _unknown;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
using NPinyin;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
class PinyinReplayData
|
|
||||||
{
|
|
||||||
public Replay Replay { get; }
|
|
||||||
public string Name { get; }
|
|
||||||
public string Map { get; }
|
|
||||||
public string Mod { get; }
|
|
||||||
public List<string> Players { get; } = new List<string>();
|
|
||||||
public List<string> RealNames { get; } = new List<string>();
|
|
||||||
public List<string> Factions { get; } = new List<string>();
|
|
||||||
|
|
||||||
public PinyinReplayData(Replay replay, PlayerIdentity playerIdentity)
|
|
||||||
{
|
|
||||||
Replay = replay;
|
|
||||||
Name = replay.FileName.ToPinyin();
|
|
||||||
Map = replay.MapName.ToPinyin();
|
|
||||||
Mod = replay.Mod.ModName.ToPinyin();
|
|
||||||
foreach (var player in replay.Players)
|
|
||||||
{
|
|
||||||
void AddIfNotEmpty(List<string> target, string s)
|
|
||||||
{
|
|
||||||
s = s.ToPinyin();
|
|
||||||
if (!string.IsNullOrEmpty(s))
|
|
||||||
{
|
|
||||||
target.Add(s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddIfNotEmpty(Players, player.PlayerName);
|
|
||||||
AddIfNotEmpty(RealNames, playerIdentity.GetRealName(player.PlayerIP));
|
|
||||||
AddIfNotEmpty(Factions, ModData.GetFaction(replay.Mod, player.FactionID).Name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool MatchPinyin(string pinyin)
|
|
||||||
{
|
|
||||||
return Name.ContainsIgnoreCase(pinyin)
|
|
||||||
|| Players.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1
|
|
||||||
|| RealNames.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1
|
|
||||||
|| Map.ContainsIgnoreCase(pinyin)
|
|
||||||
|| Mod.ContainsIgnoreCase(pinyin)
|
|
||||||
|| Factions.FindIndex(s => s.ContainsIgnoreCase(pinyin)) != -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class PinyinExtensions
|
|
||||||
{
|
|
||||||
public static bool ContainsIgnoreCase(this string self, string s)
|
|
||||||
{
|
|
||||||
return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string ToPinyin(this string self)
|
|
||||||
{
|
|
||||||
string pinyin;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
pinyin = Pinyin.GetPinyin(self);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return pinyin.Replace(" ", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static PinyinReplayData ToPinyin(this Replay replay, PlayerIdentity playerIdentity)
|
|
||||||
{
|
|
||||||
return new PinyinReplayData(replay, playerIdentity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +1,38 @@
|
|||||||
using System;
|
using AnotherReplayReader.Utils;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using System.Web.Script.Serialization;
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
internal sealed class IPAndPlayer
|
internal sealed class IpAndPlayer
|
||||||
{
|
{
|
||||||
public static string SimpleIPToString(uint ip)
|
public static string SimpleIPToString(uint ip)
|
||||||
{
|
{
|
||||||
return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}";
|
return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public uint IP
|
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||||
|
public uint Ip
|
||||||
{
|
{
|
||||||
get => _ip;
|
get => _ip;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
_ip = value;
|
_ip = value;
|
||||||
IPString = SimpleIPToString(_ip);
|
IpString = SimpleIPToString(_ip);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string IPString { get; private set; }
|
public string IpString { get; private set; } = "0.0.0.0";
|
||||||
public string ID
|
public string Id
|
||||||
{
|
{
|
||||||
get => _id;
|
get => _id;
|
||||||
set
|
set
|
||||||
@ -36,116 +41,88 @@ namespace AnotherReplayReader
|
|||||||
_pinyin = _id.ToPinyin();
|
_pinyin = _id.ToPinyin();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public string PinyinID => _pinyin;
|
public string? PinyinId => _pinyin;
|
||||||
|
|
||||||
private uint _ip;
|
private uint _ip;
|
||||||
private string _id;
|
private string _id = string.Empty;
|
||||||
private string _pinyin;
|
private string? _pinyin;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class PlayerIdentity
|
internal class PlayerIdentity
|
||||||
{
|
{
|
||||||
public bool IsUsable => IsListUsable(_list);
|
private const string StoredKey = "pt";
|
||||||
|
private const string IvKey = "jw";
|
||||||
|
private static readonly JsonSerializerOptions _jsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNameCaseInsensitive = true
|
||||||
|
};
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly Cache _cache;
|
||||||
|
private IReadOnlyDictionary<uint, string>? _list;
|
||||||
|
|
||||||
private Cache _cache;
|
public bool IsUsable => Lock.Run(_lock, () => _list is not null);
|
||||||
private volatile IReadOnlyDictionary<uint, string> _list;
|
|
||||||
|
|
||||||
public PlayerIdentity(Cache cache)
|
public PlayerIdentity(Cache cache)
|
||||||
{
|
{
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
|
Task.Run(FetchLocal);
|
||||||
Fetch();
|
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()
|
public Task Fetch()
|
||||||
{
|
{
|
||||||
return Task.Run(() =>
|
return Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
||||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
|
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
|
||||||
|
using var response = await request.GetResponseAsync().ConfigureAwait(false);
|
||||||
|
using var stream = response.GetResponseStream();
|
||||||
|
var list = await DeserializeAsync<List<IpAndPlayer>>(stream, _jsonOptions).ConfigureAwait(false);
|
||||||
|
var converted = list?.ToDictionary(x => x.Ip, x => x.Id);
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_list = converted;
|
||||||
|
}
|
||||||
|
|
||||||
using (var stream = request.GetResponse().GetResponseStream())
|
if (list is null)
|
||||||
using (var reader = new StreamReader(stream))
|
|
||||||
{
|
{
|
||||||
var response = reader.ReadToEnd();
|
_cache.Set<string?>(StoredKey, null);
|
||||||
var serializer = new JavaScriptSerializer();
|
|
||||||
var temp = serializer.Deserialize<List<IPAndPlayer>>(response);
|
|
||||||
if (temp == null)
|
|
||||||
{
|
|
||||||
_list = null;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var converted = temp.ToDictionary(x => x.IP, x => x.ID);
|
|
||||||
_list = converted;
|
|
||||||
|
|
||||||
var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp));
|
using var aes = Aes.Create();
|
||||||
var id = Encoding.UTF8.GetBytes(Auth.ID);
|
using var encryptor = aes.CreateEncryptor(Encoding.UTF8.GetBytes(Auth.ID), aes.IV);
|
||||||
var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
|
using var memory = new MemoryStream();
|
||||||
_cache.Set("pt", base64);
|
using var decryptorStream = new CryptoStream(memory, encryptor, CryptoStreamMode.Write);
|
||||||
}
|
await SerializeAsync(decryptorStream, list, _jsonOptions).ConfigureAwait(false);
|
||||||
|
memory.Flush();
|
||||||
|
_cache.SetValues((StoredKey, Convert.ToBase64String(memory.ToArray())), (IvKey, Convert.ToBase64String(aes.IV)));
|
||||||
}
|
}
|
||||||
catch { }
|
catch { }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<IPAndPlayer> AsSortedList()
|
public List<IpAndPlayer> AsSortedList()
|
||||||
{
|
{
|
||||||
var list = _list;
|
using var locker = new Lock(_lock);
|
||||||
|
|
||||||
if (!IsListUsable(list))
|
return _list?
|
||||||
{
|
.Select((kv) => new IpAndPlayer { Ip = kv.Key, Id = kv.Value })
|
||||||
return new List<IPAndPlayer>();
|
.OrderBy(x => x.Ip)
|
||||||
}
|
.ToList() ?? new(0);
|
||||||
|
|
||||||
return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public string GetRealName(uint ip)
|
public string GetRealName(uint ip)
|
||||||
{
|
{
|
||||||
var list = _list;
|
|
||||||
|
|
||||||
if (!IsListUsable(list))
|
|
||||||
{
|
|
||||||
return string.Empty;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ip == 0)
|
if (ip == 0)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!list.TryGetValue(ip, out var name))
|
using var locker = new Lock(_lock);
|
||||||
|
if (_list is null || !_list.TryGetValue(ip, out var name))
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
@ -158,27 +135,50 @@ namespace AnotherReplayReader
|
|||||||
var name = GetRealName(ip);
|
var name = GetRealName(ip);
|
||||||
if (string.IsNullOrEmpty(name))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
name = IPAndPlayer.SimpleIPToString(ip);
|
name = IpAndPlayer.SimpleIPToString(ip);
|
||||||
}
|
}
|
||||||
return $"({name})";
|
return $"({name})";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string QueryRealNameAndIP(uint ip)
|
public string QueryRealNameAndIP(uint ip)
|
||||||
{
|
{
|
||||||
var list = _list;
|
var ipText = IpAndPlayer.SimpleIPToString(ip);
|
||||||
|
var name = GetRealName(ip);
|
||||||
if (!IsListUsable(list))
|
if (string.IsNullOrEmpty(name))
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return ipText;
|
||||||
|
}
|
||||||
|
return $"{name},{ipText}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ip == 0)
|
private async Task FetchLocal()
|
||||||
{
|
{
|
||||||
return string.Empty;
|
try
|
||||||
|
{
|
||||||
|
var stored = _cache.GetOrDefault(StoredKey, string.Empty);
|
||||||
|
var iv = Convert.FromBase64String(_cache.GetOrDefault(IvKey, string.Empty));
|
||||||
|
if (string.IsNullOrWhiteSpace(stored))
|
||||||
|
{
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = list.TryGetValue(ip, out var realName) ? realName + "," : string.Empty;
|
using var aes = Aes.Create();
|
||||||
return name + IPAndPlayer.SimpleIPToString(ip);
|
using var decryptor = aes.CreateDecryptor(Encoding.UTF8.GetBytes(Auth.ID), iv);
|
||||||
|
using var memory = new MemoryStream(Convert.FromBase64String(stored));
|
||||||
|
using var decryptorStream = new CryptoStream(memory, decryptor, CryptoStreamMode.Read);
|
||||||
|
var cachedTable = await DeserializeAsync<List<IpAndPlayer>>(decryptorStream, _jsonOptions).ConfigureAwait(false);
|
||||||
|
if (cachedTable is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var converted = cachedTable.ToDictionary(x => x.Ip, x => x.Id);
|
||||||
|
converted[0] = "【没有网络连接,正在使用上次保存的数据】";
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_list ??= converted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
433
Replay.cs
433
Replay.cs
@ -1,433 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
|
||||||
{
|
|
||||||
internal sealed class Player
|
|
||||||
{
|
|
||||||
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
{ "E", "简单" },
|
|
||||||
{ "M", "中等" },
|
|
||||||
{ "H", "困难" },
|
|
||||||
{ "B", "凶残" },
|
|
||||||
};
|
|
||||||
|
|
||||||
public string PlayerName { get; private set; }
|
|
||||||
public uint PlayerIP { get; private set; }
|
|
||||||
public 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
|
|
||||||
{
|
|
||||||
var query = from splitted in description.Split(';')
|
|
||||||
where !string.IsNullOrWhiteSpace(splitted)
|
|
||||||
select splitted.Split(new[] { '=' }, 2);
|
|
||||||
entries = query.ToDictionary(x => x[0], x => x[1]);
|
|
||||||
}
|
|
||||||
catch(Exception exception)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{exception}");
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_players = entries["S"].Split(':')
|
|
||||||
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
|
|
||||||
.Select(x => new Player(x.Split(',')))
|
|
||||||
.ToList();
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException($"Failed to parse playerdata from string header of replay {Path}: \r\n{exception}");
|
|
||||||
}
|
|
||||||
|
|
||||||
MapPath = entries["M"].Substring(3);
|
|
||||||
|
|
||||||
HasCommentator = !entries["PC"].Equals("-1");
|
|
||||||
|
|
||||||
var lanFlag = int.Parse(entries["GT"]) == 0;
|
|
||||||
if (lanFlag)
|
|
||||||
{
|
|
||||||
if(_players.First().PlayerIP == 0)
|
|
||||||
{
|
|
||||||
Type = ReplayType.Skirmish;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Type = ReplayType.Lan;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Type = ReplayType.Online;
|
|
||||||
}
|
|
||||||
|
|
||||||
_replaySaverIndex = reader.ReadByte();
|
|
||||||
|
|
||||||
reader.ReadBytes(8); // 8 bit paddings
|
|
||||||
var fileNameLength = reader.ReadInt32();
|
|
||||||
reader.ReadBytes(fileNameLength * 2);
|
|
||||||
reader.ReadBytes(16);
|
|
||||||
var verMagicLength = reader.ReadInt32();
|
|
||||||
reader.ReadBytes(verMagicLength);
|
|
||||||
reader.ReadBytes(85);
|
|
||||||
|
|
||||||
if(reader.BaseStream.Position != _headerSize)
|
|
||||||
{
|
|
||||||
throw new InvalidDataException();
|
|
||||||
}
|
|
||||||
|
|
||||||
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
|
|
||||||
Length = TimeSpan.FromSeconds(Math.Round(Footer.FinalTimeCode / 15.0));
|
|
||||||
}
|
|
||||||
catch(Exception)
|
|
||||||
{
|
|
||||||
Length = null;
|
|
||||||
Footer = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ParseBody()
|
|
||||||
{
|
|
||||||
_body = new List<ReplayChunk>();
|
|
||||||
|
|
||||||
using (var stream = new FileStream(Path, FileMode.Open))
|
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
RawHeader = reader.ReadBytes((int)_headerSize);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var timeCode = reader.ReadUInt32();
|
|
||||||
if (timeCode == ReplayFooter.Terminator)
|
|
||||||
{
|
|
||||||
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
_body.Add(new ReplayChunk(timeCode, reader));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Dictionary<byte, int[]> GetCommandCounts()
|
|
||||||
{
|
|
||||||
var playerCommands = new Dictionary<byte, int[]>();
|
|
||||||
|
|
||||||
foreach(var chunk in _body)
|
|
||||||
{
|
|
||||||
if(chunk.Type != 1)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach(var command in CommandChunk.Parse(chunk))
|
|
||||||
{
|
|
||||||
var commandCount = playerCommands.TryGetValue(command.CommandID, out var current) ? current : new int[Players.Count];
|
|
||||||
if(command.PlayerIndex >= commandCount.Length) // unknown or unparsable command?
|
|
||||||
{
|
|
||||||
commandCount = commandCount
|
|
||||||
.Concat(new int[command.PlayerIndex - commandCount.Length + 1])
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
commandCount[command.PlayerIndex] = commandCount[command.PlayerIndex] + 1;
|
|
||||||
playerCommands[command.CommandID] = commandCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return playerCommands;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
148
ReplayAutoSaver.cs
Normal file
148
ReplayAutoSaver.cs
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader
|
||||||
|
{
|
||||||
|
public static class ReplayAutoSaver
|
||||||
|
{
|
||||||
|
private static int _errorMessageCount = 0;
|
||||||
|
|
||||||
|
public static void SpawnAutoSaveReplaysTask(string replayFolderPath)
|
||||||
|
{
|
||||||
|
Task.Run(() => AutoSaveReplays(replayFolderPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AutoSaveReplays(string replayFolderPath)
|
||||||
|
{
|
||||||
|
const string ourPrefix = "自动保存";
|
||||||
|
|
||||||
|
// filename and last write time
|
||||||
|
var previousFiles = new Dictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
// filename and file size
|
||||||
|
var lastReplays = new Dictionary<string, long>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var changed = (from fileName in Directory.GetFiles(replayFolderPath, "*.RA3Replay")
|
||||||
|
let info = new FileInfo(fileName)
|
||||||
|
where !info.Name.StartsWith(ourPrefix)
|
||||||
|
where !previousFiles.ContainsKey(info.FullName) || previousFiles[info.FullName] != info.LastWriteTimeUtc
|
||||||
|
select info).ToList();
|
||||||
|
|
||||||
|
foreach (var info in changed)
|
||||||
|
{
|
||||||
|
previousFiles[info.FullName] = info.LastWriteTimeUtc;
|
||||||
|
}
|
||||||
|
|
||||||
|
var replays = changed.Select(info =>
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"正在尝试检测已更改的文件:{info.FullName}\r\n";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = info.Open(FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
return new Replay(info.FullName, stream);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}).Where(replay => replay != null);
|
||||||
|
|
||||||
|
var newLastReplays = from replay in replays
|
||||||
|
let threshold = Math.Abs((DateTime.UtcNow - replay.Date).TotalSeconds)
|
||||||
|
let endDate = replay.Date.Add(replay.Length ?? TimeSpan.Zero)
|
||||||
|
let endThreshold = Math.Abs((DateTime.UtcNow - endDate).TotalSeconds)
|
||||||
|
where threshold < 40 || endThreshold < 40
|
||||||
|
select replay;
|
||||||
|
|
||||||
|
var toBeChecked = newLastReplays.ToDictionary(replay => replay.Path, StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var savedLastReplay in lastReplays.Keys)
|
||||||
|
{
|
||||||
|
if (!toBeChecked.ContainsKey(savedLastReplay))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.Open(savedLastReplay, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||||
|
toBeChecked.Add(savedLastReplay, new Replay(savedLastReplay, stream));
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"自动保存录像/检测录像更改时发生错误:{e}\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var kv in toBeChecked)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"正在检测录像更改:{kv.Key}\r\n";
|
||||||
|
var replay = kv.Value;
|
||||||
|
if (lastReplays.TryGetValue(kv.Key, out var fileSize))
|
||||||
|
{
|
||||||
|
if (fileSize == replay.Size)
|
||||||
|
{
|
||||||
|
// skip if size is not changed
|
||||||
|
Debug.Instance.DebugMessage += $"已跳过未更改的录像:{kv.Key}\r\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Debug.Instance.DebugMessage += $"将会自动保存已更改的录像:{kv.Key}\r\n";
|
||||||
|
lastReplays[kv.Key] = replay.Size;
|
||||||
|
|
||||||
|
var date = replay.Date;
|
||||||
|
|
||||||
|
var playerString = $"{replay.NumberOfPlayingPlayers}名玩家";
|
||||||
|
if (replay.NumberOfPlayingPlayers <= 2)
|
||||||
|
{
|
||||||
|
var playingPlayers = from player in replay.Players
|
||||||
|
let faction = ModData.GetFaction(replay.Mod, player.FactionId)
|
||||||
|
where faction.Kind != FactionKind.Observer
|
||||||
|
select $"{player.PlayerName}({faction.Name})";
|
||||||
|
playerString = playingPlayers.Aggregate(string.Empty, (x, y) => x + y);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dateString = $"{date.Year}{date.Month:D2}{date.Day:D2}_{date.Hour:D2}{date.Minute:D2}{date.Second:D2}";
|
||||||
|
var destinationPath = Path.Combine(replayFolderPath, $"{ourPrefix}-{playerString}{dateString}.RA3Replay");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Copy(replay.Path, destinationPath, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new Exception($"复制文件({replay.Path} -> {destinationPath})失败:{e.Message}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
var errorString = $"自动保存录像时出现错误:\r\n{e}\r\n";
|
||||||
|
Debug.Instance.DebugMessage += errorString;
|
||||||
|
if (Interlocked.Increment(ref _errorMessageCount) == 1)
|
||||||
|
{
|
||||||
|
_ = Application.Current.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
MessageBox.Show(errorString);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Decrement(ref _errorMessageCount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(10 * 1000).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,20 +2,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader.ReplayFile
|
||||||
{
|
{
|
||||||
internal static class RA3Commands
|
internal static class RA3Commands
|
||||||
{
|
{
|
||||||
//public static IReadOnlyDictionary<byte, Func<BinaryReader, string>> CommandParser { get; private set; }
|
private static readonly Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||||||
|
private static readonly Dictionary<byte, string> _commandNames;
|
||||||
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()
|
static RA3Commands()
|
||||||
{
|
{
|
||||||
@ -107,7 +100,7 @@ namespace AnotherReplayReader
|
|||||||
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
||||||
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
||||||
(0x10, ParseGarrison0x10, "进驻建筑"),
|
(0x10, ParseGarrison0x10, "进驻建筑"),
|
||||||
(0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"),
|
(0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
|
||||||
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -129,7 +122,24 @@ namespace AnotherReplayReader
|
|||||||
_commandNames.Add(0x4D, "在信标里输入文字");
|
_commandNames.Add(0x4D, "在信标里输入文字");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void UnknownCommandParser(BinaryReader current, byte commandID)
|
public static void ReadCommandData(this BinaryReader reader, byte commandId)
|
||||||
|
{
|
||||||
|
if (_commandParser.TryGetValue(commandId, out var parser))
|
||||||
|
{
|
||||||
|
_commandParser[commandId](reader);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UnknownCommandParser(reader, commandId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetCommandName(byte commandId)
|
||||||
|
{
|
||||||
|
return _commandNames.TryGetValue(commandId, out var storedName) ? storedName : $"(未知指令 0x{commandId:2X})";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UnknownCommandParser(BinaryReader current, byte commandId)
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
@ -143,11 +153,6 @@ namespace AnotherReplayReader
|
|||||||
//return $"(未知指令 0x{commandID:2X})";
|
//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)
|
private static void ParseSpecialChunk0x01(BinaryReader current)
|
||||||
{
|
{
|
||||||
var firstByte = current.ReadByte();
|
var firstByte = current.ReadByte();
|
||||||
@ -214,7 +219,7 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ParseUUID0x33(BinaryReader current)
|
private static void ParseUuid0x33(BinaryReader current)
|
||||||
{
|
{
|
||||||
current.ReadByte();
|
current.ReadByte();
|
||||||
var firstStringLength = (int)current.ReadByte();
|
var firstStringLength = (int)current.ReadByte();
|
||||||
@ -250,7 +255,7 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
internal sealed class CommandChunk
|
internal sealed class CommandChunk
|
||||||
{
|
{
|
||||||
public byte CommandID { get; private set; }
|
public byte CommandId { get; private set; }
|
||||||
public int PlayerIndex { get; private set; }
|
public int PlayerIndex { get; private set; }
|
||||||
|
|
||||||
public static List<CommandChunk> Parse(in ReplayChunk chunk)
|
public static List<CommandChunk> Parse(in ReplayChunk chunk)
|
||||||
@ -260,14 +265,12 @@ namespace AnotherReplayReader
|
|||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
|
|
||||||
int ManglePlayerID(byte ID)
|
static int ManglePlayerId(byte id)
|
||||||
{
|
{
|
||||||
return ID / 8 - 2;
|
return id / 8 - 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
using (var stream = new MemoryStream(chunk.Data))
|
using var reader = chunk.GetReader();
|
||||||
using (var reader = new BinaryReader(stream))
|
|
||||||
{
|
|
||||||
if (reader.ReadByte() != 1)
|
if (reader.ReadByte() != 1)
|
||||||
{
|
{
|
||||||
throw new InvalidDataException("Payload first byte not 1");
|
throw new InvalidDataException("Payload first byte not 1");
|
||||||
@ -277,18 +280,14 @@ namespace AnotherReplayReader
|
|||||||
var numberOfCommands = reader.ReadInt32();
|
var numberOfCommands = reader.ReadInt32();
|
||||||
for (var i = 0; i < numberOfCommands; ++i)
|
for (var i = 0; i < numberOfCommands; ++i)
|
||||||
{
|
{
|
||||||
var commandID = reader.ReadByte();
|
var commandId = reader.ReadByte();
|
||||||
var playerID = reader.ReadByte();
|
var playerId = reader.ReadByte();
|
||||||
if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
|
reader.ReadCommandData(commandId);
|
||||||
|
list.Add(new CommandChunk
|
||||||
{
|
{
|
||||||
RA3Commands.CommandParser[commandID](reader);
|
CommandId = commandId,
|
||||||
}
|
PlayerIndex = ManglePlayerId(playerId)
|
||||||
else
|
});
|
||||||
{
|
|
||||||
RA3Commands.UnknownCommandParser(reader, commandID);
|
|
||||||
}
|
|
||||||
|
|
||||||
list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reader.BaseStream.Position != reader.BaseStream.Length)
|
if (reader.BaseStream.Position != reader.BaseStream.Length)
|
||||||
@ -300,4 +299,3 @@ namespace AnotherReplayReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
41
ReplayFile/Player.cs
Normal file
41
ReplayFile/Player.cs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal sealed class Player
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyDictionary<string, string> ComputerNames = new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
{ "E", "简单" },
|
||||||
|
{ "M", "中等" },
|
||||||
|
{ "H", "困难" },
|
||||||
|
{ "B", "凶残" },
|
||||||
|
};
|
||||||
|
|
||||||
|
public string PlayerName { get; }
|
||||||
|
public uint PlayerIp { get; }
|
||||||
|
public int FactionId { get; }
|
||||||
|
public int Team { get; }
|
||||||
|
|
||||||
|
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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
346
ReplayFile/Replay.cs
Normal file
346
ReplayFile/Replay.cs
Normal file
@ -0,0 +1,346 @@
|
|||||||
|
using AnotherReplayReader.Utils;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal enum ReplayType
|
||||||
|
{
|
||||||
|
Skirmish,
|
||||||
|
Lan,
|
||||||
|
Online
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class Replay
|
||||||
|
{
|
||||||
|
public static readonly byte[] HeaderMagic = Encoding.ASCII.GetBytes("RA3 REPLAY HEADER");
|
||||||
|
public static readonly Dictionary<ReplayType, string> TypeStrings = new()
|
||||||
|
{
|
||||||
|
{ ReplayType.Skirmish, "遭遇战录像" },
|
||||||
|
{ ReplayType.Lan, "局域网录像" },
|
||||||
|
{ ReplayType.Online, "官网录像" },
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly byte _replaySaverIndex;
|
||||||
|
private readonly byte[]? _rawHeader;
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
public DateTime Date { get; }
|
||||||
|
public string MapName { get; }
|
||||||
|
public string MapPath { get; }
|
||||||
|
public IReadOnlyList<Player> Players { get; }
|
||||||
|
public int NumberOfPlayingPlayers { get; }
|
||||||
|
public Mod Mod { get; }
|
||||||
|
public ReplayType Type { get; }
|
||||||
|
public bool HasCommentator { get; }
|
||||||
|
|
||||||
|
public long Size { get; }
|
||||||
|
public ReplayFooter? Footer { get; }
|
||||||
|
public IReadOnlyList<ReplayChunk>? Body { get; }
|
||||||
|
|
||||||
|
public string FileName => System.IO.Path.GetFileNameWithoutExtension(Path);
|
||||||
|
public Player ReplaySaver => Players[_replaySaverIndex];
|
||||||
|
public string TypeString => TypeStrings[Type];
|
||||||
|
public bool HasFooter => Footer != null;
|
||||||
|
public ShortTimeSpan? Length => Footer?.ReplayLength;
|
||||||
|
|
||||||
|
public Replay(string path, bool parseBody = false) :
|
||||||
|
this(path, new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite), parseBody)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Replay(string path, Stream stream, bool parseBody = false)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
|
||||||
|
using var reader = new BinaryReader(stream);
|
||||||
|
Size = reader.BaseStream.Length;
|
||||||
|
|
||||||
|
var headerMagic = reader.ReadBytes(HeaderMagic.Length);
|
||||||
|
if (!headerMagic.SequenceEqual(HeaderMagic))
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"{Path} is not a replay, header is {BitConverter.ToString(headerMagic)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var isSkirmish = reader.ReadByte() == 0x04;
|
||||||
|
reader.ReadBytes(4 * 4); // version and builds
|
||||||
|
reader.ReadBytes(2); // commentary flag, and padding zero byte
|
||||||
|
|
||||||
|
reader.ReadUTF16String(); // title
|
||||||
|
reader.ReadUTF16String(); // description
|
||||||
|
MapName = reader.ReadUTF16String(); // map name
|
||||||
|
reader.ReadUTF16String(); // map id
|
||||||
|
|
||||||
|
NumberOfPlayingPlayers = reader.ReadByte();
|
||||||
|
|
||||||
|
for (var i = 0; i <= NumberOfPlayingPlayers; ++i)
|
||||||
|
{
|
||||||
|
reader.ReadUInt32();
|
||||||
|
reader.ReadUTF16String(); // utf16 player name
|
||||||
|
if (!isSkirmish)
|
||||||
|
{
|
||||||
|
reader.ReadByte(); // team
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = reader.ReadInt32();
|
||||||
|
var cnc3MagicLength = reader.ReadInt32();
|
||||||
|
var headerSize = checked((int)(reader.BaseStream.Position + offset));
|
||||||
|
reader.ReadBytes(cnc3MagicLength);
|
||||||
|
|
||||||
|
var modInfo = reader.ReadBytes(22);
|
||||||
|
Mod = new Mod(Encoding.UTF8.GetString(modInfo));
|
||||||
|
|
||||||
|
var timeStamp = reader.ReadUInt32();
|
||||||
|
Date = DateTimeOffset.FromUnixTimeSeconds(timeStamp).DateTime;
|
||||||
|
|
||||||
|
reader.ReadBytes(31);
|
||||||
|
var descriptionsLength = reader.ReadInt32();
|
||||||
|
var description = Encoding.UTF8.GetString(reader.ReadBytes(descriptionsLength));
|
||||||
|
|
||||||
|
var entries = null as Dictionary<string, string>;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var query = from splitted in description.Split(';')
|
||||||
|
where !string.IsNullOrWhiteSpace(splitted)
|
||||||
|
select splitted.Split(new[] { '=' }, 2);
|
||||||
|
entries = query.ToDictionary(x => x[0], x => x[1]);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException($"Failed to parse string header of replay {Path}: \r\n{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Players = entries["S"].Split(':')
|
||||||
|
.TakeWhile(x => !string.IsNullOrWhiteSpace(x) && x[0] != 'X')
|
||||||
|
.Select(x => new Player(x.Split(',')))
|
||||||
|
.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)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (acutally {reader.BaseStream.Position})\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parseBody)
|
||||||
|
{
|
||||||
|
// jump to footer directly
|
||||||
|
reader.BaseStream.Seek(-4, SeekOrigin.End);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Footer = new ReplayFooter(reader, ReplayFooterOption.SeekToFooter);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to parse replay footer, replay might be corrupt: {e}\r\n";
|
||||||
|
Footer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var body = new List<ReplayChunk>();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var timeCode = reader.ReadUInt32();
|
||||||
|
if (timeCode == ReplayFooter.Terminator)
|
||||||
|
{
|
||||||
|
Footer = new ReplayFooter(reader, ReplayFooterOption.CurrentlyAtFooter);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.Add(new ReplayChunk(timeCode, reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to parse replay body, replay might be corrupt: {e}\r\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[]? rawHeader = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 重新读取原来的整个录像头
|
||||||
|
reader.BaseStream.Seek(0, SeekOrigin.Begin);
|
||||||
|
rawHeader = reader.ReadBytes(headerSize);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: failed to read raw header: {e}\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (rawHeader.Length != headerSize)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Warning: the stored header size {headerSize} isn't correct (raw header length = {rawHeader.Length})\r\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rawHeader = rawHeader;
|
||||||
|
Body = body;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Dictionary<byte, int[]> GetCommandCounts()
|
||||||
|
{
|
||||||
|
if (Body is null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Replay body must be parsed before retrieving command chunks");
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Replay CloneHeader()
|
||||||
|
{
|
||||||
|
using var stream = new MemoryStream();
|
||||||
|
using (var writer = new BinaryWriter(stream, Encoding.UTF8, true))
|
||||||
|
{
|
||||||
|
WriteTo(writer);
|
||||||
|
}
|
||||||
|
stream.Position = 0;
|
||||||
|
return new Replay(Path, stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool PathEquals(Replay replay) => PathEquals(replay.Path);
|
||||||
|
public bool PathEquals(string path) => Path.Equals(path, StringComparison.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public string GetDetails(PlayerIdentity playerIdentity)
|
||||||
|
{
|
||||||
|
static string GetSizeString(double size)
|
||||||
|
{
|
||||||
|
if (size > 1024 * 1024)
|
||||||
|
{
|
||||||
|
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
||||||
|
}
|
||||||
|
return $"{Math.Round(size / 1024)}KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
string size = GetSizeString(Size);
|
||||||
|
string length = Length?.ToString() ?? "录像已损坏,请先修复录像";
|
||||||
|
|
||||||
|
var replaySaver = _replaySaverIndex < Players.Count
|
||||||
|
? ReplaySaver.PlayerName
|
||||||
|
: "[无法获取保存录像的玩家]";
|
||||||
|
|
||||||
|
using var writer = new StringWriter();
|
||||||
|
writer.WriteLine("文件名:{0}", FileName);
|
||||||
|
writer.WriteLine("大小:{0}", size);
|
||||||
|
writer.WriteLine("地图:{0}", MapName);
|
||||||
|
writer.WriteLine("日期:{0}", Date);
|
||||||
|
writer.WriteLine("长度:{0}", length);
|
||||||
|
writer.WriteLine("录像类别:{0}", TypeString);
|
||||||
|
writer.WriteLine("这个文件是{0}保存的", replaySaver);
|
||||||
|
writer.WriteLine("玩家列表:");
|
||||||
|
foreach (var player in Players)
|
||||||
|
{
|
||||||
|
if (player == Players.Last() && player.PlayerName.Equals("post Commentator"))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var factionName = ModData.GetFaction(Mod, player.FactionId).Name;
|
||||||
|
var realName = Type == ReplayType.Lan ? playerIdentity.FormatRealName(player.PlayerIp) : string.Empty;
|
||||||
|
writer.WriteLine($"{player.PlayerName + realName},{factionName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return writer.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer) => WriteTo(writer, false);
|
||||||
|
|
||||||
|
private void WriteTo(BinaryWriter writer, bool skipBody)
|
||||||
|
{
|
||||||
|
if ((_rawHeader is null || Body is null) && !skipBody)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Replay body must be parsed before writing replay");
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(_rawHeader);
|
||||||
|
|
||||||
|
var lastTimeCode = Footer?.FinalTimeCode;
|
||||||
|
if (Body is not null)
|
||||||
|
{
|
||||||
|
foreach (var chunk in Body)
|
||||||
|
{
|
||||||
|
lastTimeCode = chunk.TimeCode;
|
||||||
|
writer.Write(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Footer is not null)
|
||||||
|
{
|
||||||
|
writer.Write(Footer);
|
||||||
|
}
|
||||||
|
else if (lastTimeCode is uint lastTimeCodeValue)
|
||||||
|
{
|
||||||
|
writer.Write(new ReplayFooter(lastTimeCodeValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
37
ReplayFile/ReplayChunk.cs
Normal file
37
ReplayFile/ReplayChunk.cs
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal sealed class ReplayChunk
|
||||||
|
{
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public uint TimeCode { get; }
|
||||||
|
public byte Type { get; }
|
||||||
|
|
||||||
|
public ReplayChunk(uint timeCode, BinaryReader reader)
|
||||||
|
{
|
||||||
|
TimeCode = timeCode; // reader.ReadUInt32();
|
||||||
|
Type = reader.ReadByte();
|
||||||
|
var chunkSize = reader.ReadInt32();
|
||||||
|
_data = reader.ReadBytes(chunkSize);
|
||||||
|
if (reader.ReadInt32() != 0)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException("Replay Chunk not ended with zero");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public BinaryReader GetReader() => new(new MemoryStream(_data, false));
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(TimeCode);
|
||||||
|
writer.Write(Type);
|
||||||
|
writer.Write(_data.Length);
|
||||||
|
writer.Write(_data);
|
||||||
|
writer.Write(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
40
ReplayFile/ReplayExtensions.cs
Normal file
40
ReplayFile/ReplayExtensions.cs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal static class ReplayExtensions
|
||||||
|
{
|
||||||
|
public static string ReadUTF16String(this BinaryReader reader)
|
||||||
|
{
|
||||||
|
var currentBytes = new List<byte>();
|
||||||
|
var lastTwoBytes = Array.Empty<byte>();
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
lastTwoBytes = reader.ReadBytes(2);
|
||||||
|
if (lastTwoBytes.Length != 2)
|
||||||
|
{
|
||||||
|
throw new InvalidDataException();
|
||||||
|
}
|
||||||
|
if (lastTwoBytes.All(x => x == 0))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
currentBytes.AddRange(lastTwoBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Encoding.Unicode.GetString(currentBytes.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Write(this BinaryWriter writer, ReplayChunk chunk) => chunk.WriteTo(writer);
|
||||||
|
|
||||||
|
public static void Write(this BinaryWriter writer, ReplayFooter footer) => footer.WriteTo(writer);
|
||||||
|
|
||||||
|
public static void Write(this BinaryWriter writer, Replay replay) => replay.WriteTo(writer);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
90
ReplayFile/ReplayFooter.cs
Normal file
90
ReplayFile/ReplayFooter.cs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.ReplayFile
|
||||||
|
{
|
||||||
|
internal enum ReplayFooterOption
|
||||||
|
{
|
||||||
|
SeekToFooter,
|
||||||
|
CurrentlyAtFooter,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class ReplayFooter
|
||||||
|
{
|
||||||
|
public const uint Terminator = 0x7FFFFFFF;
|
||||||
|
public static readonly byte[] FooterString = Encoding.ASCII.GetBytes("RA3 REPLAY FOOTER");
|
||||||
|
|
||||||
|
private readonly byte[] _data;
|
||||||
|
|
||||||
|
public uint FinalTimeCode { get; }
|
||||||
|
public TimeSpan ReplayLength => TimeSpan.FromSeconds(FinalTimeCode / 15.0);
|
||||||
|
|
||||||
|
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());
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteTo(BinaryWriter writer)
|
||||||
|
{
|
||||||
|
writer.Write(Terminator);
|
||||||
|
writer.Write(FooterString);
|
||||||
|
writer.Write(FinalTimeCode);
|
||||||
|
writer.Write(_data);
|
||||||
|
writer.Write(FooterString.Length + _data.Length + 8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
BIN
TechnologyAssembler.Core.dll
Normal file
BIN
TechnologyAssembler.Core.dll
Normal file
Binary file not shown.
48
Utils/CancelManager.cs
Normal file
48
Utils/CancelManager.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class CancelManager : IDisposable
|
||||||
|
{
|
||||||
|
private CancellationToken _linkedToken;
|
||||||
|
private CancellationTokenSource? _source;
|
||||||
|
|
||||||
|
public CancellationToken Token => Materialize().Token;
|
||||||
|
|
||||||
|
public void Reset(CancellationToken linked)
|
||||||
|
{
|
||||||
|
_linkedToken = linked;
|
||||||
|
if (_source is { } source)
|
||||||
|
{
|
||||||
|
_source = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
source.Cancel();
|
||||||
|
}
|
||||||
|
catch (AggregateException e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Cancellation failed: {e}";
|
||||||
|
}
|
||||||
|
source.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public CancellationToken ResetAndGetToken(CancellationToken linked)
|
||||||
|
{
|
||||||
|
Reset(linked);
|
||||||
|
return Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose() => Reset(default);
|
||||||
|
|
||||||
|
private CancellationTokenSource Materialize()
|
||||||
|
{
|
||||||
|
if (_source is null)
|
||||||
|
{
|
||||||
|
_source = CancellationTokenSource.CreateLinkedTokenSource(_linkedToken);
|
||||||
|
}
|
||||||
|
return _source;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
23
Utils/CancellableTaskExtensions.cs
Normal file
23
Utils/CancellableTaskExtensions.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal static class CancellableTaskExtensions
|
||||||
|
{
|
||||||
|
public static async Task IgnoreCancel(this Task task)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await task.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Forget(this Task task)
|
||||||
|
{
|
||||||
|
const TaskContinuationOptions flags = TaskContinuationOptions.NotOnRanToCompletion | TaskContinuationOptions.ExecuteSynchronously;
|
||||||
|
task.ContinueWith(t => t.Exception?.Handle(_ => true), flags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
Utils/ImmutableArrayExtensions.cs
Normal file
20
Utils/ImmutableArrayExtensions.cs
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
static class ImmutableArrayExtensions
|
||||||
|
{
|
||||||
|
public static int? FindIndex<T>(this in ImmutableArray<T> a, Predicate<T> p)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < a.Length; ++i)
|
||||||
|
{
|
||||||
|
if (p(a[i]))
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
Utils/Lock.cs
Normal file
32
Utils/Lock.cs
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class Lock : IDisposable
|
||||||
|
{
|
||||||
|
public readonly object LockObject;
|
||||||
|
private bool _disposed = false;
|
||||||
|
|
||||||
|
public Lock(object lockObject)
|
||||||
|
{
|
||||||
|
LockObject = lockObject;
|
||||||
|
Monitor.Enter(LockObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (!_disposed)
|
||||||
|
{
|
||||||
|
Monitor.Exit(LockObject);
|
||||||
|
_disposed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Run<T>(object @lock, Func<T> action)
|
||||||
|
{
|
||||||
|
using var locker = new Lock(@lock);
|
||||||
|
return action();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
Utils/PinyinExtensions.cs
Normal file
27
Utils/PinyinExtensions.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
using NPinyin;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
static class PinyinExtensions
|
||||||
|
{
|
||||||
|
public static bool ContainsIgnoreCase(this string self, string? s)
|
||||||
|
{
|
||||||
|
return s != null && self.IndexOf(s, StringComparison.CurrentCultureIgnoreCase) != -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? ToPinyin(this string self)
|
||||||
|
{
|
||||||
|
string pinyin;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
pinyin = Pinyin.GetPinyin(self);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return pinyin.Replace(" ", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Utils/RegistryUtils.cs
Normal file
28
Utils/RegistryUtils.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.Win32;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal static class RegistryUtils
|
||||||
|
{
|
||||||
|
public static string? Retrieve(RegistryHive hive, string path, string value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var view32 = RegistryKey.OpenBaseKey(hive, RegistryView.Registry32);
|
||||||
|
using var ra3Key = view32.OpenSubKey(path, false);
|
||||||
|
return ra3Key?.GetValue(value) as string;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Debug.Instance.DebugMessage += $"Failed to retrieve registy {hive}:{path}:{value}: {e}";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? RetrieveInRa3(RegistryHive hive, string value)
|
||||||
|
{
|
||||||
|
return Retrieve(hive, @"Software\Electronic Arts\Electronic Arts\Red Alert 3", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
61
Utils/ReplayPinyinList.cs
Normal file
61
Utils/ReplayPinyinList.cs
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
using AnotherReplayReader.ReplayFile;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class ReplayPinyinList
|
||||||
|
{
|
||||||
|
private readonly PlayerIdentity _playerIdentity;
|
||||||
|
public ImmutableArray<Replay> Replays { get; } = ImmutableArray<Replay>.Empty;
|
||||||
|
public ImmutableArray<ReplayPinyinData> Pinyins { get; } = ImmutableArray<ReplayPinyinData>.Empty;
|
||||||
|
|
||||||
|
public ReplayPinyinList(PlayerIdentity playerIdentity) :
|
||||||
|
this(ImmutableArray<Replay>.Empty, playerIdentity)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplayPinyinList(ImmutableArray<Replay> replay, PlayerIdentity playerIdentity) :
|
||||||
|
this(replay,
|
||||||
|
replay.Select(replay => new ReplayPinyinData(replay, playerIdentity)).ToImmutableArray(),
|
||||||
|
playerIdentity)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReplayPinyinList(ImmutableArray<Replay> replay,
|
||||||
|
ImmutableArray<ReplayPinyinData> pinyins,
|
||||||
|
PlayerIdentity playerIdentity)
|
||||||
|
{
|
||||||
|
_playerIdentity = playerIdentity;
|
||||||
|
Replays = replay;
|
||||||
|
Pinyins = pinyins;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReplayPinyinList SetItem(int index, Replay replay)
|
||||||
|
{
|
||||||
|
return new(Replays.SetItem(index, replay),
|
||||||
|
Pinyins.SetItem(index, new(replay, _playerIdentity)),
|
||||||
|
_playerIdentity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ReplayPinyinData
|
||||||
|
{
|
||||||
|
public Replay Replay { get; }
|
||||||
|
public string? PinyinDetails { get; }
|
||||||
|
public string? PinyinMod { get; }
|
||||||
|
|
||||||
|
public ReplayPinyinData(Replay replay, PlayerIdentity playerIdentity)
|
||||||
|
{
|
||||||
|
Replay = replay;
|
||||||
|
PinyinDetails = replay.GetDetails(playerIdentity).ToPinyin();
|
||||||
|
PinyinMod = replay.Mod.ModName.ToPinyin();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MatchPinyin(string? pinyin)
|
||||||
|
{
|
||||||
|
return PinyinDetails?.ContainsIgnoreCase(pinyin) is true
|
||||||
|
|| PinyinMod?.ContainsIgnoreCase(pinyin) is true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
28
Utils/ShortTimeSpan.cs
Normal file
28
Utils/ShortTimeSpan.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
public readonly struct ShortTimeSpan : IEquatable<ShortTimeSpan>, IComparable<ShortTimeSpan>, IComparable
|
||||||
|
{
|
||||||
|
public readonly TimeSpan Value;
|
||||||
|
|
||||||
|
public ShortTimeSpan(TimeSpan value) => Value = value;
|
||||||
|
public static implicit operator TimeSpan(ShortTimeSpan span) => span.Value;
|
||||||
|
public static implicit operator ShortTimeSpan(TimeSpan value) => new(value);
|
||||||
|
public override string ToString() => $"{(int)Value.TotalMinutes:00}:{Value.Seconds:00}";
|
||||||
|
|
||||||
|
public int CompareTo(ShortTimeSpan other) => Value.CompareTo(other.Value);
|
||||||
|
public int CompareTo(object obj) => obj is ShortTimeSpan span ? CompareTo(span) : 1;
|
||||||
|
public override bool Equals(object? obj) => obj is ShortTimeSpan span && Equals(span);
|
||||||
|
public bool Equals(ShortTimeSpan other) => Value.Equals(other.Value);
|
||||||
|
public override int GetHashCode() => Value.GetHashCode();
|
||||||
|
public static bool operator ==(ShortTimeSpan left, ShortTimeSpan right) => left.Equals(right);
|
||||||
|
public static bool operator !=(ShortTimeSpan left, ShortTimeSpan right) => !(left == right);
|
||||||
|
public static bool operator <(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) < 0;
|
||||||
|
public static bool operator >(ShortTimeSpan left, ShortTimeSpan right) => left.CompareTo(right) > 0;
|
||||||
|
}
|
||||||
|
}
|
30
Utils/TaskQueue.cs
Normal file
30
Utils/TaskQueue.cs
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace AnotherReplayReader.Utils
|
||||||
|
{
|
||||||
|
internal class TaskQueue
|
||||||
|
{
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly Dispatcher _dispatcher;
|
||||||
|
private Task _current = Task.CompletedTask;
|
||||||
|
|
||||||
|
public TaskQueue(Dispatcher dispatcher)
|
||||||
|
{
|
||||||
|
_dispatcher = dispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task Enqueue(Func<Task> getTask, CancellationToken cancelToken)
|
||||||
|
{
|
||||||
|
using var locker = new Lock(_lock);
|
||||||
|
_current = _current.ContinueWith(async t =>
|
||||||
|
{
|
||||||
|
await _dispatcher.InvokeAsync(getTask, DispatcherPriority.Background, cancelToken);
|
||||||
|
}, cancelToken);
|
||||||
|
return _current.IgnoreCancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -8,15 +8,15 @@
|
|||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
Title="Window1" Height="450" Width="800">
|
Title="Window1" Height="450" Width="800">
|
||||||
<Grid>
|
<Grid>
|
||||||
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIPFieldChanged" />
|
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIpFieldChanged" />
|
||||||
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="29,28,0,0" TextWrapping="Wrap" Text="IP" VerticalAlignment="Top"/>
|
<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" TextChanged="OnIPFieldChanged" />
|
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" TextChanged="OnIpFieldChanged" />
|
||||||
<TextBlock x:Name="textBlock1" HorizontalAlignment="Left" Margin="205,28,0,0" TextWrapping="Wrap" Text="玩家名称 / 说明" VerticalAlignment="Top"/>
|
<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"/>
|
<Button x:Name="_setIPButton" Content="上传" Margin="705,26,12,0" VerticalAlignment="Top" Click="OnClick"/>
|
||||||
<DataGrid x:Name="_dataGrid" Margin="20,60,12,19" MouseDoubleClick="DataGrid_MouseDoubleClick" IsReadOnly="True">
|
<DataGrid x:Name="_dataGrid" Margin="20,60,12,19" MouseDoubleClick="DataGrid_MouseDoubleClick" IsReadOnly="True">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="IP" Binding="{Binding Path=IPString}"/>
|
<DataGridTextColumn Header="IP" Binding="{Binding Path=IpString}"/>
|
||||||
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=ID}"/>
|
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=Id}"/>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
116
Window1.xaml.cs
116
Window1.xaml.cs
@ -1,14 +1,11 @@
|
|||||||
using NPinyin;
|
using AnotherReplayReader.Utils;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Web;
|
using System.Web;
|
||||||
using System.Web.Script.Serialization;
|
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
|
||||||
namespace AnotherReplayReader
|
namespace AnotherReplayReader
|
||||||
{
|
{
|
||||||
@ -17,7 +14,7 @@ namespace AnotherReplayReader
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
internal partial class Window1 : Window
|
internal partial class Window1 : Window
|
||||||
{
|
{
|
||||||
private PlayerIdentity _identity;
|
private readonly PlayerIdentity _identity;
|
||||||
|
|
||||||
public Window1(PlayerIdentity identity)
|
public Window1(PlayerIdentity identity)
|
||||||
{
|
{
|
||||||
@ -28,80 +25,91 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
private async void Refresh()
|
private async void Refresh()
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
_setIPButton.IsEnabled = false;
|
||||||
try
|
try
|
||||||
{
|
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
{
|
||||||
_dataGrid.Items.Clear();
|
_dataGrid.Items.Clear();
|
||||||
_dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
|
_dataGrid.Items.Add(new IpAndPlayer { Ip = 0, Id = "正在加载..." });
|
||||||
});
|
|
||||||
|
|
||||||
await _identity.Fetch();
|
await _identity.Fetch();
|
||||||
|
await Display();
|
||||||
Display();
|
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"无法加载IP表:{e}"));
|
MessageBox.Show(this, $"无法加载IP表:{e}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_setIPButton.IsEnabled = true;
|
||||||
}
|
}
|
||||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Display(string filter = "", string nameFilter = "")
|
private async Task Display(string filter = "", string nameFilter = "")
|
||||||
|
{
|
||||||
|
var newList = await Task.Run(() =>
|
||||||
{
|
{
|
||||||
var pinyin = nameFilter.ToPinyin();
|
var pinyin = nameFilter.ToPinyin();
|
||||||
var newList = _identity
|
return _identity
|
||||||
.AsSortedList()
|
.AsSortedList()
|
||||||
.Where(x =>
|
.Where(x =>
|
||||||
{
|
{
|
||||||
if (!x.IPString.StartsWith(filter))
|
if (!x.IpString.Contains(filter))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (x.PinyinID?.ContainsIgnoreCase(pinyin) is true)
|
if (x.PinyinId?.ContainsIgnoreCase(pinyin) is true)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return x.ID.ContainsIgnoreCase(nameFilter);
|
return x.Id.ContainsIgnoreCase(nameFilter);
|
||||||
})
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
Dispatcher.Invoke(() =>
|
|
||||||
{
|
|
||||||
_dataGrid.Items.Clear();
|
|
||||||
foreach (var item in newList)
|
|
||||||
{
|
|
||||||
_dataGrid.Items.Add(item);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_dataGrid.Items.Clear();
|
||||||
|
_dataGrid.ItemsSource = newList;
|
||||||
|
_dataGrid.Items.Refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnClick(object sender, RoutedEventArgs e)
|
private async void OnClick(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
_setIPButton.IsEnabled = false;
|
||||||
|
try
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
var ipText = Dispatcher.Invoke(() => _ipField.Text);
|
var ipText = _ipField.Text;
|
||||||
|
|
||||||
if (!IPAddress.TryParse(ipText, out var ip))
|
if (!IPAddress.TryParse(ipText, out var ip))
|
||||||
{
|
{
|
||||||
Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
|
MessageBox.Show(this, "IP 格式不正确");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
var idText = _idField.Text;
|
||||||
var idText = Dispatcher.Invoke(() => _idField.Text);
|
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(idText))
|
if (string.IsNullOrWhiteSpace(idText))
|
||||||
{
|
{
|
||||||
var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
|
var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
|
||||||
if(result != MessageBoxResult.OK)
|
if (choice != MessageBoxResult.OK)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
var result = await UpdateIpTable(ip, idText);
|
||||||
|
if (!result)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "设置 IP 表失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"设置 IP 表时发生错误。\r\n{exception}");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_setIPButton.IsEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<bool> UpdateIpTable(IPAddress ip, string idText)
|
||||||
{
|
{
|
||||||
var bytes = ip.GetAddressBytes();
|
var bytes = ip.GetAddressBytes();
|
||||||
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
||||||
@ -109,34 +117,16 @@ namespace AnotherReplayReader
|
|||||||
|
|
||||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
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}");
|
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
|
||||||
|
using var response = await request.GetResponseAsync().ConfigureAwait(false);
|
||||||
using (var stream = request.GetResponse().GetResponseStream())
|
using var stream = response.GetResponseStream();
|
||||||
using (var reader = new StreamReader(stream))
|
return await DeserializeAsync<bool>(stream).ConfigureAwait(false);
|
||||||
{
|
|
||||||
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}"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||||
|
|
||||||
Refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
|
||||||
{
|
{
|
||||||
var ipText = _ipField.Text;
|
var ipText = _ipField.Text;
|
||||||
var idText = _idField.Text;
|
var idText = _idField.Text;
|
||||||
await Task.Run(() => Display(ipText, idText));
|
await Display(ipText, idText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
private void DataGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user