改了一亿个东西,修了一亿个 BUG
This commit is contained in:
parent
5b907309e0
commit
888ddce4ef
21
About.xaml
21
About.xaml
@ -14,20 +14,21 @@
|
||||
<RowDefinition Height="50"/>
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox x:Name="_idBox" HorizontalAlignment="Left" Margin="54,23,0,10" TextWrapping="Wrap" Text="TextBox" Width="300" BorderBrush="White" RenderTransformOrigin="0.497,0.438" Grid.Row="1"/>
|
||||
<TextBlock x:Name="textBlock" Margin="50,10,45,32" TextWrapping="Wrap" Grid.RowSpan="2">
|
||||
<Run Text="【自动录像机0.6】"/><LineBreak/>
|
||||
<Run Text="本工具目前额外支持以下mod的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
||||
<Run Text="当mod增加新的阵营时,会出现未知阵营和阵营错乱现象"/><LineBreak/>
|
||||
<TextBlock x:Name="textBlock" Margin="36,10,36,32" TextWrapping="Wrap" Grid.RowSpan="2">
|
||||
<Run Text="【自动录像机0.7】"/><LineBreak/>
|
||||
<Run Text="本工具目前额外支持以下 Mod 的读取:AR、日冕、大蜗牛、Ins、FS、WOP、Eisenreich、TNW"/><LineBreak/>
|
||||
<Run Text="当 Mod 增加新的阵营时,会出现未知阵营和阵营错乱现象"/><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/>
|
||||
<Run Text="解析Big的代码来源于OpenSage:"/><LineBreak/>
|
||||
<Hyperlink NavigateUri="https://github.com/OpenSAGE/OpenSAGE"><Run Text="https://github.com/OpenSAGE/OpenSAGE"/></Hyperlink><LineBreak/>
|
||||
<Run Text="解析Tga的代码来源于Pfim:"/><LineBreak/>
|
||||
<Run Text="解析 Big 的代码来源于" />
|
||||
<Hyperlink NavigateUri="https://github.com/Qibbi"><Run Text="Jana Mohn" /></Hyperlink>
|
||||
<Run Text="的 TechnologyAssembler" /><LineBreak />
|
||||
<Run Text="解析 Tga 的代码来源于Pfim:"/><LineBreak/>
|
||||
<Hyperlink NavigateUri="https://github.com/nickbabcock/Pfim"><Run Text="https://github.com/nickbabcock/Pfim"/></Hyperlink><LineBreak/>
|
||||
<Run Text="RA3吧:"/><LineBreak/>
|
||||
<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/>
|
||||
<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>
|
||||
</Grid>
|
||||
</Window>
|
||||
|
@ -1,17 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
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
|
||||
{
|
||||
|
@ -6,6 +6,7 @@
|
||||
<PublishUrl>publish\</PublishUrl>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<UseWPF>true</UseWPF>
|
||||
<IncludePackageReferencesDuringMarkupCompilation>true</IncludePackageReferencesDuringMarkupCompilation>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
@ -20,19 +21,34 @@
|
||||
<Page Remove="publish\**" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Web" />
|
||||
<Reference Include="System.Web.Extensions" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<None Remove="ReplayAutoSaver.cs~RF188cd1d1.TMP" />
|
||||
</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="NPinyin.Core" Version="3.0.0" />
|
||||
<PackageReference Include="OpenSage.FileFormats.Big" Version="1.0.0" />
|
||||
<PackageReference Include="Pfim" Version="0.7.0" />
|
||||
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
|
||||
<PackageReference Include="Pfim" Version="0.10.1" />
|
||||
<PackageReference Include="System.Collections.Immutable" Version="5.0.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>
|
||||
</Project>
|
@ -1,4 +1,4 @@
|
||||
<Window x:Class="AnotherReplayReader.APM"
|
||||
<Window x:Class="AnotherReplayReader.ApmWindow"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
@ -21,7 +21,7 @@
|
||||
Margin="0,19,22,0"
|
||||
VerticalAlignment="Top"
|
||||
Padding="5,2"
|
||||
Visibility="Visible"
|
||||
Visibility="Hidden"
|
||||
Click="OnSetPlayerButtonClick"
|
||||
HorizontalAlignment="Right"/>
|
||||
<DataGrid x:Name="_table"
|
@ -1,8 +1,10 @@
|
||||
using System;
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
@ -30,9 +32,8 @@ namespace AnotherReplayReader
|
||||
{
|
||||
return Value.CompareTo(other.Value);
|
||||
}
|
||||
return NumberValue.Value.CompareTo(other.NumberValue.Value);
|
||||
return NumberValue.Value.CompareTo(other.NumberValue!.Value);
|
||||
}
|
||||
|
||||
return NumberValue.HasValue ? 1 : -1;
|
||||
}
|
||||
|
||||
@ -68,11 +69,8 @@ namespace AnotherReplayReader
|
||||
}
|
||||
_values = values.Select(x => new DataValue(x)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
internal static class DataTableFactory
|
||||
{
|
||||
static public List<DataRow> Get(Replay replay, PlayerIdentity identity)
|
||||
public static List<DataRow> GetList(Replay replay, PlayerIdentity identity)
|
||||
{
|
||||
var list = new List<byte>
|
||||
{
|
||||
@ -136,7 +134,7 @@ namespace AnotherReplayReader
|
||||
};
|
||||
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();
|
||||
@ -155,65 +153,54 @@ namespace AnotherReplayReader
|
||||
return dataList;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// APM.xaml 的交互逻辑
|
||||
/// </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;
|
||||
InitializeComponent();
|
||||
InitializeApmWindowData(replay);
|
||||
}
|
||||
|
||||
private async void InitializeApmWindowData(Replay replay)
|
||||
{
|
||||
if (_identity.IsUsable)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_setPlayerButton.IsEnabled = true;
|
||||
_setPlayerButton.Visibility = Visibility.Visible;
|
||||
});
|
||||
_setPlayerButton.IsEnabled = true;
|
||||
_setPlayerButton.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_setPlayerButton.IsEnabled = false;
|
||||
_setPlayerButton.Visibility = Visibility.Hidden;
|
||||
});
|
||||
_setPlayerButton.IsEnabled = false;
|
||||
_setPlayerButton.Visibility = Visibility.Hidden;
|
||||
}
|
||||
|
||||
Task.Run(() =>
|
||||
try
|
||||
{
|
||||
try
|
||||
var dataList = await Task.Run(() => DataRow.GetList(replay, _identity));
|
||||
_table.Items.Clear();
|
||||
foreach (var row in dataList)
|
||||
{
|
||||
var dataList = DataTableFactory.Get(replay, _identity);
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_table.Items.Clear();
|
||||
foreach (var row in dataList)
|
||||
{
|
||||
_table.Items.Add(row);
|
||||
}
|
||||
_table.Items.Add(row);
|
||||
}
|
||||
|
||||
_table.Columns[1].Header = dataList[0].Player1Value;
|
||||
_table.Columns[2].Header = dataList[0].Player2Value;
|
||||
_table.Columns[3].Header = dataList[0].Player3Value;
|
||||
_table.Columns[4].Header = dataList[0].Player4Value;
|
||||
_table.Columns[5].Header = dataList[0].Player5Value;
|
||||
_table.Columns[6].Header = dataList[0].Player6Value;
|
||||
});
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
||||
});
|
||||
}
|
||||
});
|
||||
_table.Columns[1].Header = dataList[0].Player1Value;
|
||||
_table.Columns[2].Header = dataList[0].Player2Value;
|
||||
_table.Columns[3].Header = dataList[0].Player3Value;
|
||||
_table.Columns[4].Header = dataList[0].Player4Value;
|
||||
_table.Columns[5].Header = dataList[0].Player5Value;
|
||||
_table.Columns[6].Header = dataList[0].Player6Value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
MessageBox.Show($"加载录像信息失败:\r\n{e}");
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSetPlayerButtonClick(object sender, RoutedEventArgs e)
|
||||
@ -222,7 +209,7 @@ namespace AnotherReplayReader
|
||||
window1.ShowDialog();
|
||||
}
|
||||
|
||||
private void OnTableMouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
||||
private void OnTableMouseDoubleClick(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
_table.SelectAll();
|
||||
}
|
3
App.xaml
3
App.xaml
@ -2,7 +2,8 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:AnotherReplayReader"
|
||||
StartupUri="MainWindow.xaml">
|
||||
StartupUri="MainWindow.xaml"
|
||||
ShutdownMode="OnMainWindowClose">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
|
25
App.xaml.cs
25
App.xaml.cs
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
@ -12,5 +9,25 @@ namespace AnotherReplayReader
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
47
Auth.cs
47
Auth.cs
@ -1,37 +1,33 @@
|
||||
using System;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Win32;
|
||||
using System.Text;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal static class Auth
|
||||
{
|
||||
public static string ID { get; private set; }
|
||||
public static string? ID { get; }
|
||||
|
||||
static Auth()
|
||||
{
|
||||
ID = null;
|
||||
|
||||
var windowsID = null as string;
|
||||
string? windowsID;
|
||||
try
|
||||
{
|
||||
using (var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64))
|
||||
using (var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false))
|
||||
{
|
||||
windowsID = winNt?.GetValue("ProductId") as string;
|
||||
}
|
||||
using var view64 = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
||||
using var winNt = view64?.OpenSubKey(@"Software\Microsoft\Windows NT\CurrentVersion", false);
|
||||
windowsID = winNt?.GetValue("ProductId") as string;
|
||||
}
|
||||
catch(Exception)
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var randomKey = null as string;
|
||||
string? randomKey;
|
||||
try
|
||||
{
|
||||
var folderPath = Cache.CacheDirectory;
|
||||
@ -41,34 +37,31 @@ namespace AnotherReplayReader
|
||||
}
|
||||
|
||||
var keyPath = Path.Combine(folderPath, "id");
|
||||
if(!File.Exists(keyPath))
|
||||
if (!File.Exists(keyPath))
|
||||
{
|
||||
File.WriteAllText(keyPath, Guid.NewGuid().ToString());
|
||||
}
|
||||
|
||||
randomKey = File.ReadAllText(keyPath);
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey))
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
using (var sha = SHA256.Create())
|
||||
if (string.IsNullOrWhiteSpace(windowsID) || string.IsNullOrWhiteSpace(randomKey))
|
||||
{
|
||||
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
|
||||
ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
|
||||
return;
|
||||
}
|
||||
|
||||
using var sha = SHA256.Create();
|
||||
var hash = sha.ComputeHash(Encoding.UTF8.GetBytes(windowsID + randomKey));
|
||||
ID = string.Concat(hash.Skip(3).Take(10).Select(x => $"{x:X2}"));
|
||||
}
|
||||
|
||||
public static string GetKey()
|
||||
{
|
||||
if(ID == null)
|
||||
if (ID == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
@ -82,7 +75,7 @@ namespace AnotherReplayReader
|
||||
rng.GetNonZeroBytes(salt);
|
||||
}
|
||||
|
||||
for(var i = 0; i < bytes.Length; ++i)
|
||||
for (var i = 0; i < bytes.Length; ++i)
|
||||
{
|
||||
bytes[i] = (byte)(bytes[i] ^ salt[i % salt.Length]);
|
||||
}
|
||||
|
@ -1,172 +1,35 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AnotherReplayReader.Utils;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Win32;
|
||||
using OpenSage.FileFormats.Big;
|
||||
using TechnologyAssembler.Core.IO;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class BigMinimapCache
|
||||
{
|
||||
private sealed class CacheAdapter
|
||||
{
|
||||
public List<string> Bigs { get; set; }
|
||||
public Dictionary<string, string> MapsToBigs { get; set; }
|
||||
private readonly object _lock = new();
|
||||
private SkuDefFileSystemProvider? _skudefFileSystem = null;
|
||||
|
||||
public CacheAdapter()
|
||||
{
|
||||
Bigs = new List<string>();
|
||||
MapsToBigs = new Dictionary<string, string>();
|
||||
}
|
||||
public BigMinimapCache(string? ra3Directory)
|
||||
{
|
||||
Task.Run(() => Initialize(ra3Directory));
|
||||
}
|
||||
|
||||
//private Cache _cache;
|
||||
private volatile IReadOnlyDictionary<string, string> _mapsToBigs = null;
|
||||
|
||||
public BigMinimapCache(Cache cache, string ra3Directory)
|
||||
public bool TryGetEntry(string path, out Stream? bigEntry)
|
||||
{
|
||||
//_cache = cache;
|
||||
bigEntry = null;
|
||||
|
||||
Task.Run(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(ra3Directory))
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
|
||||
return;
|
||||
}
|
||||
|
||||
var bigSet = ParseSkudefs(Directory.EnumerateFiles(ra3Directory, "*.SkuDef"));
|
||||
|
||||
//var cached = _cache.GetOrDefault("bigsCache", new CacheAdapter());
|
||||
var mapsToBigs = new Dictionary<string, string>();
|
||||
|
||||
foreach (var bigPath in bigSet/*.Where(x => !cached.Bigs.Contains(x))*/)
|
||||
{
|
||||
if (!File.Exists(bigPath))
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Big {bigPath} does not exist.\r\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
Debug.Instance.DebugMessage += $"Trying to add Big {bigPath} to big minimap cache...\r\n";
|
||||
try
|
||||
{
|
||||
using (var big = new BigArchive(bigPath))
|
||||
{
|
||||
foreach (var entry in big.Entries)
|
||||
{
|
||||
if (entry.FullName.EndsWith("_art.tga", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
mapsToBigs[entry.FullName] = bigPath;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
catch(Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when reading big:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
//cached.Bigs = bigSet.ToList();
|
||||
|
||||
//_cache.Set("bigsCache", cached);
|
||||
_mapsToBigs = mapsToBigs; //cached.MapsToBigs;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static HashSet<string> ParseSkudefs(IEnumerable<string> skudefs)
|
||||
{
|
||||
var skudefSet = new HashSet<string>(skudefs.Select(x => x.ToLowerInvariant()));
|
||||
var unreadSkudefs = new HashSet<string>();
|
||||
var bigSet = new HashSet<string>();
|
||||
|
||||
void ReadSkudefLine(string baseDirectory, string line, string expectedCommand, Action<string> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
char[] separators = { ' ', '\t' };
|
||||
line = line.ToLowerInvariant();
|
||||
var splitted = line.Split(separators, 2, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (splitted[0].Equals(expectedCommand))
|
||||
{
|
||||
var path = splitted[1];
|
||||
if (!Path.IsPathRooted(path))
|
||||
{
|
||||
path = Path.Combine(baseDirectory, path);
|
||||
}
|
||||
action(path);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when parsing skudef line:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
void ReadSkudef(string fileName, Action<string, string> onBaseDirectoryAndLine)
|
||||
{
|
||||
try
|
||||
{
|
||||
var baseDirectory = Path.GetDirectoryName(fileName).ToLowerInvariant();
|
||||
foreach (var line in File.ReadAllLines(fileName))
|
||||
{
|
||||
onBaseDirectoryAndLine(baseDirectory, line);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when parsing skudef file:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var skudef in skudefSet)
|
||||
{
|
||||
ReadSkudef(skudef, (baseDirectory, line) =>
|
||||
{
|
||||
ReadSkudefLine(baseDirectory, line, "add-config", x =>
|
||||
{
|
||||
if (!skudefSet.Contains(x))
|
||||
{
|
||||
unreadSkudefs.Add(x);
|
||||
}
|
||||
});
|
||||
|
||||
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var skudef in unreadSkudefs)
|
||||
{
|
||||
ReadSkudef(skudef, (baseDirectory, line) =>
|
||||
{
|
||||
ReadSkudefLine(baseDirectory, line, "add-big", x => bigSet.Add(x));
|
||||
});
|
||||
}
|
||||
|
||||
return bigSet;
|
||||
}
|
||||
|
||||
public bool TryGetBigByEntryPath(string path, out BigArchive big)
|
||||
{
|
||||
big = null;
|
||||
|
||||
if (_mapsToBigs == null)
|
||||
using var locker = new Lock(_lock);
|
||||
if (_skudefFileSystem is not { } fs)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_mapsToBigs.ContainsKey(path))
|
||||
if (!fs.FileExists(path))
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Cannot find big entry [{path}].\r\n";
|
||||
return false;
|
||||
@ -174,58 +37,75 @@ namespace AnotherReplayReader
|
||||
|
||||
try
|
||||
{
|
||||
var bigPath = _mapsToBigs[path];
|
||||
big = new BigArchive(bigPath);
|
||||
if(big.GetEntry(path) == null)
|
||||
{
|
||||
//_cache.Remove("bigsCache");
|
||||
big.Dispose();
|
||||
big = null;
|
||||
return false;
|
||||
}
|
||||
bigEntry = fs.OpenStream(path, VirtualFileModeType.Open);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception during query (entryStream) of BigMinimapCache: \r\n{exception}\r\n";
|
||||
big = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
public byte[] TryReadBytesFromBig(string path)
|
||||
private void Initialize(string? ra3Directory)
|
||||
{
|
||||
if(_mapsToBigs == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if(!_mapsToBigs.ContainsKey(path))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bigPath = _mapsToBigs[path];
|
||||
using (var big = new BigArchive(path))
|
||||
if (ra3Directory is null || !Directory.Exists(ra3Directory))
|
||||
{
|
||||
var entry = big.GetEntry(path);
|
||||
using (var stream = entry.Open())
|
||||
using (var reader = new BinaryReader(stream))
|
||||
{
|
||||
return reader.ReadBytes((int)entry.Length);
|
||||
}
|
||||
Debug.Instance.DebugMessage += $"Will not initialize BigMinimapCache because RA3Directory {ra3Directory} does not exist.\r\n";
|
||||
return;
|
||||
}
|
||||
|
||||
var currentLanguage = RegistryUtils.RetrieveInRa3(RegistryHive.CurrentUser, "Language");
|
||||
var currentLanguage_ = $"{currentLanguage}_";
|
||||
double SkudefVersionSelector(string fullPath)
|
||||
{
|
||||
var value = -1.0;
|
||||
try
|
||||
{
|
||||
const string skudefPrefix = "RA3";
|
||||
const int prefix = 1;
|
||||
const int language = 2;
|
||||
const int majorVersion = 3;
|
||||
const int minorVersion = 4;
|
||||
const int majorVersionMultiplier = 10000;
|
||||
const int correctLanguageBonus = 1000_0000;
|
||||
|
||||
value = 0;
|
||||
var stem = Path.GetFileNameWithoutExtension(fullPath);
|
||||
var match = Regex.Match(stem, @"([^_]*)_([^0-9]*)([0-9]*)\.([0-9]*)");
|
||||
if (!match.Success || match.Groups.Cast<Group>().Any(g => !g.Success))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
value += match.Groups[prefix].Value == skudefPrefix ? 0.1 : 0;
|
||||
value += match.Groups[language].Value == currentLanguage_ ? correctLanguageBonus : 0;
|
||||
value += int.Parse(match.Groups[majorVersion].Value) * majorVersionMultiplier;
|
||||
value += int.Parse(match.Groups[minorVersion].Value);
|
||||
return value;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Failed to retrieve skudef: {e}\r\n";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
var highestSkudef = (from p in Directory.EnumerateFiles(ra3Directory, "*.SkuDef")
|
||||
orderby SkudefVersionSelector(p) descending
|
||||
select p).First();
|
||||
Debug.Instance.DebugMessage += $"Retrieved highest skudef: {highestSkudef}\r\n";
|
||||
|
||||
using var locker = new Lock(_lock);
|
||||
DronePlatform.BuildTechnologyAssembler();
|
||||
_skudefFileSystem = new SkuDefFileSystemProvider("config", highestSkudef);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception during query (bytes) of BigMinimapCache: \r\n{exception}\r\n";
|
||||
//_cache.Remove("bigsCache");
|
||||
Debug.Instance.DebugMessage += $"Exception during initialization of BigMinimapCache: \r\n{exception}\r\n";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
62
Cache.cs
62
Cache.cs
@ -2,11 +2,8 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Web.Script.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
@ -15,24 +12,32 @@ namespace AnotherReplayReader
|
||||
public static string CacheDirectory => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "RA3Bar.Lanyi.AnotherReplayReader");
|
||||
public static string CacheFilePath => Path.Combine(CacheDirectory, "AnotherReplayReader.cache");
|
||||
|
||||
private ConcurrentDictionary<string, string> _storage;
|
||||
private readonly ConcurrentDictionary<string, string> _storage = new();
|
||||
|
||||
public Cache()
|
||||
{
|
||||
try
|
||||
Task.Run(async () =>
|
||||
{
|
||||
if (!Directory.Exists(CacheDirectory))
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
}
|
||||
if (!Directory.Exists(CacheDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(CacheDirectory);
|
||||
}
|
||||
|
||||
var serializer = new JavaScriptSerializer();
|
||||
_storage = serializer.Deserialize<ConcurrentDictionary<string, string>>(File.ReadAllText(CacheFilePath));
|
||||
}
|
||||
catch
|
||||
{
|
||||
_storage = new ConcurrentDictionary<string, string>();
|
||||
}
|
||||
using var cacheStream = File.OpenRead(CacheFilePath);
|
||||
var futureCache = DeserializeAsync<Dictionary<string, string>>(cacheStream).ConfigureAwait(false);
|
||||
if (await futureCache is not { } cached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
foreach (var kv in cached)
|
||||
{
|
||||
_storage.TryAdd(kv.Key, kv.Value);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
|
||||
public T GetOrDefault<T>(string key, in T defaultValue)
|
||||
@ -41,8 +46,7 @@ namespace AnotherReplayReader
|
||||
{
|
||||
if (_storage.TryGetValue(key, out var valueString))
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
return serializer.Deserialize<T>(valueString);
|
||||
return Deserialize<T>(valueString) ?? defaultValue;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
@ -53,8 +57,20 @@ namespace AnotherReplayReader
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
_storage[key] = serializer.Serialize(value);
|
||||
_storage[key] = 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();
|
||||
}
|
||||
catch { }
|
||||
@ -66,12 +82,12 @@ namespace AnotherReplayReader
|
||||
Save();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
public async void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var serializer = new JavaScriptSerializer();
|
||||
File.WriteAllText(CacheFilePath, serializer.Serialize(_storage));
|
||||
using var cacheStream = File.Open(CacheFilePath, FileMode.Create, FileAccess.Write);
|
||||
await SerializeAsync(cacheStream, _storage).ConfigureAwait(false);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Microsoft.Win32;
|
||||
using AnotherReplayReader.Utils;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
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 Proxy DebugMessage
|
||||
{
|
||||
get => new Proxy();
|
||||
set => NewText?.Invoke(value.Payload);
|
||||
get => new();
|
||||
set
|
||||
{
|
||||
var text = value.Payload;
|
||||
using var locker = new Lock(_lock);
|
||||
if (NewText is null || _list.Count > 0)
|
||||
{
|
||||
_list.Add(text);
|
||||
if (NewText is not null)
|
||||
{
|
||||
text = string.Join(string.Empty, _list);
|
||||
_list.Clear();
|
||||
_list.Capacity = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
NewText(text);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action? RequestedSave;
|
||||
@ -36,18 +60,31 @@ namespace AnotherReplayReader
|
||||
/// </summary>
|
||||
public sealed partial class Debug : Window
|
||||
{
|
||||
public static readonly DebugMessageWrapper Instance = new DebugMessageWrapper();
|
||||
public static readonly DebugMessageWrapper Instance = new();
|
||||
private static readonly object _lock = new();
|
||||
private static Debug? _window = null;
|
||||
|
||||
public static void Initialize()
|
||||
{
|
||||
Interlocked.CompareExchange(ref _window, new(), null);
|
||||
_window.InitializeComponent();
|
||||
using var locker = new Lock(_lock);
|
||||
if (_window is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var window = new Debug();
|
||||
window.InitializeComponent();
|
||||
_window = window;
|
||||
Instance.NewText += _window.AppendText;
|
||||
Instance.RequestedSave += _window.ExportInMainWindow;
|
||||
}
|
||||
|
||||
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() { }
|
||||
|
||||
@ -55,7 +92,7 @@ namespace AnotherReplayReader
|
||||
|
||||
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);
|
||||
|
||||
@ -72,11 +109,9 @@ namespace AnotherReplayReader
|
||||
var result = saveFileDialog.ShowDialog(owner);
|
||||
if (result == true)
|
||||
{
|
||||
using (var file = saveFileDialog.OpenFile())
|
||||
using (var writer = new StreamWriter(file))
|
||||
{
|
||||
writer.Write(_textBox.Text);
|
||||
}
|
||||
using var file = saveFileDialog.OpenFile();
|
||||
using var writer = new StreamWriter(file);
|
||||
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": {
|
||||
"OutputFile": null,
|
||||
"TargetPlatform": null,
|
||||
"KeyFile": null,
|
||||
"AlternativeILMergePath": null,
|
||||
"InputAssemblies": [
|
||||
"$(TargetDir)Microsoft.DotNet.PlatformAbstractions.dll",
|
||||
"$(TargetDir)Microsoft.Extensions.DependencyModel.dll",
|
||||
"$(TargetDir)Microsoft.Win32.Primitives.dll",
|
||||
"$(TargetDir)NativeLibraryLoader.dll",
|
||||
"$(TargetDir)netstandard.dll",
|
||||
"$(TargetDir)Newtonsoft.Json.dll",
|
||||
"$(TargetDir)NLog.dll",
|
||||
"$(TargetDir)OpenSage.Core.dll",
|
||||
"$(TargetDir)OpenSage.FileFormats.Big.dll",
|
||||
"$(TargetDir)OpenSage.FileFormats.dll",
|
||||
"$(TargetDir)OpenSage.FileFormats.RefPack.dll",
|
||||
"$(TargetDir)OpenSage.Mathematics.dll",
|
||||
"$(TargetDir)Pfim.dll",
|
||||
"$(TargetDir)System.AppContext.dll",
|
||||
"$(TargetDir)System.Collections.Concurrent.dll",
|
||||
"$(TargetDir)System.Collections.dll",
|
||||
"$(TargetDir)System.Collections.NonGeneric.dll",
|
||||
"$(TargetDir)System.Collections.Specialized.dll",
|
||||
"$(TargetDir)System.ComponentModel.dll",
|
||||
"$(TargetDir)System.ComponentModel.EventBasedAsync.dll",
|
||||
"$(TargetDir)System.ComponentModel.Primitives.dll",
|
||||
"$(TargetDir)System.ComponentModel.TypeConverter.dll",
|
||||
"$(TargetDir)System.Console.dll",
|
||||
"$(TargetDir)System.Data.Common.dll",
|
||||
"$(TargetDir)System.Diagnostics.Contracts.dll",
|
||||
"$(TargetDir)System.Diagnostics.Debug.dll",
|
||||
"$(TargetDir)System.Diagnostics.DiagnosticSource.dll",
|
||||
"$(TargetDir)System.Diagnostics.FileVersionInfo.dll",
|
||||
"$(TargetDir)System.Diagnostics.Process.dll",
|
||||
"$(TargetDir)System.Diagnostics.StackTrace.dll",
|
||||
"$(TargetDir)System.Diagnostics.TextWriterTraceListener.dll",
|
||||
"$(TargetDir)System.Diagnostics.Tools.dll",
|
||||
"$(TargetDir)System.Diagnostics.TraceSource.dll",
|
||||
"$(TargetDir)System.Diagnostics.Tracing.dll",
|
||||
"$(TargetDir)System.Drawing.Primitives.dll",
|
||||
"$(TargetDir)System.Dynamic.Runtime.dll",
|
||||
"$(TargetDir)System.Globalization.Calendars.dll",
|
||||
"$(TargetDir)System.Globalization.dll",
|
||||
"$(TargetDir)System.Globalization.Extensions.dll",
|
||||
"$(TargetDir)System.IO.Compression.dll",
|
||||
"$(TargetDir)System.IO.Compression.ZipFile.dll",
|
||||
"$(TargetDir)System.IO.dll",
|
||||
"$(TargetDir)System.IO.FileSystem.dll",
|
||||
"$(TargetDir)System.IO.FileSystem.DriveInfo.dll",
|
||||
"$(TargetDir)System.IO.FileSystem.Primitives.dll",
|
||||
"$(TargetDir)System.IO.FileSystem.Watcher.dll",
|
||||
"$(TargetDir)System.IO.IsolatedStorage.dll",
|
||||
"$(TargetDir)System.IO.MemoryMappedFiles.dll",
|
||||
"$(TargetDir)System.IO.Pipes.dll",
|
||||
"$(TargetDir)System.IO.UnmanagedMemoryStream.dll",
|
||||
"$(TargetDir)System.Linq.dll",
|
||||
"$(TargetDir)System.Linq.Expressions.dll",
|
||||
"$(TargetDir)System.Linq.Parallel.dll",
|
||||
"$(TargetDir)System.Linq.Queryable.dll",
|
||||
"$(TargetDir)System.Net.Http.dll",
|
||||
"$(TargetDir)System.Net.NameResolution.dll",
|
||||
"$(TargetDir)System.Net.NetworkInformation.dll",
|
||||
"$(TargetDir)System.Net.Ping.dll",
|
||||
"$(TargetDir)System.Net.Primitives.dll",
|
||||
"$(TargetDir)System.Net.Requests.dll",
|
||||
"$(TargetDir)System.Net.Security.dll",
|
||||
"$(TargetDir)System.Net.Sockets.dll",
|
||||
"$(TargetDir)System.Net.WebHeaderCollection.dll",
|
||||
"$(TargetDir)System.Net.WebSockets.Client.dll",
|
||||
"$(TargetDir)System.Net.WebSockets.dll",
|
||||
"$(TargetDir)System.Numerics.Vectors.dll",
|
||||
"$(TargetDir)System.ObjectModel.dll",
|
||||
"$(TargetDir)System.Reflection.dll",
|
||||
"$(TargetDir)System.Reflection.Extensions.dll",
|
||||
"$(TargetDir)System.Reflection.Primitives.dll",
|
||||
"$(TargetDir)System.Resources.Reader.dll",
|
||||
"$(TargetDir)System.Resources.ResourceManager.dll",
|
||||
"$(TargetDir)System.Resources.Writer.dll",
|
||||
"$(TargetDir)System.Runtime.CompilerServices.Unsafe.dll",
|
||||
"$(TargetDir)System.Runtime.CompilerServices.VisualC.dll",
|
||||
"$(TargetDir)System.Runtime.dll",
|
||||
"$(TargetDir)System.Runtime.Extensions.dll",
|
||||
"$(TargetDir)System.Runtime.Handles.dll",
|
||||
"$(TargetDir)System.Runtime.InteropServices.dll",
|
||||
"$(TargetDir)System.Runtime.InteropServices.RuntimeInformation.dll",
|
||||
"$(TargetDir)System.Runtime.Numerics.dll",
|
||||
"$(TargetDir)System.Runtime.Serialization.Formatters.dll",
|
||||
"$(TargetDir)System.Runtime.Serialization.Json.dll",
|
||||
"$(TargetDir)System.Runtime.Serialization.Primitives.dll",
|
||||
"$(TargetDir)System.Runtime.Serialization.Xml.dll",
|
||||
"$(TargetDir)System.Security.Claims.dll",
|
||||
"$(TargetDir)System.Security.Cryptography.Algorithms.dll",
|
||||
"$(TargetDir)System.Security.Cryptography.Csp.dll",
|
||||
"$(TargetDir)System.Security.Cryptography.Encoding.dll",
|
||||
"$(TargetDir)System.Security.Cryptography.Primitives.dll",
|
||||
"$(TargetDir)System.Security.Cryptography.X509Certificates.dll",
|
||||
"$(TargetDir)System.Security.Principal.dll",
|
||||
"$(TargetDir)System.Security.SecureString.dll",
|
||||
"$(TargetDir)System.Text.Encoding.CodePages.dll",
|
||||
"$(TargetDir)System.Text.Encoding.dll",
|
||||
"$(TargetDir)System.Text.Encoding.Extensions.dll",
|
||||
"$(TargetDir)System.Text.RegularExpressions.dll",
|
||||
"$(TargetDir)System.Threading.dll",
|
||||
"$(TargetDir)System.Threading.Overlapped.dll",
|
||||
"$(TargetDir)System.Threading.Tasks.dll",
|
||||
"$(TargetDir)System.Threading.Tasks.Parallel.dll",
|
||||
"$(TargetDir)System.Threading.Thread.dll",
|
||||
"$(TargetDir)System.Threading.ThreadPool.dll",
|
||||
"$(TargetDir)System.Threading.Timer.dll",
|
||||
"$(TargetDir)System.ValueTuple.dll",
|
||||
"$(TargetDir)System.Xml.ReaderWriter.dll",
|
||||
"$(TargetDir)System.Xml.XDocument.dll",
|
||||
"$(TargetDir)System.Xml.XmlDocument.dll",
|
||||
"$(TargetDir)System.Xml.XmlSerializer.dll",
|
||||
"$(TargetDir)System.Xml.XPath.dll",
|
||||
"$(TargetDir)System.Xml.XPath.XDocument.dll"
|
||||
"NPinyin.Core.dll",
|
||||
"Pfim.dll",
|
||||
"TechnologyAssembler.Core.dll"
|
||||
]
|
||||
},
|
||||
"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:local="clr-namespace:AnotherReplayReader"
|
||||
mc:Ignorable="d"
|
||||
Title="MainWindow" Width="800" Height="600">
|
||||
Title="MainWindow" Width="800" Height="600"
|
||||
Loaded="OnMainWindowLoaded">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="20"/>
|
||||
@ -71,7 +72,8 @@
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="文件名" Binding="{Binding Path=FileName}" Width="4*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="玩家人数" Binding="{Binding Path=NumberOfPlayingPlayers}" Width="1.5*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="录像时长" Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="录像时长"
|
||||
Binding="{Binding Path=Length, TargetNullValue='?'}" Width="1.65*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="Mod" Binding="{Binding Path=Mod}" Width="1.5*" IsReadOnly="True"/>
|
||||
<DataGridTextColumn Header="录像日期" Binding="{Binding Path=Date, StringFormat='{}{0:yyyy/MM/dd HH:mm:SS}'}" Width="3*" IsReadOnly="True" />
|
||||
</DataGrid.Columns>
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Microsoft.Win32;
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using AnotherReplayReader.Utils;
|
||||
using Microsoft.Win32;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
@ -17,28 +20,14 @@ namespace AnotherReplayReader
|
||||
{
|
||||
public MainWindowProperties()
|
||||
{
|
||||
string userDataLeafName = null;
|
||||
string replayFolderName = null;
|
||||
try
|
||||
{
|
||||
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";
|
||||
}
|
||||
|
||||
const RegistryHive hklm = RegistryHive.LocalMachine;
|
||||
RA3Directory = RegistryUtils.RetrieveInRa3(hklm, "Install Dir");
|
||||
string? userDataLeafName = RegistryUtils.RetrieveInRa3(hklm, "UserDataLeafName");
|
||||
string? replayFolderName = RegistryUtils.RetrieveInRa3(hklm, "ReplayFolderName");
|
||||
if (string.IsNullOrWhiteSpace(userDataLeafName))
|
||||
{
|
||||
userDataLeafName = "Red Alert 3";
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(replayFolderName))
|
||||
{
|
||||
replayFolderName = "Replays";
|
||||
@ -50,23 +39,21 @@ namespace AnotherReplayReader
|
||||
ModsDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), userDataLeafName, "Mods");
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
// This method is called by the Set accessor of each property.
|
||||
// The CallerMemberName attribute that is applied to the optional propertyName
|
||||
// parameter causes the property name of the caller to be substituted as an argument.
|
||||
private void SetAndNotifyPropertyChanged<T>(ref T target, T newValue, [CallerMemberName] string propertyName = "")
|
||||
{
|
||||
var equals = Equals(target, newValue);
|
||||
target = newValue;
|
||||
if (!equals)
|
||||
if (!Equals(target, newValue))
|
||||
{
|
||||
target = newValue;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public string RA3Directory { get; }
|
||||
public string? RA3Directory { get; }
|
||||
public string RA3ReplayFolderPath { get; }
|
||||
public string RA3Exe => Path.Combine(RA3Directory, "RA3.exe");
|
||||
public string CustomMapsDirectory { get; }
|
||||
@ -78,19 +65,19 @@ namespace AnotherReplayReader
|
||||
set => SetAndNotifyPropertyChanged(ref _replayFolderPath, value);
|
||||
}
|
||||
|
||||
public string ReplayDetails
|
||||
public string? ReplayDetails
|
||||
{
|
||||
get => _replayDetails;
|
||||
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;
|
||||
set
|
||||
@ -103,9 +90,9 @@ namespace AnotherReplayReader
|
||||
}
|
||||
}
|
||||
|
||||
private string _replayFolderPath;
|
||||
private string _replayDetails;
|
||||
private Replay _currentReplay;
|
||||
private string _replayFolderPath = null!;
|
||||
private string? _replayDetails;
|
||||
private Replay? _currentReplay;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -113,346 +100,236 @@ namespace AnotherReplayReader
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private readonly MainWindowProperties _properties = new MainWindowProperties();
|
||||
private readonly Cache _cache = new Cache();
|
||||
private readonly TaskQueue _taskQueue;
|
||||
private readonly MainWindowProperties _properties = new();
|
||||
private readonly Cache _cache = new();
|
||||
private readonly PlayerIdentity _playerIdentity;
|
||||
private readonly BigMinimapCache _minimapCache;
|
||||
private readonly MinimapReader _minimapReader;
|
||||
private List<Replay> _replayList = new List<Replay>();
|
||||
private List<PinyinReplayData> _pinyinList = new List<PinyinReplayData>();
|
||||
private CancellationTokenSource _loadReplaysToken;
|
||||
private readonly CancelManager _cancelLoadReplays = new();
|
||||
private readonly CancelManager _cancelFilterReplays = new();
|
||||
private readonly CancelManager _cancelDisplayReplays = new();
|
||||
private ReplayPinyinList _replayList;
|
||||
private ImmutableArray<string> _filterStrings = ImmutableArray<string>.Empty;
|
||||
private ImmutableArray<Replay> _filteredReplays = ImmutableArray<Replay>.Empty;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
DataContext = _properties;
|
||||
|
||||
var handling = new bool[1] { false };
|
||||
Application.Current.Dispatcher.UnhandledException += (sender, eventArgs) =>
|
||||
{
|
||||
if (handling == null || handling[0])
|
||||
{
|
||||
return;
|
||||
}
|
||||
handling[0] = true;
|
||||
Dispatcher.Invoke(() => MessageBox.Show($"错误:\r\n{eventArgs.Exception}"));
|
||||
};
|
||||
|
||||
_taskQueue = new(Dispatcher);
|
||||
_playerIdentity = new PlayerIdentity(_cache);
|
||||
_minimapCache = new BigMinimapCache(_cache, _properties.RA3Directory);
|
||||
_minimapReader = new MinimapReader(_minimapCache, _properties.RA3Directory, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
||||
_minimapCache = new BigMinimapCache(_properties.RA3Directory);
|
||||
_minimapReader = new MinimapReader(_minimapCache, _properties.CustomMapsDirectory, _properties.ModsDirectory);
|
||||
_replayList = new(_playerIdentity);
|
||||
|
||||
DataContext = _properties;
|
||||
InitializeComponent();
|
||||
Closing += (sender, eventArgs) => _cache.Save();
|
||||
|
||||
LoadReplays();
|
||||
Task.Run(() => AutoSaveReplays(Dispatcher, _properties.RA3ReplayFolderPath));
|
||||
Closing += (sender, eventArgs) =>
|
||||
{
|
||||
_cache.Save();
|
||||
Application.Current.Shutdown();
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task AutoSaveReplays(Dispatcher dispatcher, string replayFolderPath)
|
||||
private async void OnMainWindowLoaded(object sender, EventArgs e)
|
||||
{
|
||||
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);
|
||||
}
|
||||
Debug.Initialize();
|
||||
ReplayAutoSaver.SpawnAutoSaveReplaysTask(_properties.RA3ReplayFolderPath);
|
||||
var token = _cancelLoadReplays.ResetAndGetToken(CancellationToken.None);
|
||||
await _taskQueue.Enqueue(() => LoadReplays(null, token), token);
|
||||
}
|
||||
|
||||
private async void LoadReplays(string nextSelected = null)
|
||||
private async Task LoadReplays(string? nextSelected, CancellationToken cancelToken)
|
||||
{
|
||||
const string loadingString = "正在加载录像列表,请稍候… 已加载 {0} 个录像";
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
var filterToken = _cancelFilterReplays.ResetAndGetToken(cancelToken);
|
||||
_cancelDisplayReplays.Reset(_cancelFilterReplays.Token);
|
||||
|
||||
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;
|
||||
}
|
||||
if (_dataGrid != null)
|
||||
_properties.CurrentReplay = null;
|
||||
_image.Source = null;
|
||||
if (_dataGrid.Items.Count > 0)
|
||||
{
|
||||
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||
_dataGrid.Items.Refresh();
|
||||
}
|
||||
_replayList.Clear();
|
||||
_pinyinList.Clear();
|
||||
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
_replayList = new(_playerIdentity);
|
||||
|
||||
var path = _properties.ReplayFolderPath;
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
DisplayReplays("这个文件夹并不存在。", nextSelected);
|
||||
await FilterReplays("这个文件夹并不存在。", nextSelected, filterToken);
|
||||
return;
|
||||
}
|
||||
|
||||
var (newList, newPinyinList) = await Task.Run(() =>
|
||||
var result = await Task.Run(() =>
|
||||
{
|
||||
var list = new List<Replay>();
|
||||
var pinyinList = new List<PinyinReplayData>();
|
||||
var clock = new Stopwatch();
|
||||
clock.Start();
|
||||
foreach (var replayPath in Directory.EnumerateFiles(path, "*.RA3Replay"))
|
||||
{
|
||||
if (cancelToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
var replay = new Replay(replayPath);
|
||||
list.Add(replay);
|
||||
pinyinList.Add(replay.ToPinyin(_playerIdentity));
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Uncaught exception when loading replay list: \r\n{exception}\r\n";
|
||||
continue;
|
||||
}
|
||||
_ = Dispatcher.Invoke(() => _properties.ReplayDetails = string.Format(loadingString, _replayList.Count));
|
||||
}
|
||||
return (list, pinyinList);
|
||||
});
|
||||
|
||||
_replayList = newList;
|
||||
_pinyinList = newPinyinList;
|
||||
DisplayReplays(string.Empty, nextSelected);
|
||||
}
|
||||
|
||||
private void DisplayReplays(string message = null, string nextSelected = null, Replay[] filtered = null)
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
filtered = filtered ?? _replayList.ToArray();
|
||||
_properties.CurrentReplay = null;
|
||||
_dataGrid.ItemsSource = filtered;
|
||||
_dataGrid.Items.Refresh();
|
||||
_properties.ReplayDetails = message;
|
||||
|
||||
if (nextSelected != null)
|
||||
{
|
||||
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
||||
if (clock.ElapsedMilliseconds > 300)
|
||||
{
|
||||
if (_dataGrid.Items[i] is Replay replay && replay.Path.Equals(nextSelected, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_dataGrid.SelectedIndex = i;
|
||||
OnReplaySelectionChanged(null, null);
|
||||
break;
|
||||
}
|
||||
var text = $"正在加载录像列表,请稍候… 已加载 {list.Count} 个录像";
|
||||
Dispatcher.Invoke(() => _properties.ReplayDetails = text);
|
||||
clock.Restart();
|
||||
}
|
||||
}
|
||||
});
|
||||
return new ReplayPinyinList(list.ToImmutableArray(), _playerIdentity);
|
||||
}, cancelToken);
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
_replayList = result;
|
||||
await FilterReplays(string.Empty, nextSelected, filterToken);
|
||||
}
|
||||
|
||||
private void OnReplayFolderPathBoxTextChanged(object sender, EventArgs e)
|
||||
private async Task FilterReplays(string message, string? nextSelected, CancellationToken cancelToken)
|
||||
{
|
||||
LoadReplays();
|
||||
if (!IsLoaded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
_cancelDisplayReplays.Reset(cancelToken);
|
||||
_filteredReplays = _replayList.Replays;
|
||||
|
||||
_properties.CurrentReplay = null;
|
||||
_dataGrid.SelectedItem = null;
|
||||
_image.Source = null;
|
||||
|
||||
if (_filterStrings.Any())
|
||||
{
|
||||
_properties.ReplayDetails = "正在筛选符合条件的录像…";
|
||||
if (_dataGrid.Items.Count > 0)
|
||||
{
|
||||
_dataGrid.ItemsSource = Array.Empty<Replay>();
|
||||
_dataGrid.Items.Refresh();
|
||||
}
|
||||
|
||||
var (pinyins, list) = (_filterStrings, _replayList.Pinyins);
|
||||
var result = await Task.Run(() =>
|
||||
{
|
||||
var query = from replay in list.AsParallel().WithCancellation(cancelToken)
|
||||
where pinyins.Any(pinyin => replay.MatchPinyin(pinyin))
|
||||
select replay.Replay;
|
||||
return query.ToImmutableArray();
|
||||
}, cancelToken);
|
||||
cancelToken.ThrowIfCancellationRequested();
|
||||
_filteredReplays = result;
|
||||
}
|
||||
|
||||
_properties.CurrentReplay = null;
|
||||
_dataGrid.SelectedItem = null;
|
||||
_dataGrid.ItemsSource = _filteredReplays;
|
||||
_dataGrid.Items.Refresh();
|
||||
_properties.ReplayDetails = message;
|
||||
|
||||
if (nextSelected is not null)
|
||||
{
|
||||
for (var i = 0; i < _dataGrid.Items.Count; ++i)
|
||||
{
|
||||
|
||||
if (_dataGrid.Items[i] is Replay replay && replay.PathEquals(nextSelected))
|
||||
{
|
||||
_dataGrid.SelectedIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnReplaySelectionChanged(object sender, EventArgs e)
|
||||
private async Task DisplayReplayDetail(Replay replay, string replayDetails, CancellationToken cancelToken)
|
||||
{
|
||||
_properties.CurrentReplay = _dataGrid.SelectedItem as Replay;
|
||||
if (_properties.CurrentReplay == null)
|
||||
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;
|
||||
}
|
||||
|
||||
// 假如小地图变了(这……),那么重新加载小地图
|
||||
if (replay.MapPath != mapPath)
|
||||
{
|
||||
minimapTask.Forget();
|
||||
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;
|
||||
}
|
||||
|
||||
Dispatcher.Invoke(() => { _image.Source = null; });
|
||||
|
||||
string GetSizeString(double size)
|
||||
{
|
||||
if (size > 1024 * 1024)
|
||||
{
|
||||
return $"{Math.Round(size / (1024 * 1024), 2)}MB";
|
||||
}
|
||||
return $"{Math.Round(size / 1024)}KB";
|
||||
}
|
||||
|
||||
const string formatA = "文件名:{0}\n大小:{1}\n";
|
||||
const string formatB = "地图:{0}\n日期:{1}\n长度:{2}\n";
|
||||
const string formatC = "录像类别:{0}\n这个文件是{1}保存的\n";
|
||||
const string playerListTitle = "玩家列表:\n";
|
||||
var replay = _properties.CurrentReplay;
|
||||
var sizeString = GetSizeString(replay.Size);
|
||||
var lengthString = "录像已损坏,请先修复录像";
|
||||
if (replay.HasFooter)
|
||||
{
|
||||
lengthString = $"{replay.Length}";
|
||||
}
|
||||
|
||||
var replaySaver = "[无法获取保存录像的玩家]";
|
||||
try
|
||||
{
|
||||
replaySaver = replay.ReplaySaver.PlayerName;
|
||||
}
|
||||
catch { }
|
||||
|
||||
_properties.ReplayDetails = string.Format(formatA, replay.FileName, sizeString);
|
||||
_properties.ReplayDetails += string.Format(formatB, replay.MapName, replay.Date, lengthString);
|
||||
_properties.ReplayDetails += string.Format(formatC, replay.TypeString, replaySaver);
|
||||
_properties.ReplayDetails += playerListTitle;
|
||||
foreach (var player in replay.Players)
|
||||
{
|
||||
if (player == replay.Players.Last() && player.PlayerName.Equals("post Commentator"))
|
||||
{
|
||||
break;
|
||||
}
|
||||
var factionName = ModData.GetFaction(replay.Mod, player.FactionID).Name;
|
||||
var realName = replay.Type == ReplayType.Lan ? _playerIdentity.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";
|
||||
}
|
||||
var token = _cancelDisplayReplays.ResetAndGetToken(_cancelFilterReplays.Token);
|
||||
_properties.ReplayDetails = replay.GetDetails(_playerIdentity);
|
||||
await _taskQueue.Enqueue(() => DisplayReplayDetail(replay, _properties.ReplayDetails, token), token);
|
||||
}
|
||||
|
||||
private void OnAboutButton_Click(object sender, RoutedEventArgs e)
|
||||
@ -477,7 +354,6 @@ namespace AnotherReplayReader
|
||||
var fileName = openFileDialog.FileName;
|
||||
var directoryName = Path.GetDirectoryName(fileName);
|
||||
_properties.ReplayFolderPath = directoryName;
|
||||
LoadReplays(fileName);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@ -488,7 +364,7 @@ namespace AnotherReplayReader
|
||||
|
||||
private void OnDetailsButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
var detailsWindow = new APM(_properties.CurrentReplay, _playerIdentity);
|
||||
var detailsWindow = new ApmWindow(_properties.CurrentReplay!, _playerIdentity);
|
||||
detailsWindow.ShowDialog();
|
||||
}
|
||||
|
||||
@ -499,12 +375,12 @@ namespace AnotherReplayReader
|
||||
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
|
||||
{
|
||||
var replay = _properties.CurrentReplay;
|
||||
|
||||
var saveFileDialog = new SaveFileDialog
|
||||
{
|
||||
Filter = "红警3录像文件 (*.RA3Replay)|*.RA3Replay|所有文件 (*.*)|*.*",
|
||||
@ -516,11 +392,9 @@ namespace AnotherReplayReader
|
||||
var result = saveFileDialog.ShowDialog(this);
|
||||
if (result == true)
|
||||
{
|
||||
using (var file = saveFileDialog.OpenFile())
|
||||
using (var writer = new BinaryWriter(file))
|
||||
{
|
||||
writer.WriteReplay(replay);
|
||||
}
|
||||
using var file = saveFileDialog.OpenFile();
|
||||
using var writer = new BinaryWriter(file);
|
||||
writer.Write(replay);
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
@ -528,42 +402,21 @@ namespace AnotherReplayReader
|
||||
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)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pinyins = _replayFilterBox.Text.Split(',', ' ', ',')
|
||||
.Select(x => x.ToPinyin())
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.ToArray();
|
||||
if (!pinyins.Any())
|
||||
{
|
||||
DisplayReplays();
|
||||
return;
|
||||
}
|
||||
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}";
|
||||
}
|
||||
_filterStrings = _replayFilterBox.Text
|
||||
.Split(',', ' ', ',')
|
||||
.Select(x => x.ToPinyin())
|
||||
.Where(x => !string.IsNullOrEmpty(x))
|
||||
.ToImmutableArray()!;
|
||||
var currentReplayPath = _properties?.CurrentReplay?.Path;
|
||||
|
||||
var token = _cancelFilterReplays.ResetAndGetToken(_cancelLoadReplays.Token);
|
||||
await _taskQueue.Enqueue(() => FilterReplays(string.Empty, currentReplayPath, token), token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
143
MinimapReader.cs
143
MinimapReader.cs
@ -1,13 +1,13 @@
|
||||
using System;
|
||||
using AnotherReplayReader.ReplayFile;
|
||||
using Pfim;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using OpenSage.FileFormats.Big;
|
||||
using Pfim;
|
||||
using TechnologyAssembler.Core.IO;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
@ -24,104 +24,93 @@ namespace AnotherReplayReader
|
||||
};
|
||||
|
||||
private readonly BigMinimapCache _cache;
|
||||
private readonly string _ra3InstallPath;
|
||||
private readonly string _mapFolderPath;
|
||||
private readonly string _modFolderPath;
|
||||
|
||||
public MinimapReader(BigMinimapCache cache, string ra3InstallPath, string mapFolderPath, string modFolderPath)
|
||||
public MinimapReader(BigMinimapCache cache, string mapFolderPath, string modFolderPath)
|
||||
{
|
||||
_cache = cache;
|
||||
_ra3InstallPath = ra3InstallPath;
|
||||
_mapFolderPath = mapFolderPath;
|
||||
_modFolderPath = modFolderPath;
|
||||
}
|
||||
|
||||
public BitmapSource TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
||||
public Task<BitmapSource?> TryReadTargaAsync(Replay replay, double dpiX = 96.0, double dpiY = 96.0)
|
||||
{
|
||||
using (var targa = TryGetTarga(path, mod))
|
||||
{
|
||||
if(targa == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var mapPath = replay.MapPath.TrimEnd('/');
|
||||
var mapName = mapPath.Substring(mapPath.LastIndexOf('/') + 1);
|
||||
var minimapPath = $"{mapPath}/{mapName}_art.tga";
|
||||
return Task.Run(() => TryReadTarga(minimapPath, replay.Mod, dpiX, dpiY));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
|
||||
return null;
|
||||
}
|
||||
public BitmapSource? TryReadTarga(string path, Mod mod, double dpiX = 96.0, double dpiY = 96.0)
|
||||
{
|
||||
using var targa = TryGetTarga(path, mod);
|
||||
if (targa == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var bitmap = BitmapSource.Create(targa.Width, targa.Height, dpiX, dpiY, FormatMapper[targa.Format], null, targa.Data, targa.Stride);
|
||||
bitmap.Freeze();
|
||||
return bitmap;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception creating BitmapSource from minimap:\r\n {exception}\r\n";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Targa TryGetTarga(string path, Mod mod)
|
||||
private Targa? TryGetTarga(string path, Mod mod)
|
||||
{
|
||||
var tga = null as Targa;
|
||||
const string customMapPrefix = "data/maps/internal/";
|
||||
if (Directory.Exists(_mapFolderPath) && path.StartsWith(customMapPrefix))
|
||||
{
|
||||
var minimapPath = Path.Combine(_mapFolderPath, path.Substring(customMapPrefix.Length));
|
||||
if(File.Exists(minimapPath))
|
||||
{
|
||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
// now, normalize paths
|
||||
path = path.Replace('/', '\\');
|
||||
|
||||
if(!mod.IsRA3)
|
||||
{
|
||||
try
|
||||
{
|
||||
var modSkudefPaths = Enumerable.Empty<string>();
|
||||
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
|
||||
{
|
||||
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
|
||||
}
|
||||
|
||||
foreach (var modSkudefPath in modSkudefPaths)
|
||||
{
|
||||
var modBigPaths = BigMinimapCache.ParseSkudefs(new[] { modSkudefPath });
|
||||
foreach (var modBigPath in modBigPaths)
|
||||
{
|
||||
using (var modBig = new BigArchive(modBigPath))
|
||||
{
|
||||
var entry = modBig.GetEntry(path);
|
||||
if (entry != null)
|
||||
{
|
||||
return Targa.Create(entry.Open(), new PfimConfig());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when reading minimap from Skudef big:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
|
||||
if (_cache != null && _cache.TryGetBigByEntryPath(path, out var big))
|
||||
{
|
||||
using (big)
|
||||
{
|
||||
return Targa.Create(big.GetEntry(path).Open(), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
if (Directory.Exists(_ra3InstallPath))
|
||||
{
|
||||
var minimapPath = Path.Combine(_mapFolderPath, path);
|
||||
if (File.Exists(minimapPath))
|
||||
{
|
||||
return Targa.Create(File.ReadAllBytes(minimapPath), new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
if (!mod.IsRA3)
|
||||
{
|
||||
var modSkudefPaths = Enumerable.Empty<string>();
|
||||
foreach (var subFolder in Directory.EnumerateDirectories(_modFolderPath))
|
||||
{
|
||||
modSkudefPaths = modSkudefPaths.Concat(Directory.EnumerateFiles(subFolder, $"{mod.ModName}_{mod.ModVersion}.SkuDef"));
|
||||
}
|
||||
|
||||
DronePlatform.BuildTechnologyAssembler();
|
||||
foreach (var modSkudefPath in modSkudefPaths)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var fs = new SkuDefFileSystemProvider("modConfig", modSkudefPath);
|
||||
if (!fs.FileExists(path))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
using var stream = fs.OpenStream(path, VirtualFileModeType.Open);
|
||||
return Targa.Create(stream, new PfimConfig());
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Debug.Instance.DebugMessage += $"Exception when reading minimap from mod bigs:\r\n {exception}\r\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_cache != null && _cache.TryGetEntry(path, out var big))
|
||||
{
|
||||
using (big)
|
||||
{
|
||||
return Targa.Create(big, new PfimConfig());
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
108
ModData.cs
108
ModData.cs
@ -1,8 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
@ -42,13 +39,13 @@ namespace AnotherReplayReader
|
||||
|
||||
public int CompareTo(object other)
|
||||
{
|
||||
if(!(other is Mod))
|
||||
if (!(other is Mod))
|
||||
{
|
||||
return GetType().FullName.CompareTo(other.GetType().FullName);
|
||||
}
|
||||
|
||||
var otherMod = (Mod)other;
|
||||
if(IsRA3 != otherMod.IsRA3)
|
||||
if (IsRA3 != otherMod.IsRA3)
|
||||
{
|
||||
if (IsRA3)
|
||||
{
|
||||
@ -84,20 +81,13 @@ namespace AnotherReplayReader
|
||||
|
||||
internal static class ModData
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3Factions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3ARFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3CoronaFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3DawnFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3INSFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3FSFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3EisenreichFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3TNWFactions;
|
||||
private static readonly IReadOnlyDictionary<int, Faction> _ra3WOPFactions;
|
||||
private static readonly Faction _unknown;
|
||||
private static readonly Faction _unknown = new(FactionKind.Unknown, "未知阵营");
|
||||
private static readonly IReadOnlyDictionary<string, IReadOnlyDictionary<int, Faction>> _factions;
|
||||
|
||||
|
||||
static ModData()
|
||||
{
|
||||
_ra3Factions = new Dictionary<int, Faction>
|
||||
var ra3Factions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -106,8 +96,8 @@ namespace AnotherReplayReader
|
||||
{ 4, new Faction(FactionKind.Player, "盟军") },
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3ARFactions = new Dictionary<int, Faction>
|
||||
};
|
||||
var arFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 1, new Faction(FactionKind.Player, "阳炎") },
|
||||
{ 2, new Faction(FactionKind.Player, "天琼") },
|
||||
@ -123,7 +113,7 @@ namespace AnotherReplayReader
|
||||
{ 14, new Faction(FactionKind.Player, "涅墨西斯") },
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
};
|
||||
_ra3CoronaFactions = new Dictionary<int, Faction>
|
||||
var coronaFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -134,7 +124,7 @@ namespace AnotherReplayReader
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
{ 9, new Faction(FactionKind.Player, "神州") },
|
||||
};
|
||||
_ra3DawnFactions = new Dictionary<int, Faction>
|
||||
var dawnFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -147,7 +137,7 @@ namespace AnotherReplayReader
|
||||
{ 11, new Faction(FactionKind.Player, "随机") },
|
||||
{ 12, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3INSFactions = new Dictionary<int, Faction>
|
||||
var insFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -157,7 +147,7 @@ namespace AnotherReplayReader
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3FSFactions = new Dictionary<int, Faction>
|
||||
var fsFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -167,7 +157,7 @@ namespace AnotherReplayReader
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3EisenreichFactions = new Dictionary<int, Faction>
|
||||
var eisenreichFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -177,7 +167,7 @@ namespace AnotherReplayReader
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3TNWFactions = new Dictionary<int, Faction>
|
||||
var tnwFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -187,7 +177,7 @@ namespace AnotherReplayReader
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_ra3WOPFactions = new Dictionary<int, Faction>
|
||||
var wopFactions = new Dictionary<int, Faction>
|
||||
{
|
||||
{ 0, new Faction(FactionKind.Player, "AI") },
|
||||
{ 1, new Faction(FactionKind.Observer, "观察员") },
|
||||
@ -197,61 +187,31 @@ namespace AnotherReplayReader
|
||||
{ 7, new Faction(FactionKind.Player, "随机") },
|
||||
{ 8, new Faction(FactionKind.Player, "苏联") },
|
||||
};
|
||||
_unknown = new Faction(FactionKind.Unknown, "未知阵营");
|
||||
_factions = new Dictionary<string, IReadOnlyDictionary<int, Faction>>(StringComparer.CurrentCultureIgnoreCase)
|
||||
{
|
||||
["RA3"] = ra3Factions,
|
||||
["Armor Rush"] = arFactions,
|
||||
["ART"] = arFactions,
|
||||
["corona"] = coronaFactions,
|
||||
["Dawn"] = dawnFactions,
|
||||
["Insurrection"] = insFactions,
|
||||
["1.12+FS"] = fsFactions,
|
||||
["Eisenreich"] = eisenreichFactions,
|
||||
["The New World"] = tnwFactions,
|
||||
["War Of Powers"] = wopFactions
|
||||
};
|
||||
}
|
||||
|
||||
public static Faction GetFaction(Mod mod, int factionID)
|
||||
public static Faction GetFaction(Mod mod, int factionId)
|
||||
{
|
||||
new Faction(FactionKind.Player, mod.ModName + "-" + factionID);
|
||||
|
||||
if
|
||||
(mod.ModName.Equals("RA3"))
|
||||
if (_factions.TryGetValue(mod.ModName, out var table))
|
||||
{
|
||||
return _ra3Factions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
if (table.TryGetValue(factionId, out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (mod.ModName.Equals("Armor Rush"))
|
||||
{
|
||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("ART"))
|
||||
{
|
||||
return _ra3ARFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("corona"))
|
||||
{
|
||||
return _ra3CoronaFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("Dawn"))
|
||||
{
|
||||
return _ra3DawnFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("Insurrection"))
|
||||
{
|
||||
return _ra3INSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("1.12+FS"))
|
||||
{
|
||||
return _ra3FSFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("Eisenreich"))
|
||||
{
|
||||
return _ra3EisenreichFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("The New World"))
|
||||
{
|
||||
return _ra3TNWFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
if (mod.ModName.Equals("War Of Powers"))
|
||||
{
|
||||
return _ra3WOPFactions.TryGetValue(factionID, out var faction) ? faction : _unknown;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
return _unknown;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Script.Serialization;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
internal sealed class IPAndPlayer
|
||||
internal sealed class IpAndPlayer
|
||||
{
|
||||
public static string SimpleIPToString(uint ip)
|
||||
{
|
||||
return $"{ip / 256 / 256 / 256}.{ip / 256 / 256 % 256}.{ip / 256 % 256}.{ip % 256}";
|
||||
}
|
||||
|
||||
public uint IP
|
||||
[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)]
|
||||
public uint Ip
|
||||
{
|
||||
get => _ip;
|
||||
set
|
||||
{
|
||||
_ip = value;
|
||||
IPString = SimpleIPToString(_ip);
|
||||
IpString = SimpleIPToString(_ip);
|
||||
}
|
||||
}
|
||||
public string IPString { get; private set; }
|
||||
public string ID
|
||||
public string IpString { get; private set; } = "0.0.0.0";
|
||||
public string Id
|
||||
{
|
||||
get => _id;
|
||||
set
|
||||
@ -36,116 +41,88 @@ namespace AnotherReplayReader
|
||||
_pinyin = _id.ToPinyin();
|
||||
}
|
||||
}
|
||||
public string PinyinID => _pinyin;
|
||||
public string? PinyinId => _pinyin;
|
||||
|
||||
private uint _ip;
|
||||
private string _id;
|
||||
private string _pinyin;
|
||||
private string _id = string.Empty;
|
||||
private string? _pinyin;
|
||||
}
|
||||
|
||||
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;
|
||||
private volatile IReadOnlyDictionary<uint, string> _list;
|
||||
public bool IsUsable => Lock.Run(_lock, () => _list is not null);
|
||||
|
||||
public PlayerIdentity(Cache cache)
|
||||
{
|
||||
_cache = cache;
|
||||
Task.Run(FetchLocal);
|
||||
Fetch();
|
||||
|
||||
try
|
||||
{
|
||||
var stored = _cache.GetOrDefault("pt", string.Empty);
|
||||
if (string.IsNullOrWhiteSpace(stored))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bytes = Convert.FromBase64String(stored);
|
||||
var id = Encoding.UTF8.GetBytes(Auth.ID);
|
||||
var data = Encoding.UTF8.GetString(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
|
||||
var serializer = new JavaScriptSerializer();
|
||||
var cachedTable = serializer.Deserialize<List<IPAndPlayer>>(data);
|
||||
if (cachedTable == null)
|
||||
{
|
||||
_list = null;
|
||||
return;
|
||||
}
|
||||
var converted = cachedTable.ToDictionary(x => x.IP, x => x.ID);
|
||||
converted[0] = "【没有网络连接,正在使用上次保存的数据】";
|
||||
|
||||
_list = converted;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
public static bool IsListUsable(IReadOnlyDictionary<uint, string> list)
|
||||
{
|
||||
return list != null && list.Count != 0;
|
||||
}
|
||||
|
||||
public Task Fetch()
|
||||
{
|
||||
return Task.Run(() =>
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=getTable&key={key}");
|
||||
|
||||
using (var stream = request.GetResponse().GetResponseStream())
|
||||
using (var reader = new StreamReader(stream))
|
||||
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)
|
||||
{
|
||||
var response = reader.ReadToEnd();
|
||||
var serializer = new JavaScriptSerializer();
|
||||
var temp = serializer.Deserialize<List<IPAndPlayer>>(response);
|
||||
if (temp == null)
|
||||
{
|
||||
_list = null;
|
||||
return;
|
||||
}
|
||||
var converted = temp.ToDictionary(x => x.IP, x => x.ID);
|
||||
_list = converted;
|
||||
|
||||
var bytes = Encoding.UTF8.GetBytes(serializer.Serialize(temp));
|
||||
var id = Encoding.UTF8.GetBytes(Auth.ID);
|
||||
var base64 = Convert.ToBase64String(bytes.Select((x, i) => (byte)(x ^ id[i % id.Length])).ToArray());
|
||||
_cache.Set("pt", base64);
|
||||
}
|
||||
|
||||
if (list is null)
|
||||
{
|
||||
_cache.Set<string?>(StoredKey, null);
|
||||
return;
|
||||
}
|
||||
|
||||
using var aes = Aes.Create();
|
||||
using var encryptor = aes.CreateEncryptor(Encoding.UTF8.GetBytes(Auth.ID), aes.IV);
|
||||
using var memory = new MemoryStream();
|
||||
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 { }
|
||||
});
|
||||
}
|
||||
|
||||
public List<IPAndPlayer> AsSortedList()
|
||||
public List<IpAndPlayer> AsSortedList()
|
||||
{
|
||||
var list = _list;
|
||||
using var locker = new Lock(_lock);
|
||||
|
||||
if (!IsListUsable(list))
|
||||
{
|
||||
return new List<IPAndPlayer>();
|
||||
}
|
||||
|
||||
return _list.Select((kv) => new IPAndPlayer { IP = kv.Key, ID = kv.Value }).OrderBy(x => x.IP).ToList();
|
||||
return _list?
|
||||
.Select((kv) => new IpAndPlayer { Ip = kv.Key, Id = kv.Value })
|
||||
.OrderBy(x => x.Ip)
|
||||
.ToList() ?? new(0);
|
||||
}
|
||||
|
||||
public string GetRealName(uint ip)
|
||||
{
|
||||
var list = _list;
|
||||
|
||||
if (!IsListUsable(list))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (ip == 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
@ -158,27 +135,50 @@ namespace AnotherReplayReader
|
||||
var name = GetRealName(ip);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
name = IPAndPlayer.SimpleIPToString(ip);
|
||||
name = IpAndPlayer.SimpleIPToString(ip);
|
||||
}
|
||||
return $"({name})";
|
||||
}
|
||||
|
||||
public string QueryRealNameAndIP(uint ip)
|
||||
{
|
||||
var list = _list;
|
||||
|
||||
if (!IsListUsable(list))
|
||||
var ipText = IpAndPlayer.SimpleIPToString(ip);
|
||||
var name = GetRealName(ip);
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
return string.Empty;
|
||||
return ipText;
|
||||
}
|
||||
return $"{name},{ipText}";
|
||||
}
|
||||
|
||||
if (ip == 0)
|
||||
private async Task FetchLocal()
|
||||
{
|
||||
try
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
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;
|
||||
return name + IPAndPlayer.SimpleIPToString(ip);
|
||||
using var aes = Aes.Create();
|
||||
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.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
namespace AnotherReplayReader.ReplayFile
|
||||
{
|
||||
internal static class RA3Commands
|
||||
{
|
||||
//public static IReadOnlyDictionary<byte, Func<BinaryReader, string>> CommandParser { get; private set; }
|
||||
|
||||
public static IReadOnlyDictionary<byte, Action<BinaryReader>> CommandParser => _commandParser;
|
||||
public static IReadOnlyDictionary<byte, string> CommandNames => _commandNames;
|
||||
|
||||
private static Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||||
private static Dictionary<byte, string> _commandNames;
|
||||
private static readonly Dictionary<byte, Action<BinaryReader>> _commandParser;
|
||||
private static readonly Dictionary<byte, string> _commandNames;
|
||||
|
||||
static RA3Commands()
|
||||
{
|
||||
@ -31,13 +24,13 @@ namespace AnotherReplayReader
|
||||
};
|
||||
}
|
||||
|
||||
Action<BinaryReader> variableSizeParser (byte command, int offset)
|
||||
Action<BinaryReader> variableSizeParser(byte command, int offset)
|
||||
{
|
||||
return (BinaryReader current) =>
|
||||
{
|
||||
var totalBytes = 2;
|
||||
totalBytes += current.ReadBytes(offset - 2).Length;
|
||||
for(var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
|
||||
for (var x = current.ReadByte(); x != 0xFF; x = current.ReadByte())
|
||||
{
|
||||
totalBytes += 1;
|
||||
|
||||
@ -107,7 +100,7 @@ namespace AnotherReplayReader
|
||||
(0x02, ParseSetRallyPoint0x02, "设计集结点"),
|
||||
(0x0C, ParseUngarrison0x0C, "从进驻的建筑撤出(?)"),
|
||||
(0x10, ParseGarrison0x10, "进驻建筑"),
|
||||
(0x33, ParseUUID0x33, "[游戏自动生成的UUID指令]"),
|
||||
(0x33, ParseUuid0x33, "[游戏自动生成的UUID指令]"),
|
||||
(0x4B, ParsePlaceBeacon0x4B, "信标")
|
||||
};
|
||||
|
||||
@ -129,9 +122,26 @@ namespace AnotherReplayReader
|
||||
_commandNames.Add(0x4D, "在信标里输入文字");
|
||||
}
|
||||
|
||||
public static void UnknownCommandParser(BinaryReader current, byte commandID)
|
||||
public static void ReadCommandData(this BinaryReader reader, byte commandId)
|
||||
{
|
||||
while(true)
|
||||
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)
|
||||
{
|
||||
var value = current.ReadByte();
|
||||
if (value == 0xFF)
|
||||
@ -143,11 +153,6 @@ namespace AnotherReplayReader
|
||||
//return $"(未知指令 0x{commandID:2X})";
|
||||
}
|
||||
|
||||
public static string GetCommandName(byte commandID)
|
||||
{
|
||||
return CommandNames.TryGetValue(commandID, out var storedName) ? storedName : $"(未知指令 0x{commandID:2X})";
|
||||
}
|
||||
|
||||
private static void ParseSpecialChunk0x01(BinaryReader current)
|
||||
{
|
||||
var firstByte = current.ReadByte();
|
||||
@ -157,7 +162,7 @@ namespace AnotherReplayReader
|
||||
}
|
||||
|
||||
var sixthByte = current.ReadBytes(5).Last();
|
||||
if(sixthByte == 0xFF)
|
||||
if (sixthByte == 0xFF)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -165,7 +170,7 @@ namespace AnotherReplayReader
|
||||
var sixteenthByte = current.ReadBytes(10).Last();
|
||||
var size = (int)(sixteenthByte + 1) * 4 + 14;
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if(lastByte != 0xFF)
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
@ -198,23 +203,23 @@ namespace AnotherReplayReader
|
||||
{
|
||||
var type = current.ReadByte();
|
||||
var size = -1;
|
||||
if(type == 0x14)
|
||||
if (type == 0x14)
|
||||
{
|
||||
size = 9;
|
||||
}
|
||||
else if(type == 0x04)
|
||||
else if (type == 0x04)
|
||||
{
|
||||
size = 10;
|
||||
}
|
||||
|
||||
var lastByte = current.ReadBytes(size).Last();
|
||||
if(lastByte != 0xFF)
|
||||
if (lastByte != 0xFF)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseUUID0x33(BinaryReader current)
|
||||
private static void ParseUuid0x33(BinaryReader current)
|
||||
{
|
||||
current.ReadByte();
|
||||
var firstStringLength = (int)current.ReadByte();
|
||||
@ -231,11 +236,11 @@ namespace AnotherReplayReader
|
||||
{
|
||||
var type = current.ReadByte();
|
||||
var size = -1;
|
||||
if(type == 0x04)
|
||||
if (type == 0x04)
|
||||
{
|
||||
size = 5;
|
||||
}
|
||||
else if(type == 0x07)
|
||||
else if (type == 0x07)
|
||||
{
|
||||
size = 13;
|
||||
}
|
||||
@ -250,7 +255,7 @@ namespace AnotherReplayReader
|
||||
|
||||
internal sealed class CommandChunk
|
||||
{
|
||||
public byte CommandID { get; private set; }
|
||||
public byte CommandId { get; private set; }
|
||||
public int PlayerIndex { get; private set; }
|
||||
|
||||
public static List<CommandChunk> Parse(in ReplayChunk chunk)
|
||||
@ -260,44 +265,37 @@ namespace AnotherReplayReader
|
||||
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 = new BinaryReader(stream))
|
||||
using var reader = chunk.GetReader();
|
||||
if (reader.ReadByte() != 1)
|
||||
{
|
||||
if(reader.ReadByte() != 1)
|
||||
{
|
||||
throw new InvalidDataException("Payload first byte not 1");
|
||||
}
|
||||
|
||||
var list = new List<CommandChunk>();
|
||||
var numberOfCommands = reader.ReadInt32();
|
||||
for(var i = 0; i < numberOfCommands; ++i)
|
||||
{
|
||||
var commandID = reader.ReadByte();
|
||||
var playerID = reader.ReadByte();
|
||||
if (RA3Commands.CommandParser.TryGetValue(commandID, out var parser))
|
||||
{
|
||||
RA3Commands.CommandParser[commandID](reader);
|
||||
}
|
||||
else
|
||||
{
|
||||
RA3Commands.UnknownCommandParser(reader, commandID);
|
||||
}
|
||||
|
||||
list.Add(new CommandChunk { CommandID = commandID, PlayerIndex = ManglePlayerID(playerID) });
|
||||
}
|
||||
|
||||
if(reader.BaseStream.Position != reader.BaseStream.Length)
|
||||
{
|
||||
throw new InvalidDataException("Payload not fully parsed");
|
||||
}
|
||||
|
||||
return list;
|
||||
throw new InvalidDataException("Payload first byte not 1");
|
||||
}
|
||||
|
||||
var list = new List<CommandChunk>();
|
||||
var numberOfCommands = reader.ReadInt32();
|
||||
for (var i = 0; i < numberOfCommands; ++i)
|
||||
{
|
||||
var commandId = reader.ReadByte();
|
||||
var playerId = reader.ReadByte();
|
||||
reader.ReadCommandData(commandId);
|
||||
list.Add(new CommandChunk
|
||||
{
|
||||
CommandId = commandId,
|
||||
PlayerIndex = ManglePlayerId(playerId)
|
||||
});
|
||||
}
|
||||
|
||||
if (reader.BaseStream.Position != reader.BaseStream.Length)
|
||||
{
|
||||
throw new InvalidDataException("Payload not fully parsed");
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
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"
|
||||
Title="Window1" Height="450" Width="800">
|
||||
<Grid>
|
||||
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIPFieldChanged" />
|
||||
<TextBox x:Name="_ipField" HorizontalAlignment="Left" Height="16" Margin="45,27,0,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" Width="155" TextChanged="OnIpFieldChanged" />
|
||||
<TextBlock x:Name="textBlock" HorizontalAlignment="Left" Margin="29,28,0,0" TextWrapping="Wrap" Text="IP" VerticalAlignment="Top"/>
|
||||
<TextBox x:Name="_idField" Height="16" Margin="294,27,92,0" TextWrapping="Wrap" Text="" VerticalAlignment="Top" 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"/>
|
||||
<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.Columns>
|
||||
<DataGridTextColumn Header="IP" Binding="{Binding Path=IPString}"/>
|
||||
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=ID}"/>
|
||||
<DataGridTextColumn Header="IP" Binding="{Binding Path=IpString}"/>
|
||||
<DataGridTextColumn Header="玩家名称 / 说明" Binding="{Binding Path=Id}"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
|
150
Window1.xaml.cs
150
Window1.xaml.cs
@ -1,14 +1,11 @@
|
||||
using NPinyin;
|
||||
using AnotherReplayReader.Utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Web;
|
||||
using System.Web.Script.Serialization;
|
||||
using System.Windows;
|
||||
using static System.Text.Json.JsonSerializer;
|
||||
|
||||
namespace AnotherReplayReader
|
||||
{
|
||||
@ -17,7 +14,7 @@ namespace AnotherReplayReader
|
||||
/// </summary>
|
||||
internal partial class Window1 : Window
|
||||
{
|
||||
private PlayerIdentity _identity;
|
||||
private readonly PlayerIdentity _identity;
|
||||
|
||||
public Window1(PlayerIdentity identity)
|
||||
{
|
||||
@ -28,115 +25,108 @@ namespace AnotherReplayReader
|
||||
|
||||
private async void Refresh()
|
||||
{
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
||||
_setIPButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
_dataGrid.Items.Clear();
|
||||
_dataGrid.Items.Add(new IPAndPlayer { IP = 0, ID = "正在加载..." });
|
||||
});
|
||||
|
||||
_dataGrid.Items.Clear();
|
||||
_dataGrid.Items.Add(new IpAndPlayer { Ip = 0, Id = "正在加载..." });
|
||||
await _identity.Fetch();
|
||||
|
||||
Display();
|
||||
await Display();
|
||||
}
|
||||
catch(Exception e)
|
||||
catch (Exception e)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"无法加载IP表:{e}"));
|
||||
MessageBox.Show(this, $"无法加载IP表:{e}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_setIPButton.IsEnabled = true;
|
||||
}
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = true);
|
||||
}
|
||||
|
||||
private void Display(string filter = "", string nameFilter = "")
|
||||
private async Task Display(string filter = "", string nameFilter = "")
|
||||
{
|
||||
var pinyin = nameFilter.ToPinyin();
|
||||
var newList = _identity
|
||||
.AsSortedList()
|
||||
.Where(x =>
|
||||
{
|
||||
if (!x.IPString.StartsWith(filter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (x.PinyinID?.ContainsIgnoreCase(pinyin) is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return x.ID.ContainsIgnoreCase(nameFilter);
|
||||
})
|
||||
.ToArray();
|
||||
Dispatcher.Invoke(() =>
|
||||
var newList = await Task.Run(() =>
|
||||
{
|
||||
_dataGrid.Items.Clear();
|
||||
foreach (var item in newList)
|
||||
{
|
||||
_dataGrid.Items.Add(item);
|
||||
}
|
||||
var pinyin = nameFilter.ToPinyin();
|
||||
return _identity
|
||||
.AsSortedList()
|
||||
.Where(x =>
|
||||
{
|
||||
if (!x.IpString.Contains(filter))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (x.PinyinId?.ContainsIgnoreCase(pinyin) is true)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return x.Id.ContainsIgnoreCase(nameFilter);
|
||||
})
|
||||
.ToArray();
|
||||
});
|
||||
|
||||
_dataGrid.Items.Clear();
|
||||
_dataGrid.ItemsSource = newList;
|
||||
_dataGrid.Items.Refresh();
|
||||
}
|
||||
|
||||
private async void OnClick(object sender, RoutedEventArgs e)
|
||||
{
|
||||
Dispatcher.Invoke(() => _setIPButton.IsEnabled = false);
|
||||
|
||||
await Task.Run(() =>
|
||||
_setIPButton.IsEnabled = false;
|
||||
try
|
||||
{
|
||||
var ipText = Dispatcher.Invoke(() => _ipField.Text);
|
||||
|
||||
var ipText = _ipField.Text;
|
||||
if (!IPAddress.TryParse(ipText, out var ip))
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, "IP格式不正确"));
|
||||
MessageBox.Show(this, "IP 格式不正确");
|
||||
return;
|
||||
}
|
||||
|
||||
var idText = Dispatcher.Invoke(() => _idField.Text);
|
||||
|
||||
var idText = _idField.Text;
|
||||
if (string.IsNullOrWhiteSpace(idText))
|
||||
{
|
||||
var result = Dispatcher.Invoke(() => MessageBox.Show(this, "你没填输入任何说明,是否确认继续?", "注意", MessageBoxButton.OKCancel));
|
||||
if(result != MessageBoxResult.OK)
|
||||
var choice = MessageBox.Show(this, "没有输入任何关于该玩家的说明,是否继续?", "注意", MessageBoxButton.OKCancel);
|
||||
if (choice != MessageBoxResult.OK)
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
var result = await UpdateIpTable(ip, idText);
|
||||
if (!result)
|
||||
{
|
||||
var bytes = ip.GetAddressBytes();
|
||||
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
||||
var text = HttpUtility.UrlEncode(idText);
|
||||
|
||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
|
||||
|
||||
using (var stream = request.GetResponse().GetResponseStream())
|
||||
using (var reader = new StreamReader(stream))
|
||||
{
|
||||
var response = reader.ReadToEnd();
|
||||
var serializer = new JavaScriptSerializer();
|
||||
var result = serializer.Deserialize<bool>(response);
|
||||
if(!result)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, "设置IP表失败"));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Dispatcher.Invoke(() => MessageBox.Show(this, $"设置IP表时发生错误。\r\n{exception}"));
|
||||
MessageBox.Show(this, "设置 IP 表失败");
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
Refresh();
|
||||
Refresh();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
MessageBox.Show(this, $"设置 IP 表时发生错误。\r\n{exception}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_setIPButton.IsEnabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnIPFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
private static async Task<bool> UpdateIpTable(IPAddress ip, string idText)
|
||||
{
|
||||
var bytes = ip.GetAddressBytes();
|
||||
var ipNum = (uint)bytes[0] * 256 * 256 * 256 + bytes[1] * 256 * 256 + bytes[2] * 256 + bytes[3];
|
||||
var text = HttpUtility.UrlEncode(idText);
|
||||
|
||||
var key = HttpUtility.UrlEncode(Auth.GetKey());
|
||||
var request = WebRequest.Create($"https://lanyi.altervista.org/playertable/playertable.php?do=setIP&ip={ipNum}&id={text}&key={key}");
|
||||
using var response = await request.GetResponseAsync().ConfigureAwait(false);
|
||||
using var stream = response.GetResponseStream();
|
||||
return await DeserializeAsync<bool>(stream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async void OnIpFieldChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
|
||||
{
|
||||
var ipText = _ipField.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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user